@sap/cds 6.6.2 → 6.7.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 (130) hide show
  1. package/CHANGELOG.md +59 -2
  2. package/README.md +1 -1
  3. package/apis/connect.d.ts +11 -4
  4. package/apis/core.d.ts +1 -1
  5. package/apis/csn.d.ts +1 -0
  6. package/apis/internal/inference.d.ts +15 -2
  7. package/apis/log.d.ts +10 -0
  8. package/apis/serve.d.ts +4 -9
  9. package/apis/services.d.ts +86 -19
  10. package/bin/build/buildTaskEngine.js +16 -42
  11. package/bin/build/constants.js +4 -2
  12. package/bin/build/provider/buildTaskProviderInternal.js +117 -85
  13. package/bin/build/provider/hana/index.js +6 -1
  14. package/bin/build/provider/mtx-extension/index.js +74 -34
  15. package/bin/build/provider/mtx-sidecar/index.js +3 -3
  16. package/bin/build/provider/nodejs/index.js +2 -2
  17. package/bin/build/util.js +63 -14
  18. package/bin/cds-serve.js +6 -0
  19. package/bin/cds.js +20 -4
  20. package/bin/deploy/to-hana/cfUtil.js +15 -1
  21. package/bin/mtx/in-cds.js +2 -9
  22. package/bin/plugins.js +31 -0
  23. package/bin/serve.js +12 -12
  24. package/lib/compile/etc/_localized.js +1 -1
  25. package/lib/compile/for/lean_drafts.js +22 -6
  26. package/lib/compile/for/nodejs.js +4 -1
  27. package/lib/compile/load.js +4 -2
  28. package/lib/core/index.js +35 -15
  29. package/lib/dbs/cds-deploy.js +129 -133
  30. package/lib/env/cds-env.js +25 -17
  31. package/lib/env/cds-requires.js +10 -40
  32. package/lib/env/compat.js +12 -0
  33. package/lib/env/defaults.js +17 -9
  34. package/lib/env/plugins.js +29 -0
  35. package/lib/env/schemas/cds-rc.json +14 -0
  36. package/lib/index.js +3 -0
  37. package/lib/log/cds-log.js +7 -4
  38. package/lib/ql/CREATE.js +1 -1
  39. package/lib/ql/DELETE.js +1 -1
  40. package/lib/ql/DROP.js +3 -3
  41. package/lib/ql/INSERT.js +1 -1
  42. package/lib/ql/Query.js +14 -6
  43. package/lib/ql/SELECT.js +8 -2
  44. package/lib/ql/UPDATE.js +1 -1
  45. package/lib/ql/Whereable.js +1 -1
  46. package/lib/ql/cds-ql.js +1 -9
  47. package/lib/req/cds-context.js +1 -4
  48. package/lib/req/request.js +63 -2
  49. package/lib/req/response.js +3 -2
  50. package/lib/srv/bindings.js +69 -71
  51. package/lib/srv/cds-connect.js +4 -1
  52. package/lib/srv/cds-serve.js +4 -0
  53. package/lib/srv/middlewares/index.js +37 -6
  54. package/lib/srv/protocols/_legacy.js +1 -1
  55. package/lib/srv/protocols/index.js +1 -1
  56. package/lib/srv/srv-api.js +4 -6
  57. package/lib/srv/srv-dispatch.js +4 -3
  58. package/lib/srv/srv-handlers.js +1 -1
  59. package/lib/srv/srv-methods.js +8 -2
  60. package/lib/utils/cds-test.js +4 -1
  61. package/libx/_runtime/audit/Service.js +8 -9
  62. package/libx/_runtime/audit/generic/personal/index.js +1 -1
  63. package/libx/_runtime/audit/generic/personal/utils.js +1 -1
  64. package/libx/_runtime/audit/utils/v2.js +17 -20
  65. package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +2 -0
  66. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +11 -4
  67. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +0 -1
  68. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/readToCQN.js +3 -3
  69. package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +1 -1
  70. package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +4 -4
  71. package/libx/_runtime/cds-services/adapter/odata-v4/utils/oDataConfiguration.js +2 -2
  72. package/libx/_runtime/cds-services/adapter/odata-v4/utils/result.js +1 -1
  73. package/libx/_runtime/cds-services/services/Service.js +1 -1
  74. package/libx/_runtime/cds-services/util/assert.js +41 -65
  75. package/libx/_runtime/common/code-ext/WorkerPool.js +90 -0
  76. package/libx/_runtime/common/code-ext/WorkerReq.js +0 -4
  77. package/libx/_runtime/common/code-ext/execute.js +28 -18
  78. package/libx/_runtime/common/code-ext/handlers.js +5 -4
  79. package/libx/_runtime/common/code-ext/worker.js +45 -3
  80. package/libx/_runtime/common/code-ext/workerQueryExecutor.js +8 -7
  81. package/libx/_runtime/common/composition/delete.js +1 -1
  82. package/libx/_runtime/common/composition/update.js +3 -5
  83. package/libx/_runtime/common/generic/auth/expand.js +1 -1
  84. package/libx/_runtime/common/generic/auth/readOnly.js +5 -4
  85. package/libx/_runtime/common/generic/auth/restrict.js +7 -2
  86. package/libx/_runtime/common/generic/crud.js +12 -1
  87. package/libx/_runtime/common/generic/etag.js +11 -3
  88. package/libx/_runtime/common/generic/input.js +8 -6
  89. package/libx/_runtime/common/generic/paging.js +25 -8
  90. package/libx/_runtime/common/generic/put.js +1 -1
  91. package/libx/_runtime/common/generic/sorting.js +0 -1
  92. package/libx/_runtime/common/i18n/messages.properties +1 -0
  93. package/libx/_runtime/common/utils/cqn.js +5 -1
  94. package/libx/_runtime/common/utils/cqn2cqn4sql.js +2 -2
  95. package/libx/_runtime/common/utils/resolveView.js +14 -10
  96. package/libx/_runtime/common/utils/rewriteAsterisks.js +2 -3
  97. package/libx/_runtime/common/utils/templateProcessor.js +15 -17
  98. package/libx/_runtime/common/utils/templateProcessorPathSerializer.js +18 -6
  99. package/libx/_runtime/db/Service.js +1 -0
  100. package/libx/_runtime/db/data-conversion/post-processing.js +0 -18
  101. package/libx/_runtime/db/expand/expand-v2.js +2 -2
  102. package/libx/_runtime/db/expand/rawToExpanded.js +6 -6
  103. package/libx/_runtime/db/generic/integrity.js +1 -1
  104. package/libx/_runtime/db/utils/columns.js +5 -5
  105. package/libx/_runtime/fiori/generic/activate.js +3 -3
  106. package/libx/_runtime/fiori/generic/edit.js +1 -1
  107. package/libx/_runtime/fiori/generic/new.js +4 -0
  108. package/libx/_runtime/fiori/lean-draft.js +138 -46
  109. package/libx/_runtime/hana/execute.js +3 -1
  110. package/libx/_runtime/hana/pool.js +10 -2
  111. package/libx/_runtime/messaging/common-utils/AMQPClient.js +6 -1
  112. package/libx/_runtime/messaging/enterprise-messaging.js +1 -0
  113. package/libx/_runtime/remote/Service.js +16 -13
  114. package/libx/_runtime/remote/utils/client.js +6 -1
  115. package/libx/_runtime/sqlite/Service.js +5 -59
  116. package/libx/_runtime/sqlite/convertDraftAdminPathExpression.js +1 -0
  117. package/libx/_runtime/sqlite/customBuilder/CustomUpsertBuilder.js +2 -2
  118. package/libx/_runtime/sqlite/execute.js +3 -1
  119. package/libx/_runtime/types/api.js +12 -3
  120. package/libx/odata/afterburner.js +36 -0
  121. package/libx/odata/cqn2odata.js +1 -1
  122. package/libx/odata/grammar.pegjs +5 -3
  123. package/libx/odata/parser.js +1 -1
  124. package/libx/odata/utils.js +1 -1
  125. package/libx/rest/RestAdapter.js +1 -1
  126. package/libx/rest/RestRequest.js +1 -0
  127. package/package.json +5 -2
  128. package/libx/_runtime/common/code-ext/workerQuery.js +0 -45
  129. package/libx/_runtime/common/constants/limit.js +0 -12
  130. package/libx/_runtime/common/utils/page.js +0 -39
@@ -13,7 +13,7 @@ const cds = require('../../../lib');
13
13
  const LOG = cds.log ? cds.log('deploy') : console;
14
14
  const DEBUG = cds.debug('deploy');
15
15
 
16
- const { bold } = require('../../utils/term');
16
+ const { bold, warn } = require('../../utils/term');
17
17
 
18
18
  const CF_COMMAND = 'cf';
19
19
 
@@ -25,6 +25,8 @@ const OPERATION_STATE_IN_PROGRESS = 'in progress';
25
25
  const OPERATION_STATE_FAILED = 'failed';
26
26
  const OPERATION_STATE_SUCCEEDED = 'succeeded';
27
27
 
28
+ const CF_CLIENT_MINIMUM_VERSION = 8;
29
+
28
30
 
29
31
  class CfUtil {
30
32
 
@@ -135,10 +137,22 @@ class CfUtil {
135
137
  }
136
138
 
137
139
  async getCfTarget() {
140
+ await this.checkCliVersion();
138
141
  await this._cfRun('oauth-token'); // check if token is valid or expired / missing
139
142
  return await this.getCfTargetFromConfigFile() || await this.getCfTargetFromCli();
140
143
  }
141
144
 
145
+ async checkCliVersion() {
146
+ const result = await this._cfRun('-v');
147
+ const version = result?.stdout?.match(/version.*(\d+\.\d+\.\d+)/i)
148
+ if (parseInt(version?.[1]) < CF_CLIENT_MINIMUM_VERSION) {
149
+ console.log(warn(`
150
+ [Warning] You are using Cloud Foundry client version ${version[1]}. We recommend version ${CF_CLIENT_MINIMUM_VERSION} or higher.
151
+ Deployment will stop in the near future for Cloud Foundry client versions < ${CF_CLIENT_MINIMUM_VERSION}.
152
+ `));
153
+ }
154
+ }
155
+
142
156
  async getCfSpaceInfo() {
143
157
  if (!this.spaceInfo) {
144
158
  LOG.debug('getting space info');
package/bin/mtx/in-cds.js CHANGED
@@ -1,14 +1,6 @@
1
1
  const cds = require('../build/cds')
2
- const { _oldMtx } = cds.utils
3
2
 
4
- const _is_streamlined_mtx = ()=>{
5
- if (_oldMtx()) return false
6
- try { return !!require.resolve('@sap/cds-mtxs/srv/deployment-service') }
7
- catch {/* ignored */}
8
- }
9
-
10
- if (!cds.requires.multitenancy || _is_streamlined_mtx()) module.exports = undefined
11
- else try {
3
+ if (cds.requires.multitenancy && cds.utils._oldMtx()) try {
12
4
  // eslint-disable-next-line cds/no-missing-dependencies
13
5
  const mtx = module.exports = require ('@sap/cds-mtx')()
14
6
  mtx.inject (cds)
@@ -17,6 +9,7 @@ else try {
17
9
  if (e.code === 'MODULE_NOT_FOUND') throw new Error('Error serving MTX APIs: @sap/cds-mtx is not installed')
18
10
  else throw e
19
11
  }
12
+ else module.exports = undefined
20
13
 
21
14
  /*
22
15
  cds.requires.multitenancy -- use this to check whether mt is enabled (old or new)
package/bin/plugins.js ADDED
@@ -0,0 +1,31 @@
1
+ const cds = require('../lib')
2
+ const DEBUG = cds.debug('plugins')
3
+
4
+ module.exports = async function load_plugins (log = console.log) {
5
+
6
+ if (DEBUG) console.time('[cds] - loaded plugins in')
7
+ const plugins = []
8
+ if (cds.env.plugins) {
9
+ for (let each of cds.env.plugins) {
10
+ let impl = typeof each === 'string' ? each : each.impl
11
+ if (impl.startsWith('.'))
12
+ impl = require.resolve(cds.root + '/' + impl)
13
+ plugins.push(_load_plugin(impl, each))
14
+ }
15
+ } else
16
+ try {
17
+ const { dependencies } = require(cds.root + '/package.json')
18
+ for (let each in dependencies) {
19
+ plugins.push(_load_plugin(each + '/cds-plugin'))
20
+ }
21
+ } catch { /* ignored */ }
22
+ try { await Promise.all(plugins); } catch { /* ignored */ }
23
+ if (DEBUG) console.timeEnd('[cds] - loaded plugins in')
24
+
25
+ async function _load_plugin (impl, conf) {
26
+ // TODO support ESM plugins. But see cap/cds/pull/1838#issuecomment-1177200 !
27
+ const plugin = require(impl)
28
+ log('loaded plugin:', { impl })
29
+ if (plugin.activate) await plugin.activate(conf)
30
+ }
31
+ }
package/bin/serve.js CHANGED
@@ -154,12 +154,19 @@ async function serve (all=[], o={}) {
154
154
  if (o.watch) return _watch.call(this, o.project,o) // cds serve --watch <project>
155
155
  if (o.project) _chdir_to (o.project) // cds run --project <project>
156
156
 
157
+ const TRACE = cds.debug('trace')
158
+ // if (TRACE) {
159
+ // TRACE?.time('load express '); require('express') // eslint-disable-line cds/no-missing-dependencies
160
+ // TRACE?.timeEnd('load express ')
161
+ // }
162
+ TRACE?.time('cds bootstrap ')
163
+
157
164
  // Load local server.js early in order to allow setting custom cds.log.Loggers
158
165
  const cds_server = await _local_server_js() || cds.server
159
166
  if (!o.silent) _prepare_logging ()
160
167
 
161
168
  // The following things are meant for dev mode, which can be overruled by feature flagse...
162
- const {features} = cds.env
169
+ const {features,fiori} = cds.env
163
170
  {
164
171
  // handle --with-mocks resp. --mocked
165
172
  if (features.with_mocks) o.mocked = _with_mocks(o)
@@ -174,23 +181,16 @@ async function serve (all=[], o={}) {
174
181
  if (features.live_reload) require('../app/etc/livereload')
175
182
 
176
183
  // add dev helper for Fiori URLs
177
- if (features.fiori_routes) require('../app/fiori/routes')
184
+ if (fiori.routes) require('../app/fiori/routes')
178
185
 
179
186
  // add fiori preview links to default index.html
180
- if (features.fiori_preview) require('../app/fiori/preview')
187
+ if (fiori.preview) require('../app/fiori/preview')
181
188
 
182
189
  }
183
190
 
184
191
  // activate plugins
185
- if (cds.env.plugins) for (let each of cds.env.plugins) {
186
- if (typeof each === 'string') each = {impl:each}
187
- let impl = each.impl
188
- if (impl.startsWith('.')) impl = require.resolve(cds.root+'/'+impl)
189
- log ('loading plugin:', {impl})
190
- // TODO support ESM plugins. But see cap/cds/pull/1838#issuecomment-1177200 !
191
- const plugin = require(impl)
192
- if (plugin.activate) await plugin.activate(each)
193
- }
192
+ await require("./plugins")(log)
193
+ TRACE?.timeEnd('cds bootstrap ')
194
194
 
195
195
  // bootstrap server from project-local server.js or from @sap/cds/server.js
196
196
  const server = await cds_server(o)
@@ -45,7 +45,7 @@ function unfold_csn (m) { // NOSONAR
45
45
  const pass2 = []
46
46
 
47
47
  const _conf = env.requires.db || env.requires.sql || env.requires.kinds && env.requires.kinds.sql
48
- const _on_sqlite = _conf.kind === 'sqlite' || _conf.kind === 'better-sqlite' || _conf.dialect === 'sqlite'
48
+ const _on_sqlite = _conf.kind === 'sqlite' || _conf.dialect === 'sqlite'
49
49
  const _locales = _on_sqlite && _locales_4sql.sqlite
50
50
 
51
51
  // Pass 1 - add localized.<locale> entities and views
@@ -19,7 +19,13 @@ function _isCompositionBacklink(e) {
19
19
  }
20
20
  }
21
21
 
22
- const IGNORED_ANNOTATIONS = [
22
+ const IGNORED_ENTITY_ANNOTATIONS = new Set([
23
+ '@readonly',
24
+ '@insertonly',
25
+ '@restrict',
26
+ ])
27
+
28
+ const IGNORED_ELEMENT_ANNOTATIONS = [
23
29
  '@assert.range',
24
30
  '@assert.enum',
25
31
  '@assert.format',
@@ -27,14 +33,21 @@ const IGNORED_ANNOTATIONS = [
27
33
  '@mandatory',
28
34
  '@Core.Immutable',
29
35
  '@readonly',
30
- '@cds.on.update',
31
- '@cds.on.insert',
32
36
  '@Core.Computed',
33
37
  '@Common.FieldControl.Readonly',
34
38
  '@Common.FieldControl.Mandatory',
35
39
  '@FieldControl.Mandatory',
36
40
  '@FieldControl.ReadOnly',
37
- '@Common.FieldControl'
41
+ '@Common.FieldControl',
42
+ '@PersonalData.DataSubjectRole',
43
+ '@PersonalData.EntitySemantics',
44
+ '@PersonalData.IsPotentiallyPersonal',
45
+ '@PersonalData.IsPotentiallySensitive',
46
+ '@PersonalData.FieldSemantics'
47
+ // These are still needed:
48
+ // '@odata.etag',
49
+ // '@cds.on.update',
50
+ // '@cds.on.insert',
38
51
  ]
39
52
 
40
53
  module.exports = function cds_compile_for_lean_drafts(csn) {
@@ -96,7 +109,10 @@ module.exports = function cds_compile_for_lean_drafts(csn) {
96
109
  Object.defineProperty(active, 'drafts', { value: draft })
97
110
  Object.defineProperty(draft, 'actives', { value: active })
98
111
  draft['@cds.persistence.table'] = _draftEntity
99
- if (draft['@restrict']) draft['@restrict'] = undefined
112
+
113
+ for (const key in draft) {
114
+ if (IGNORED_ENTITY_ANNOTATIONS.has(key) || key.startsWith('@Capabilities') || key.startsWith('@PersonalData')) draft[key] = undefined
115
+ }
100
116
  // Recursively add drafts for compositions
101
117
  for (const each in draft.elements) {
102
118
  const e = draft.elements[each]
@@ -106,7 +122,7 @@ module.exports = function cds_compile_for_lean_drafts(csn) {
106
122
  _redirect(newEl, draftEntity(e._target, model))
107
123
  }
108
124
  newEl.parent = draft
109
- for (const ignoredAnno of IGNORED_ANNOTATIONS) {
125
+ for (const ignoredAnno of IGNORED_ELEMENT_ANNOTATIONS) {
110
126
  if (newEl[ignoredAnno]) newEl[ignoredAnno] = undefined
111
127
  }
112
128
  draft.elements[each] = newEl
@@ -1,13 +1,16 @@
1
1
  const cds = require ('../../index')
2
+ const TRACE = cds.debug('trace')
2
3
 
3
4
  module.exports = function cds_compile_for_nodejs (csn,o) {
4
5
  if ('_4nodejs' in csn) return csn._4nodejs
6
+ TRACE?.time('cds.compile 4n ')
5
7
  let dsn = csn // cds.minify (csn)
6
8
  dsn = cds.compile.for.drafts (csn,o) //> creates a partial copy -> avoid any cds.linked() before
7
9
  dsn = cds.compile._localized.unfold_csn (dsn)
8
10
  dsn = cds.linked (dsn)
9
- if (cds.env.features.lean_draft) cds.compile.for.lean_drafts(dsn, o)
11
+ if (cds.env.fiori.lean_draft) cds.compile.for.lean_drafts(dsn, o)
10
12
  Object.defineProperty (csn, '_4nodejs', {value:dsn})
11
13
  Object.defineProperty (dsn, '_4nodejs', {value:dsn})
14
+ TRACE?.timeEnd('cds.compile 4n ')
12
15
  return dsn
13
16
  }
@@ -1,9 +1,9 @@
1
1
  const cds = require('..')
2
-
2
+ const TRACE = cds.debug('trace')
3
3
 
4
4
  module.exports = exports = function cds_load (files, options) {
5
5
  const all = cds.resolve(files,options)
6
- if (!all) return Promise.reject (new cds.error ({
6
+ if (!all) return Promise.reject (new cds.error ({
7
7
  message: `Couldn't find a CDS model for '${files}' in ${cds.root}`,
8
8
  code: 'MODEL_NOT_FOUND', files,
9
9
  }))
@@ -13,6 +13,7 @@ module.exports = exports = function cds_load (files, options) {
13
13
 
14
14
  exports.parsed = function cds_get (files, options, _flavor) { // NOSONAR
15
15
 
16
+ TRACE?.time('cds.load model ')
16
17
  const o = typeof options === 'string' ? { flavor:options } : options || {}
17
18
  if (!files) files = ['*']; else if (!Array.isArray(files)) files = [files]
18
19
  if (o.files || o.flavor === 'files') return cds.resolve(files,o)
@@ -31,6 +32,7 @@ exports.parsed = function cds_get (files, options, _flavor) { // NOSONAR
31
32
 
32
33
  const _finalize = (csn,o) => {
33
34
  if (!o.silent) cds.emit ('loaded', csn)
35
+ TRACE?.timeEnd('cds.load model ')
34
36
  return csn
35
37
  }
36
38
 
package/lib/core/index.js CHANGED
@@ -50,27 +50,21 @@ const roots = _roots ({
50
50
  /** Construct builtin.types as dictionary of all roots and common types */
51
51
  const types = _common ({ __proto__: roots,
52
52
  UUID: {type:'string',length:36,isUUID:true},
53
+ String: {type:'string'}, LargeString: {type:'String'},
54
+ Binary: {type:'string'}, LargeBinary: {type:'Binary'},
53
55
  Boolean: {type:'boolean'},
54
56
  Integer: {type:'number'},
55
57
  UInt8: {type:'Integer'},
56
- Int16: {type:'Integer'},
57
- Int32: {type:'Integer'},
58
- Int64: {type:'Integer'},
59
- Integer16: {type:'Int16'},
60
- Integer32: {type:'Int32'},
61
- Integer64: {type:'Int64'},
62
- Decimal: {type:'number'},
63
- DecimalFloat: {type:'number'},
58
+ Int16: {type:'Integer'}, Integer16: {type:'Int16'},
59
+ Int32: {type:'Integer'}, Integer32: {type:'Int32'},
60
+ Int64: {type:'Integer'}, Integer64: {type:'Int64'},
64
61
  Float: {type:'number'},
65
- Double: {type:'number'},
66
- DateTime: {type:'date'},
62
+ Double: {type:'Float'},
63
+ Decimal: {type:'Float'}, DecimalFloat: {type:'Decimal'},
67
64
  Date: {type:'date'},
68
65
  Time: {type:'date'},
69
- Timestamp: {type:'date'},
70
- String: {type:'string'},
71
- Binary: {type:'string'},
72
- LargeString: {type:'string'},
73
- LargeBinary: {type:'string'}
66
+ DateTime: {type:'date'},
67
+ Timestamp: {type:'DateTime'},
74
68
  })
75
69
 
76
70
  /**
@@ -90,5 +84,31 @@ function _common (defs) {
90
84
  return prefixed
91
85
  }
92
86
 
87
+ ;(
88
+ /**
89
+ * Adds convenience functions which can be used like that:
90
+ * ```js
91
+ * var { Date, Time, DateTime } = cds.builtin.types
92
+ * DateTime.now() //> 2023-02-10T14:41:36.218Z
93
+ * Date.now() //> 2023-02-10T14:41:36.218Z
94
+ * Time.now() //> 14:43:18
95
+ * Date.today() //> 2023-02-10
96
+ * ```
97
+ */
98
+ function _add_convenience_functions(){
99
+ Object.defineProperties (types.Date, {
100
+ today: { value: ()=> (new Date).toISOString().slice(0,10) },
101
+ now: { value: ()=> (new Date).toISOString() },
102
+ })
103
+
104
+ Object.defineProperties (types.Time, {
105
+ now: { value: ()=> (new Date).toISOString().slice(11,19) },
106
+ })
107
+
108
+ Object.defineProperties (types.DateTime, {
109
+ now: { value: ()=> (new Date).toISOString() },
110
+ })
111
+ }
112
+ )()
93
113
 
94
114
  module.exports = { types, classes }
@@ -1,12 +1,9 @@
1
- const cds = require('../index'), { local, inspect } = cds.utils, { UPSERT } = cds.ql
1
+ const cds = require('../index'), { local } = cds.utils
2
+ const COLORS = !!process.stdout.isTTY && !!process.stderr.isTTY
3
+ const GREY = COLORS ? '\x1b[2m' : ''
4
+ const RESET = COLORS ? '\x1b[0m' : ''
2
5
  const DEBUG = cds.debug('deploy')
3
-
4
- const colors = !!process.stdout.isTTY && !!process.stderr.isTTY
5
- const term = {
6
- x1b2: colors ? '\x1b[2m' : '',
7
- x1b0: colors ? '\x1b[0m' : '',
8
- info: colors ? (s => term.x1b2 + s + term.x1b0) : (s => s)
9
- }
6
+ const TRACE = cds.debug('trace')
10
7
 
11
8
  /**
12
9
  * Implementation of `cds.deploy` common to all databases.
@@ -18,18 +15,10 @@ exports = module.exports = function cds_deploy (model,options,csvs) { return {
18
15
 
19
16
  /** @param {cds.Service} db */
20
17
  async to (db, o = options || cds.options || {}) {
21
- if (!model) throw new Error('Must provide a model or a path to model, received: ' + model)
22
- const LOG = o.silent || !cds.log('deploy')._info ? ()=>{} : console.log
18
+ TRACE?.time ('cds.deploy db ')
23
19
 
24
- if (model && !model.definitions) {
25
- model = await cds.load (model) .then (cds.minify)
26
- if (DEBUG) try {
27
- DEBUG (`loaded model from ${model.$sources.length} file(s):\n${term.x1b2}`)
28
- for (let each of model.$sources) console.log (' ', local(each))
29
- } finally {
30
- console.log (term.x1b0)
31
- }
32
- }
20
+ if (!model) throw new Error('Must provide a model or a path to model, received: ' + model)
21
+ if (model && !model.definitions) model = await cds.load(model).then(cds.minify)
33
22
 
34
23
  if (o.mocked) exports.include_external_entities_in (model)
35
24
  else exports.exclude_external_entities_in (model)
@@ -39,18 +28,18 @@ exports = module.exports = function cds_deploy (model,options,csvs) { return {
39
28
  if (!db.model) db.model = model
40
29
 
41
30
  // create tables & views...
31
+ const LOG = o.silent || !cds.log('deploy')._info ? ()=>{} : console.log
42
32
  const any = await exports.create (db,model,o)
43
33
  if (!any && !csvs) return db
44
34
 
45
35
  // fill in initial data...
46
- await exports.init (db,model,o,csvs, file => LOG(
47
- term.info(` > init from ${local(file)}`)
48
- ))
36
+ await exports.init (db,model,o,csvs, file => LOG (GREY,` > init from ${local(file)}`, RESET))
49
37
 
50
38
  // done
51
- const file = db.getDbUrl(cds.context?.tenant)
52
- if (file !== ':memory:') LOG (`/> successfully deployed to ./${file}\n`)
53
- else LOG (`/> successfully deployed to sqlite in-memory db\n`)
39
+ let url = db.url4 (cds.context?.tenant); if (url === ':memory:') url = 'in-memory database.'
40
+ LOG ('/> successfully deployed to', url, '\n')
41
+
42
+ TRACE?.timeEnd ('cds.deploy db ')
54
43
  return db
55
44
  },
56
45
 
@@ -105,127 +94,104 @@ exports.exclude_external_entities_in = function (csn) { // NOSONAR
105
94
  }
106
95
  }
107
96
 
108
- function getSqls(db, csn, o, beforeCsn) {
109
- const schemaEvo = (db.options?.schema_evolution === 'auto' || o.schema_evolution === 'auto')
110
- if (schemaEvo) {
111
- const { afterImage: afterCsn, drops, createsAndAlters: creas } = cds.compile.to.sql.delta (csn, o, beforeCsn);
112
- if(beforeCsn === undefined) {
113
- // If this is the first deployment done with automatic schema evolution, generate everything as if it was a drop create
114
- // but set the afterCsn in the db so we can calculate a delta going forward
115
- return { afterCsn, drops: creas.map (each => {
116
- let [, kind, entity] = each.match(/^CREATE (TABLE|VIEW) "?([^\s"(]+)/im) || []
117
- return `DROP ${kind} IF EXISTS ${entity};`
118
- }).reverse(), creas };
119
- }
120
- return { afterCsn, drops, creas };
121
- } else {
122
- const creas = cds.compile.to.sql(csn, o);
123
- const drops = creas.map (each => {
124
- let [, kind, entity] = each.match(/^CREATE (TABLE|VIEW) "?([^\s"(]+)/im) || []
125
- return `DROP ${kind} IF EXISTS ${entity};`
126
- }).reverse();
127
- return { afterCsn: {}, drops, creas };
128
- }
129
- }
130
97
 
131
- exports.create = async function (db, csn=db.model, o) {
132
- const schemaEvo = (db.options?.schema_evolution === 'auto' || o.schema_evolution === 'auto')
133
- if(db.deploy && !schemaEvo) {
134
- // reset CSN state saved in db - if there is any
135
- if(!o.dry) await db.run('DROP table if exists cds_Model;');
136
- return db.deploy(csn, o);
98
+ exports.create = async function cds_deploy_create (db, csn=db.model, o) {
99
+
100
+ let drops, creas, schevo = db.options?.schema_evolution === 'auto' || o.schema_evolution === 'auto'
101
+ if (schevo) {
102
+ // REVISIT: replace by db-specific ways to read/updated recent deployed model
103
+ let before = await db.run (async tx => { try {
104
+ let [{ csn }] = await tx.run('SELECT csn from cds_Model')
105
+ return JSON.parse(csn)
106
+ } catch(e) { if (!e.message.includes('no such table')) throw e
107
+ await tx.run(`CREATE table cds_Model (csn CLOB)`)
108
+ await tx.run(`INSERT into cds_Model values ('null')`)
109
+ }})
110
+ const { afterImage, drops:_drops, createsAndAlters:_creas } = cds.compile.to.sql.delta (csn, o, before)
111
+ creas = _creas.concat (o.dry ? [] : UPDATE('cds.Model').with({ csn: JSON.stringify(afterImage) }))
112
+ drops = before ? _drops : _drops4(_creas)
113
+ } else {
114
+ creas = cds.compile.to.sql (csn, o)
115
+ drops = _drops4(creas) .concat ('DROP table if exists cds_Model;')
137
116
  }
117
+ if (!creas || creas.length === 0) return
138
118
 
139
- let beforeCsn
140
- if (schemaEvo) try {
141
- const [{ csn }] = await db.read('cds.Model')
142
- beforeCsn = JSON.parse(csn);
143
- } catch(e) {
144
- if (e.message.includes('no such table: cds_Model')) {
145
- await db.run('CREATE table cds_Model ( csn CLOB )')
146
- await db.insert({csn:''}).into('cds.Model')
147
- }
119
+ function _drops4 (creas) {
120
+ return creas.map (each => {
121
+ let [, kind, entity] = each.match(/^CREATE (TABLE|VIEW) ("[^"]+"|[^\s(]+)/im) || []
122
+ return `DROP ${kind} IF EXISTS ${entity};`
123
+ }).reverse()
148
124
  }
149
125
 
150
- const { afterCsn, drops, creas } = getSqls(db, csn, o, beforeCsn);
151
-
152
- if (!creas || creas.length === 0) return
153
126
  if (o.dry) {
154
- console.log(); for (let each of drops) console.log(each,'\n')
127
+ console.log(); for (let each of drops) console.log(each)
155
128
  console.log(); for (let each of creas) console.log(each,'\n')
156
- return
157
- } else return db.run (async tx => {
129
+ }
130
+ else return db.run (async tx => {
131
+ // This runs in a new transaction if called from CLI, while joining
132
+ // existing root tx, e.g. when called from DeploymentService.
133
+ if (!schevo && await tx.exists('sqlite.schema').where({name:'cds_xt_Extensions'})) {
134
+ // Poor man's schema evolution for extensions in drop-create deployments
135
+ drops = drops.filter(d => !d.includes('cds_xt_Extensions'))
136
+ creas = creas.filter(c => !c.includes('cds_xt_Extensions'))
137
+ }
138
+ // Set the context model while deploying for cqn42sql in new db layers
139
+ tx.model = cds.compile.for.nodejs(csn)
158
140
  await tx.run(drops)
159
141
  await tx.run(creas)
160
- if (schemaEvo) {
161
- await tx.update('cds.Model').with({ csn: JSON.stringify(afterCsn) })
162
- }
163
142
  return true
164
143
  })
165
144
  }
166
145
 
167
146
 
168
- exports.init = (db, csn=db.model, o, csvs, log=()=>{}) => db.run (async tx => {
169
-
170
- const {tenant} = cds.context; if (tenant && tenant === cds.requires.multitenancy?.t0) return
171
- const schemaEvo = db.options?.schema_evolution === 'auto' || o?.schema_evolution === 'auto'
172
- const resources = await exports.resources(csn, {testdata: cds.env.features.test_data})
173
- const inits=[]
174
-
175
- if (csvs) {
176
- const ccsn = cds.compile.for['nodejs'](csn) // compile to calculate keys for newly added entities
177
- for(let [file,src] of Object.entries(csvs)) {
178
- const entity = _entity4(path.basename(file, '.csv'), csn)
179
- if (entity?.name) {
180
- const q = INSERT_from_csv (entity.name,src,schemaEvo); if (!q) continue
181
- if (db.kind === 'better-sqlite') _add_missing_pks2(q)
182
- q._target = ccsn.definitions[entity.name]
183
- inits.push (tx.run(q) .catch (e => {
184
- throw Object.assign (e, { message: 'in cds.deploy(): ' + e.message +'\n'+ inspect(q) })
185
- }))
186
- }
187
- }
188
- } else {
189
- for (let [file,e] of Object.entries(resources)) {
190
- if (e === '*') { // init.js/ts
191
- let x = await cds.utils._import(file); if (!x) continue
192
- if (x.default) x = x.default // default ESM export
193
- inits.push (!x.then && typeof x === 'function' ? x(tx,csn) : x)
194
- log (file)
195
- } else { // from .csv or .json
196
- const INSERT_into = _from_csv_or_json [path.extname(file)]
197
- const src = await read(file,'utf8'); if (!src) continue
198
- const q = INSERT_into (e,src,schemaEvo); if (!q) continue
199
- if (db.kind === 'better-sqlite') _add_missing_pks2(q)
200
- if (cds.requires['cds.xt.ModelProviderService']?.kind === 'in-sidecar') q._target = csn.definitions[e]
201
- log (file,e)
202
- inits.push (tx.run(q) .catch (e => {
203
- throw Object.assign (e, { message: 'in cds.deploy(): ' + e.message +'\n'+ inspect(q) })
204
- }))
205
- }
206
- }
207
- }
147
+ exports.init = async function cds_deploy_init (db, csn=db.model, o, srces, log=()=>{}) {
148
+ const t = cds.context?.tenant; if (t && t === cds.requires.multitenancy?.t0) return
149
+ return db.run (async tx => {
208
150
 
209
- await Promise.all (inits)
151
+ const m = tx.model = cds.compile.for.nodejs(csn) //> use correct model while deploying
152
+ const data = await exports.data (m,srces)
153
+ const query = _queries4 (db,m)
154
+ const INSERT_from = INSERT_from4 (db,o)
210
155
 
211
- function _add_missing_pks2 (q) {
212
- const {columns,rows} = q.UPSERT || q.INSERT // REVISIT: .entries are covered by current runtime. Should eventually also be handled here, as we likely don't do so in new db services
213
- if (columns) {
214
- const entity = csn.definitions[q._target.name], {uuid} = cds.utils
215
- for (let k in entity.keys) if (!columns.includes(k) && !entity.keys[k].isAssociation) {
216
- columns.push(k)
217
- const t = entity.keys[k]._type, pk = t === 'cds.UUID' ? uuid : index => index+1
218
- rows.forEach ((row,index) => row.push(pk(index)))
156
+ for await (let [ file, entity, src ] of data) {
157
+ log (file)
158
+ if (entity) {
159
+ const q = INSERT_from (file) .into (entity, src)
160
+ if (q) try { await tx.run (query(q)) } catch(e) {
161
+ throw Object.assign (e, { message: 'in cds.deploy(): ' + e.message +'\n'+ cds.utils.inspect(q) })
162
+ }
163
+ } else { //> init.js/ts case
164
+ if (typeof src === 'function') src(tx,csn)
219
165
  }
220
166
  }
221
- }
167
+ })
168
+ }
222
169
 
223
- })
170
+
171
+ /** Prepare input from .csv, .json, init.js, ... */
172
+ exports.data = async function cds_deploy_prepare_data (csn, srces) {
173
+ // In case of extension deployment .csv or .json input are provided through argument `srces`.
174
+ if (srces) return Object.entries(srces) .map (([file, src]) => {
175
+ let e = _entity4 (path.basename(file,'.csv'), csn)
176
+ return [ file, e, src ]
177
+ })
178
+ // If not, we load them from cds.deploy.resources(csn)
179
+ const resources = await exports.resources(csn, { testdata: cds.env.features.test_data })
180
+ return Object.entries(resources) .map (async ([file,e]) => {
181
+ if (e === '*') {
182
+ let init_js = await cds.utils._import (file)
183
+ return [ file, null, init_js.default || init_js ]
184
+ } else {
185
+ let src = await read (file, 'utf8')
186
+ return [ file, e, src ]
187
+ }
188
+ })
189
+ }
224
190
 
225
191
 
226
- exports.resources = async function (csn, opts) {
192
+ exports.resources = async function cds_deploy_resources (csn, opts) {
227
193
  if (!csn || !csn.definitions) csn = await cds.load (csn||'*') .then (cds.minify)
228
- const folders = await exports.resources.folders(csn, opts)
194
+ const folders = await cds_deploy_resources.folders(csn, opts)
229
195
  const found={}, ts = process.env.CDS_TYPESCRIPT
230
196
  for (let folder of folders) {
231
197
  // fetching init.js files
@@ -237,7 +203,7 @@ exports.resources = async function (csn, opts) {
237
203
  const files = await readdir (subdir)
238
204
  for (let fx of files) {
239
205
  if (fx[0] === '-') continue
240
- const ext = path.extname(fx); if (ext in _from_csv_or_json) {
206
+ const ext = path.extname(fx); if (ext in {'.csv':1,'.json':2}) {
241
207
  const f = fx.slice(0,-ext.length)
242
208
  if (/[._]texts$/.test(f) && files.some(g => g.startsWith(f+'_'))) {
243
209
  // ignores 'Books_texts.csv/json' if there is any 'Books_texts_LANG.csv/json'
@@ -287,17 +253,47 @@ const _entity4 = (file,csn) => {
287
253
  return entity.name ? entity : { name, __proto__:entity }
288
254
  }
289
255
 
290
- const INSERT_from_csv = (entity, csv, schemaEvo) => {
291
- let [ cols, ...rows ] = cds.parse.csv (csv)
292
- if (rows.length > 0) return (schemaEvo ? UPSERT : INSERT).into (entity) .columns (cols) .rows (rows)
256
+
257
+ /** Prepare special handling for new db services */
258
+ const _queries4 = (db,csn) => !db.cqn2sql ? q => q : q => {
259
+ const { columns, rows } = q.INSERT || q.UPSERT; if (!columns) return q // REVISIT: .entries are covered by current runtime -> should eventually also be handled here
260
+ const entity = csn.definitions[q._target.name]
261
+
262
+ // Fill in missing primary keys...
263
+ const { uuid } = cds.utils
264
+ for (let k in entity.keys) if (entity.keys[k].isUUID && !columns.includes(k)) {
265
+ columns.push(k)
266
+ rows.forEach(row => row.push(uuid()))
267
+ }
268
+
269
+ // Fill in missing managed data...
270
+ const pseudos = { $user: 'anonymous', $now: (new Date).toISOString() }
271
+ for (let k in entity.elements) {
272
+ const managed = entity.elements[k]['@cds.on.insert']?.['=']
273
+ if (managed && !columns.includes(k)) {
274
+ columns.push(k)
275
+ rows.forEach(row => row.push(pseudos[managed]))
276
+ }
277
+ }
278
+
279
+ return q
293
280
  }
294
281
 
295
- const INSERT_from_json = (entity, json, schemaEvo) => {
296
- let records = JSON.parse (json)
297
- if (records.length > 0) return (schemaEvo ? UPSERT : INSERT).into (entity) .entries (records)
282
+
283
+ const INSERT_from4 = (db,o) => {
284
+ const schevo = db.options?.schema_evolution === 'auto' || o?.schema_evolution === 'auto'
285
+ const INSERT_into = (schevo ? UPSERT : INSERT).into
286
+ return (file) => ({
287
+ '.json': { into (entity, json) {
288
+ let records = JSON.parse(json)
289
+ if (records.length > 0) return INSERT_into(entity).entries(records)
290
+ }},
291
+ '.csv': { into (entity, csv) {
292
+ let [cols, ...rows] = cds.parse.csv(csv)
293
+ if (rows.length > 0) return INSERT_into(entity).columns(cols).rows(rows)
294
+ }},
295
+ }) [path.extname(file)]
298
296
  }
299
297
 
300
- const _from_csv_or_json = { '.json': INSERT_from_json, '.csv': INSERT_from_csv, }
301
298
  const _skip = e => !e || e['@cds.persistence.skip'] === true
302
-
303
299
  /* eslint-disable no-console */