@sap/cds 7.6.4 → 7.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/CHANGELOG.md +39 -1
  2. package/_i18n/i18n.properties +3 -0
  3. package/app/index.js +14 -8
  4. package/bin/serve.js +51 -19
  5. package/common.cds +16 -0
  6. package/lib/auth/ias-auth.js +2 -2
  7. package/lib/auth/index.js +1 -1
  8. package/lib/auth/jwt-auth.js +1 -1
  9. package/lib/compile/cdsc.js +23 -11
  10. package/lib/compile/for/nodejs.js +2 -2
  11. package/lib/compile/for/odata.js +4 -0
  12. package/lib/compile/load.js +7 -2
  13. package/lib/compile/to/sql.js +3 -0
  14. package/lib/dbs/cds-deploy.js +197 -220
  15. package/lib/env/defaults.js +2 -1
  16. package/lib/index.js +8 -2
  17. package/lib/linked/types.js +1 -0
  18. package/lib/log/format/json.js +4 -1
  19. package/lib/plugins.js +2 -2
  20. package/lib/ql/SELECT.js +8 -8
  21. package/lib/req/context.js +22 -13
  22. package/lib/req/request.js +10 -4
  23. package/lib/srv/cds-connect.js +9 -3
  24. package/lib/srv/cds-serve.js +5 -3
  25. package/lib/srv/middlewares/ctx-model.js +1 -1
  26. package/lib/srv/protocols/odata-v4.js +38 -9
  27. package/lib/srv/srv-api.js +98 -140
  28. package/lib/srv/srv-models.js +2 -2
  29. package/lib/srv/srv-tx.js +1 -0
  30. package/lib/utils/cds-utils.js +32 -23
  31. package/lib/utils/data.js +1 -1
  32. package/lib/utils/tar.js +1 -1
  33. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +1 -3
  34. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +0 -2
  35. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +18 -1
  36. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/index.js +1 -1
  37. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/utils.js +7 -3
  38. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriParser.js +2 -1
  39. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/index.js +5 -0
  40. package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +71 -25
  41. package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +10 -2
  42. package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +6 -1
  43. package/libx/_runtime/cds-services/util/assert.js +50 -240
  44. package/libx/_runtime/cds.js +5 -0
  45. package/libx/_runtime/common/aspects/any.js +53 -45
  46. package/libx/_runtime/common/generic/input.js +14 -10
  47. package/libx/_runtime/common/generic/paging.js +1 -1
  48. package/libx/_runtime/common/utils/cqn.js +1 -1
  49. package/libx/_runtime/common/utils/cqn2cqn4sql.js +1 -1
  50. package/libx/_runtime/common/utils/keys.js +1 -1
  51. package/libx/_runtime/common/utils/quotingStyles.js +1 -1
  52. package/libx/_runtime/common/utils/resolveStructured.js +4 -1
  53. package/libx/_runtime/common/utils/rewriteAsterisks.js +5 -12
  54. package/libx/_runtime/common/utils/stream.js +2 -16
  55. package/libx/_runtime/common/utils/streamProp.js +16 -6
  56. package/libx/_runtime/common/utils/ucsn.js +1 -0
  57. package/libx/_runtime/db/expand/expandCQNToJoin.js +1 -1
  58. package/libx/_runtime/db/sql-builder/InsertBuilder.js +1 -1
  59. package/libx/_runtime/db/utils/columns.js +6 -1
  60. package/libx/_runtime/fiori/generic/activate.js +11 -3
  61. package/libx/_runtime/fiori/generic/edit.js +8 -2
  62. package/libx/_runtime/fiori/lean-draft.js +94 -30
  63. package/libx/_runtime/hana/execute.js +2 -5
  64. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +12 -22
  65. package/libx/_runtime/messaging/service.js +6 -2
  66. package/libx/common/assert/index.js +232 -0
  67. package/libx/common/assert/type.js +109 -0
  68. package/libx/common/assert/utils.js +125 -0
  69. package/libx/common/assert/validation.js +109 -0
  70. package/libx/odata/index.js +5 -5
  71. package/libx/odata/middleware/create.js +83 -0
  72. package/libx/odata/middleware/delete.js +38 -0
  73. package/libx/odata/middleware/error.js +8 -0
  74. package/libx/odata/{metadata.js → middleware/metadata.js} +8 -6
  75. package/libx/odata/middleware/operation.js +78 -0
  76. package/libx/odata/middleware/parse.js +11 -0
  77. package/libx/odata/{read.js → middleware/read.js} +42 -20
  78. package/libx/odata/{service-document.js → middleware/service-document.js} +2 -1
  79. package/libx/odata/middleware/stream.js +237 -0
  80. package/libx/odata/middleware/update.js +165 -0
  81. package/libx/odata/{afterburner.js → parse/afterburner.js} +79 -29
  82. package/libx/odata/{cqn2odata.js → parse/cqn2odata.js} +5 -3
  83. package/libx/odata/{parseToCqn.js → parse/parseToCqn.js} +3 -6
  84. package/libx/odata/{utils.js → utils/index.js} +95 -9
  85. package/libx/outbox/index.js +2 -1
  86. package/libx/rest/RestAdapter.js +0 -1
  87. package/libx/rest/middleware/operation.js +6 -4
  88. package/libx/rest/middleware/parse.js +20 -2
  89. package/package.json +1 -1
  90. package/server.js +43 -71
  91. package/libx/odata/create.js +0 -44
  92. package/libx/odata/delete.js +0 -25
  93. package/libx/odata/error.js +0 -12
  94. package/libx/odata/update.js +0 -110
  95. /package/libx/odata/{grammar.peggy → parse/grammar.peggy} +0 -0
  96. /package/libx/odata/{parser.js → parse/parser.js} +0 -0
  97. /package/libx/odata/{result.js → utils/result.js} +0 -0
@@ -1,73 +1,61 @@
1
1
  #!/usr/bin/env node
2
- const cds = require('../index'), { local } = cds.utils
3
- const crypto = require('crypto')
4
- const COLORS = !!process.stdout.isTTY && !!process.stderr.isTTY && !process.env.NO_COLOR
5
- const GREY = COLORS ? '\x1b[2m' : ''
6
- const RESET = COLORS ? '\x1b[0m' : ''
2
+ const cds = require('../index'), { local, path } = cds.utils
7
3
  const DEBUG = cds.debug('deploy')
4
+ const TRACE = cds.debug('trace')
5
+
6
+
7
+ /** Fluent API: cds.deploy(model).to(db) */
8
+ const deploy = module.exports = function cds_deploy (model, options, csvs) {
9
+ return { async to (/** @type {import('../../lib/srv/srv-api')} */ db, o = options||{}) {
10
+
11
+ // prepare logging
12
+ const [ GREY, RESET ] = !!process.stdout.isTTY && !!process.stderr.isTTY && !process.env.NO_COLOR ? ['\x1b[2m', '\x1b[0m' ] : ['','']
13
+ const LOG = !o.silent && !o.dry && cds.log('deploy')._info ? console.log : undefined
14
+
15
+ // prepare model
16
+ if (!model) throw new Error('Must provide a model or a path to model, received: ' + model)
17
+ if (!model?.definitions) model = await cds.load(model).then(cds.minify)
18
+ if (o.mocked) deploy.include_external_entities_in(model)
19
+ else deploy.exclude_external_entities_in(model)
20
+
21
+ // prepare db
22
+ if (!db.run) db = await cds.connect.to(db)
23
+ if (!cds.db) cds.db = cds.services.db = db
24
+ if (!db.model) db.model = model // NOTE: this calls compile.for.nodejs! Has to happen here for db/init.js to access cds.entities
25
+ // NOTE: This ^^^^^^^^^^^^^^^^^ is to support tests that use cds.deploy() to bootstrap a functional db like so:
26
+ // const db = await cds.deploy ('<filename>') .to ('sqlite::memory:')
27
+
28
+ // prepare db description for log output below
29
+ let descr = db.url4 (cds.context?.tenant)
30
+ if (descr === ':memory:') descr = 'in-memory database.'
31
+ else if (!descr.startsWith('http:')) descr = local (descr)
32
+
33
+ // deploy schema and initial data...
34
+ try {
35
+ const _run = fn => o.dry ? fn(db) : db.run(fn)
36
+ await _run (async tx => {
37
+ let any = await deploy.schema (tx, model, o)
38
+ if (any || csvs) await deploy.data (tx, model, o, csvs, file => LOG?.(GREY, ' > init from', local(file), RESET))
39
+ })
40
+ LOG?.('/> successfully deployed to', descr, '\n')
41
+ } catch (e) {
42
+ LOG?.('/> deployment to', descr, 'failed\n')
43
+ throw e
44
+ }
45
+ return db
46
+ },
8
47
 
9
- /**
10
- * Implementation of `cds.deploy` common to all databases.
11
- * It uses the database-specific `db.deploy` to prepare the database, e.g.
12
- * deploy create tables and views in case of a SQL database, then fills
13
- * in initial data, if present.
14
- */
15
- module.exports = exports = function cds_deploy (model,options,csvs) {
16
- return {
17
- /** @param {import('@sap/cds/lib/srv/srv-api')} db */
18
- async to(db, o = options || {}) {
19
-
20
- const TRACE = cds.debug('trace')
21
- TRACE?.time('cds.deploy db ')
22
-
23
- if (!model) throw new Error('Must provide a model or a path to model, received: ' + model)
24
- if (!model?.definitions) model = await cds.load(model).then(cds.minify)
25
-
26
- if (o.mocked) exports.include_external_entities_in(model)
27
- else exports.exclude_external_entities_in(model)
28
-
29
- if (!db.run) db = await cds.connect.to(db)
30
- if (!cds.db) cds.db = cds.services.db = db
31
- if (!db.model) db.model = model // NOTE: this calls compile.for.nodejs!
32
-
33
- // eslint-disable-next-line no-console
34
- const LOG = o.silent || o.dry || !cds.log('deploy')._info ? () => {} : console.log
35
- const _deploy = async tx => {
36
- // create / update schema
37
- let any = await exports.create(tx, model, o)
38
- if (!any && !csvs) return db
39
- // fill in initial data
40
- await exports.init(tx, model, o, csvs, file => LOG(GREY, ` > init from ${local(file)}`, RESET))
41
- }
42
-
43
- let url = db.url4(cds.context?.tenant)
44
- if (url === ':memory:') url = 'in-memory database.'
45
- else if (!url.startsWith('http:')) url = local(url)
46
- try {
47
- await (o.dry ? _deploy(db) : db.run(_deploy))
48
- LOG('/> successfully deployed to', url, '\n')
49
- } catch (e) {
50
- LOG('/> deployment to', url, 'failed\n')
51
- throw e
52
- }
53
-
54
- TRACE?.timeEnd('cds.deploy db ')
55
- return db
56
- },
57
-
58
- // continue to support cds.deploy() as well...
59
- then(n, e) {
60
- return this.to(cds.db || 'db').then(n, e)
61
- },
62
- catch(e) {
63
- return this.to(cds.db || 'db').catch(e)
64
- },
65
- }
66
- }
67
-
68
- exports.create = async function cds_deploy_create (db, csn=db.model, o) {
48
+ // Also support await cds.deploy()...
49
+ then(n, e) {
50
+ return this.to(cds.db || cds.requires.db && 'db' || 'sqlite::memory:').then(n,e)
51
+ },
52
+ catch(e) {
53
+ return this.to(cds.db || cds.requires.db && 'db' || 'sqlite::memory:').catch(e)
54
+ },
55
+ }}
69
56
 
70
- /* eslint-disable no-console */
57
+ /** Deploy database schema, i.e., generate and apply SQL DDL. */
58
+ deploy.schema = async function (db, csn = db.model, o) {
71
59
 
72
60
  if (!o.to || o.to === db.options.kind) o = { ...db.options, ...o }
73
61
  if (o.impl === '@cap-js/sqlite') {
@@ -102,6 +90,8 @@ exports.create = async function cds_deploy_create (db, csn=db.model, o) {
102
90
  creas = cds.compile.to.sql(csn,o) // NOTE: this used to call cds.linked(cds.minify) and thereby corrupted the passed in csn
103
91
  }
104
92
 
93
+ TRACE?.time('cds.deploy schema'.padEnd(22))
94
+
105
95
  if (!drops) {
106
96
  drops = [];
107
97
  creas.forEach(each => {
@@ -118,21 +108,21 @@ exports.create = async function cds_deploy_create (db, csn=db.model, o) {
118
108
  if (!drops.length && !creas.length) return !o.dry
119
109
 
120
110
  if (o.dry) {
121
- console.log()
122
- for (let each of drops) console.log(each)
123
- console.log()
124
- for (let each of creas) console.log(each, '\n')
111
+ console.log(); for (let each of drops) console.log(each)
112
+ console.log(); for (let each of creas) console.log(each, '\n')
125
113
  return
126
114
  }
127
115
 
128
116
  await db.run(drops)
129
117
  await db.run(creas)
118
+
119
+ TRACE?.timeEnd('cds.deploy schema'.padEnd(22))
130
120
  return true
131
121
 
132
122
  async function get_prior_model() {
133
123
  let file = o['delta-from']
134
124
  if (file) {
135
- let prior = await cds.utils.read(file)
125
+ let prior = await cds.utils.read (file)
136
126
  return { prior: typeof prior === 'string' ? JSON.parse(prior) : prior }
137
127
  }
138
128
  if (o.dry) return {}
@@ -155,57 +145,17 @@ exports.create = async function cds_deploy_create (db, csn=db.model, o) {
155
145
  }
156
146
 
157
147
 
158
- const { fs, path, read } = cds.utils
159
- const { readdir } = fs.promises
160
- const isdir = (..._) => fs.isdir(path.join(..._))
161
- const isfile = (..._) => fs.isfile(path.join(..._))
148
+ /** Deploy initial data */
149
+ deploy.data = async function (db, csn = db.model, o, srces, log=()=>{}) {
162
150
 
163
- exports.include_external_entities_in = function (model) {
164
- if (model._mocked) return model; else Object.defineProperty(model,'_mocked',{value:true})
165
- for (let each in model.definitions) {
166
- const def = model.definitions[each]
167
- if (def['@cds.persistence.mock'] === false) continue
168
- if (def['@cds.persistence.skip'] === true) {
169
- DEBUG?.('including mocked', each)
170
- delete def['@cds.persistence.skip']
171
- }
172
- }
173
- exports.exclude_external_entities_in (model)
174
- return model
175
- }
176
-
177
- exports.exclude_external_entities_in = function (csn) { // NOSONAR
178
- // IMPORTANT to use cds.env.requires below, not cds.requires !!
179
- for (let [each,{service=each,model,credentials}] of Object.entries (cds.env.requires)) {
180
- if (!model) continue //> not for internal services like cds.requires.odata
181
- if (!credentials && csn._mocked) continue //> not for mocked unbound services
182
- DEBUG?.('excluding external entities for', service, '...')
183
- const prefix = service+'.'
184
- for (let each in csn.definitions) if (each.startsWith(prefix)) _exclude (each)
185
- }
186
- return csn
187
-
188
- function _exclude (each) {
189
- const def = csn.definitions[each]; if (def.kind !== 'entity') return
190
- if (def['@cds.persistence.table'] === true) return // do not exclude replica table
191
- DEBUG?.('excluding external entity', each)
192
- def['@cds.persistence.skip'] = true
193
- // propagate to all views on top...
194
- for (let other in csn.definitions) {
195
- const d = csn.definitions[other]
196
- const p = d.query && d.query.SELECT || d.projection
197
- if (p && p.from.ref && p.from.ref[0] === each) _exclude (other)
198
- }
199
- }
200
- }
201
-
202
-
203
- exports.init = async function cds_deploy_init (db, csn=db.model, o, srces, log=()=>{}) {
204
151
  const t = cds.context?.tenant; if (t && t === cds.requires.multitenancy?.t0) return
152
+ const crypto = require('crypto')
153
+
205
154
  return db.run (async tx => {
155
+ TRACE?.time('cds.deploy data'.padEnd(22))
206
156
 
207
157
  const m = tx.model = cds.compile.for.nodejs(csn) // NOTE: this used to create a redundant 4nodejs model for tha same csn
208
- const data = await exports.data (m,srces)
158
+ const data = await deploy.prepare (m,srces)
209
159
  const query = _queries4 (db,m)
210
160
  const INSERT_from = INSERT_from4 (db,m,o)
211
161
 
@@ -214,18 +164,89 @@ exports.init = async function cds_deploy_init (db, csn=db.model, o, srces, log=(
214
164
  if (entity) {
215
165
  const q = INSERT_from (file) .into (entity, src)
216
166
  if (q) try { await tx.run (query(q)) } catch(e) {
217
- throw Object.assign (e, { message: 'in cds.deploy(): ' + e.message +'\n'+ cds.utils.inspect(q) })
167
+ throw Object.assign (e, { message: 'in cds.deploy(): ' + e.message +'\n'+ cds.utils.inspect(q, {depth:11}) })
218
168
  }
219
169
  } else { //> init.js/ts case
220
170
  if (typeof src === 'function') await src(tx,csn)
221
171
  }
222
172
  }
173
+
174
+ TRACE?.timeEnd('cds.deploy data'.padEnd(22))
223
175
  })
176
+
177
+
178
+ /** Prepare special handling for new db services */
179
+ function _queries4 (db, m) {
180
+ return !db.cqn2sql ? q => q : q => {
181
+ const { columns, rows } = q.INSERT || q.UPSERT; if (!columns) return q // REVISIT: .entries are covered by current runtime -> should eventually also be handled here
182
+ const entity = m.definitions[q._target.name]
183
+
184
+ // Fill in missing primary keys...
185
+ const { uuid } = cds.utils
186
+ for (let k in entity.keys) if (entity.keys[k].isUUID && !columns.includes(k)) {
187
+ columns.push(k)
188
+ rows.forEach(row => row.push(uuid()))
189
+ }
190
+
191
+ // Fill in missing managed data...
192
+ const pseudos = { $user: 'anonymous', $now: (new Date).toISOString() }
193
+ for (let k in entity.elements) {
194
+ const managed = entity.elements[k]['@cds.on.insert']?.['=']
195
+ if (managed && !columns.includes(k)) {
196
+ columns.push(k)
197
+ rows.forEach(row => row.push(pseudos[managed]))
198
+ }
199
+ }
200
+ return q
201
+ }
202
+ }
203
+
204
+ function INSERT_from4 (db,m,o) {
205
+ const schevo = o?.schema_evolution === 'auto' || db.options.schema_evolution === 'auto'
206
+ const INSERT_into = (schevo ? UPSERT : INSERT).into
207
+ return (file) => ({
208
+ '.json': { into (entity, json) {
209
+ let records = JSON.parse(json); if (!records.length) return
210
+ _add_ID_texts4 (entity, records)
211
+ return INSERT_into(entity).entries(records)
212
+ }},
213
+ '.csv': { into (entity, csv) {
214
+ let [cols, ...rows] = cds.parse.csv(csv); if (!rows.length) return
215
+ _add_ID_texts4 (entity, rows, cols)
216
+ return INSERT_into(entity).columns(cols).rows(rows)
217
+ }},
218
+ }) [path.extname(file)]
219
+ }
220
+
221
+ /**
222
+ * Fills in missing ID_texts for respective .texts entities.
223
+ * IMPORTANT: we use UUIDs generated from hashes of all original key values (ID, locale, ...)
224
+ * to ensure same ID_texts values for same keys across different deployments.
225
+ */
226
+ function _add_ID_texts4 (entity, records, cols) {
227
+ if (entity.name) entity = entity.name //> entity can be an entity name or a definition
228
+ if (!csn.definitions[entity]?.keys?.ID_texts) return // it's not a .texts entity with ID_texts key
229
+ if ((cols || Object.keys(records[0])).includes('ID_texts')) return // already there
230
+ else DEBUG?.(`adding ID_texts for ${entity}`)
231
+ const keys = Object.keys (csn.definitions[entity.slice(0,-6)].keys) .concat ('locale')
232
+ if (cols) {
233
+ cols.push ('ID_texts')
234
+ const indexes = keys.map (k => cols.indexOf(k))
235
+ for (let each of records) each.push (_uuid4(each,indexes))
236
+ } else {
237
+ for (let each of records) each.ID_texts = _uuid4(each,keys)
238
+ }
239
+ function _uuid4 (data, keys) {
240
+ const s = keys.reduce ((s,k) => s + data[k],'')
241
+ const h = crypto.createHash('md5').update(s).digest('hex')
242
+ return h.slice(0,8) + '-' + h.slice(8,12) + '-' + h.slice(12,16) + '-' + h.slice(16,20) + '-' + h.slice(20)
243
+ }
244
+ }
224
245
  }
225
246
 
226
247
 
227
248
  /** Prepare input from .csv, .json, init.js, ... */
228
- exports.data = async function cds_deploy_prepare_data (csn, srces) {
249
+ deploy.prepare = async function (csn, srces) {
229
250
  // In case of extension deployment .csv or .json input are provided through argument `srces`.
230
251
  if (srces) return Object.entries(srces) .map (([file, src]) => {
231
252
  let e = _entity4 (path.basename(file,'.csv'), csn)
@@ -233,14 +254,14 @@ exports.data = async function cds_deploy_prepare_data (csn, srces) {
233
254
  })
234
255
  // If not, we load them from cds.deploy.resources(csn)
235
256
  const data = []
236
- const resources = await exports.resources(csn, { testdata: cds.env.features.test_data })
257
+ const resources = await deploy.resources(csn, { testdata: cds.env.features.test_data })
237
258
  const resEntries = Object.entries(resources).reverse() // reversed $sources, relevant as UPSERT order
238
259
  for (const [file,e] of resEntries) {
239
260
  if (e === '*') {
240
261
  let init_js = await cds.utils._import (file)
241
262
  data.push([ file, null, init_js.default || init_js ])
242
263
  } else {
243
- let src = await read (file, 'utf8')
264
+ let src = await cds.utils.read (file, 'utf8')
244
265
  data.push([ file, e, src ])
245
266
  }
246
267
  }
@@ -248,15 +269,17 @@ exports.data = async function cds_deploy_prepare_data (csn, srces) {
248
269
  }
249
270
 
250
271
 
251
- exports.resources = async function cds_deploy_resources (csn, opts) {
272
+ /** Resolve initial data resources for given model */
273
+ deploy.resources = async function (csn, opts) {
252
274
  if (!csn || !csn.definitions) csn = await cds.load (csn||'*') .then (cds.minify)
253
- const folders = await cds_deploy_resources.folders(csn, opts)
275
+ const { fs, isdir, isfile } = cds.utils
276
+ const folders = await deploy.folders(csn, opts)
254
277
  const found={}, ts = process.env.CDS_TYPESCRIPT
255
278
  for (let folder of folders) {
256
279
  // fetching .csv and .json files
257
280
  for (let each of ['data','csv']) {
258
281
  const subdir = isdir(folder,each); if (!subdir) continue
259
- const files = await readdir (subdir)
282
+ const files = await fs.promises.readdir (subdir)
260
283
  for (let fx of files) {
261
284
  if (fx[0] === '-') continue
262
285
  const ext = path.extname(fx); if (ext in {'.csv':1,'.json':2}) {
@@ -266,7 +289,7 @@ exports.resources = async function cds_deploy_resources (csn, opts) {
266
289
  DEBUG?.(`ignoring '${fx}' in favor of translated ones`)
267
290
  continue
268
291
  }
269
- const e = _entity4(f,csn); if (_skip(e)) continue
292
+ const e = _entity4(f,csn); if (!e || e['@cds.persistence.skip'] === true) continue
270
293
  if (cds.env.features.deploy_data_onconflict === 'replace' && !/[._]texts_/.test(f)) {
271
294
  const seenBefore = Object.entries(found).find(([_, entity]) => entity === e.name )
272
295
  if (seenBefore) {
@@ -286,7 +309,8 @@ exports.resources = async function cds_deploy_resources (csn, opts) {
286
309
  }
287
310
 
288
311
 
289
- exports.resources.folders = async function (csn, o={}) {
312
+ /** Resolve folders to fetch for initial data resources for given model */
313
+ deploy.folders = async function (csn, o={}) {
290
314
  if (!csn || !csn.definitions) csn = await cds.load (csn||'*') .then (cds.minify)
291
315
  const folders = new Set (csn.$sources.map (path.dirname) .filter (f => f !== cds.home))
292
316
  if (cds.env.folders.db) folders.add (path.resolve(cds.root, cds.env.folders.db))
@@ -295,7 +319,51 @@ exports.resources.folders = async function (csn, o={}) {
295
319
  }
296
320
 
297
321
 
298
- const _entity4 = (file,csn) => {
322
+ /** Include external entities in the given model */
323
+ deploy.include_external_entities_in = function (csn) {
324
+ if (csn._mocked) return csn; else Object.defineProperty(csn,'_mocked',{value:true})
325
+ for (let each in csn.definitions) {
326
+ const def = csn.definitions[each]
327
+ if (def['@cds.persistence.mock'] === false) continue
328
+ if (def['@cds.persistence.skip'] === true) {
329
+ DEBUG?.('including mocked', each)
330
+ delete def['@cds.persistence.skip']
331
+ }
332
+ }
333
+ deploy.exclude_external_entities_in (csn)
334
+ return csn
335
+ }
336
+
337
+ /** Exclude external entities from the given model */
338
+ deploy.exclude_external_entities_in = function (csn) {
339
+ // IMPORTANT to use cds.env.requires below, not cds.requires !!
340
+ for (let [each,{service=each,model,credentials}] of Object.entries (cds.env.requires)) {
341
+ if (!model) continue //> not for internal services like cds.requires.odata
342
+ if (!credentials && csn._mocked) continue //> not for mocked unbound services
343
+ DEBUG?.('excluding external entities for', service, '...')
344
+ const prefix = service+'.'
345
+ for (let each in csn.definitions) if (each.startsWith(prefix)) _exclude (each)
346
+ }
347
+ return csn
348
+
349
+ function _exclude (each) {
350
+ const def = csn.definitions[each]; if (def.kind !== 'entity') return
351
+ if (def['@cds.persistence.table'] === true) return // do not exclude replica table
352
+ DEBUG?.('excluding external entity', each)
353
+ def['@cds.persistence.skip'] = true
354
+ // propagate to all views on top...
355
+ for (let other in csn.definitions) {
356
+ const d = csn.definitions[other]
357
+ const p = d.query && d.query.SELECT || d.projection
358
+ if (p && p.from.ref && p.from.ref[0] === each) _exclude (other)
359
+ }
360
+ }
361
+
362
+ }
363
+
364
+
365
+ /** Helper for resolving entity for given .csv file */
366
+ const _entity4 = (file, csn) => {
299
367
  const name = file.replace(/-/g,'.')
300
368
  const entity = csn.definitions [name]
301
369
  if (!entity) {
@@ -313,99 +381,8 @@ const _entity4 = (file,csn) => {
313
381
  return entity.name ? entity : { name, __proto__:entity }
314
382
  }
315
383
 
316
-
317
- /** Prepare special handling for new db services */
318
- const _queries4 = (db,csn) => !db.cqn2sql ? q => q : q => {
319
- const { columns, rows } = q.INSERT || q.UPSERT; if (!columns) return q // REVISIT: .entries are covered by current runtime -> should eventually also be handled here
320
- const entity = csn.definitions[q._target.name]
321
-
322
- // Fill in missing primary keys...
323
- const { uuid } = cds.utils
324
- for (let k in entity.keys) if (entity.keys[k].isUUID && !columns.includes(k)) {
325
- columns.push(k)
326
- rows.forEach(row => row.push(uuid()))
327
- }
328
-
329
- // Fill in missing managed data...
330
- const pseudos = { $user: 'anonymous', $now: (new Date).toISOString() }
331
- for (let k in entity.elements) {
332
- const managed = entity.elements[k]['@cds.on.insert']?.['=']
333
- if (managed && !columns.includes(k)) {
334
- columns.push(k)
335
- rows.forEach(row => row.push(pseudos[managed]))
336
- }
337
- }
338
-
339
- return q
340
- }
341
-
342
-
343
- const INSERT_from4 = (db,m,o) => {
344
- const schevo = o?.schema_evolution === 'auto' || db.options.schema_evolution === 'auto'
345
- const INSERT_into = (schevo ? UPSERT : INSERT).into
346
- return (file) => ({
347
- '.json': { into (entity, json) {
348
- let records = JSON.parse(json)
349
- if (records.length > 0) {
350
- fill_ID_texts_json(records, m, entity)
351
- return INSERT_into(entity).entries(records)
352
- }
353
- }},
354
- '.csv': { into (entity, csv) {
355
- let [cols, ...rows] = cds.parse.csv(csv)
356
- if (rows.length > 0) {
357
- fill_ID_texts_csv(cols, rows, m, entity)
358
- return INSERT_into(entity).columns(cols).rows(rows)
359
- }
360
- }},
361
- }) [path.extname(file)]
362
- }
363
-
364
- const fill_ID_texts_json = (records, m, entity) => {
365
- const baseKey = idTextsBaseKey(m, entity)
366
- if (baseKey) {
367
- records.forEach(record => {
368
- if (!record.ID_texts) {
369
- record.ID_texts = hashedUUID(record[baseKey], record.locale)
370
- }
371
- })
372
- }
373
- }
374
-
375
- const fill_ID_texts_csv = (cols, rows, m, entity) => {
376
- const baseKey = idTextsBaseKey(m, entity)
377
- if (baseKey && !cols.find(r => r.toLowerCase() === 'id_texts')) { // and no such column in csv?
378
- DEBUG?.(`adding ID_texts for ${entity}`)
379
- const indexBaseKey = cols.findIndex(c => c.toLowerCase() === baseKey.toLowerCase())
380
- const indexLocale = cols.findIndex(c => c.toLowerCase() === 'locale')
381
- rows.forEach(row => {
382
- const idtexts = hashedUUID(row[indexBaseKey], row[indexLocale])
383
- row.push(idtexts)
384
- })
385
- cols.push('ID_texts')
386
- }
387
- }
388
-
389
- const idTextsBaseKey = (m, entity) => {
390
- let base
391
- if (m.definitions[entity]?.keys?.ID_texts // ID_text key?
392
- && /(.+)[._]texts$/.test(entity) && (base = m.definitions[RegExp.$1])) { // in a .text entity?
393
- const baseKey = Object.keys(base.keys)[0] // base entity's key is usually, but not always 'ID'
394
- return baseKey
395
- }
396
- }
397
-
398
- const hashedUUID = (...values) => {
399
- const sum = values.reduce((acc, curr) => acc + curr, '')
400
- const h = crypto.createHash('md5').update(sum).digest('hex')
401
- return h.slice(0, 8) + '-' + h.slice(8, 12) + '-' + h.slice(12, 16) + '-' + h.slice(16, 20) + '-' + h.slice(20)
402
- }
403
-
404
- const _skip = e => !e || e['@cds.persistence.skip'] === true
405
-
406
-
407
-
408
- if (!module.parent) (async () => {
384
+ /** CLI used as via cds-deploy as deployer for PostgreSQL */
385
+ if (!module.parent) (async function CLI () {
409
386
  await cds.plugins // IMPORTANT: that has to go before any call to cds.env, like through cds.deploy or cds.requires below
410
387
  let db = cds.requires.db
411
388
  try {
@@ -1,4 +1,3 @@
1
- const { join } = require('path')
2
1
  const production = process.env.NODE_ENV === 'production'
3
2
 
4
3
  const defaults = module.exports = {
@@ -43,6 +42,8 @@ const defaults = module.exports = {
43
42
  routes: !production,
44
43
  lean_draft: true,
45
44
  wrap_multiple_errors: true, // switch default with cds 8
45
+ draft_lock_timeout: true,
46
+ draft_deletion_timeout: false, // switch default with cds 8
46
47
  draft_compat: undefined,
47
48
  '[better-sqlite]': { lean_draft: true },
48
49
  '[lean-draft]': { lean_draft: true },
package/lib/index.js CHANGED
@@ -99,7 +99,7 @@ const cds = module.exports = global.cds = new class cds extends EventEmitter {
99
99
  get debug() { return super.debug = this.log.debug }
100
100
  get lazify() { return lazify }
101
101
  get lazified() { return lazify }
102
- get clone() { return super.clone = this.utils.clone }
102
+ clone(x) { return structuredClone(x) }
103
103
  exit(code){ return cds.shutdown ? cds.shutdown() : process.exit(code) }
104
104
 
105
105
  // Querying and Databases
@@ -140,4 +140,10 @@ extend (global) .with (class {
140
140
  if (process.env.CDS_JEST_MEM_FIX && typeof jest !== 'undefined') require('./utils/jest.js')
141
141
 
142
142
  // Allow for import cds from '@sap/cds' without esModuleInterop
143
- Object.defineProperties(module.exports, { default: {value:module.exports}, __esModule: {value:true} })
143
+ // FIXME: remove this flag in the next release. Only serves as fallback switch if people report issues with value:cds
144
+ // Setting it to module.exports lead to issues with vitest while setting it to cds apparently works fine.
145
+ if (process.env.CDS_ESM_INTEROP_DEFAULT) {
146
+ Object.defineProperties(module.exports, { default: {value:module.exports}, __esModule: {value:true} })
147
+ } else {
148
+ Object.defineProperties(module.exports, { default: {value:cds}, __esModule: {value:true} })
149
+ }
@@ -66,6 +66,7 @@ const types = _common ({ __proto__: roots,
66
66
  Time: {type:'date'},
67
67
  DateTime: {type:'date'},
68
68
  Timestamp: {type:'DateTime'},
69
+ Vector: {type:'Binary'},
69
70
  })
70
71
 
71
72
  /**
@@ -66,7 +66,7 @@ module.exports = function format(module, level, ...args) {
66
66
  toLog.timestamp = new Date()
67
67
 
68
68
  // start message with leading string args (if any)
69
- const i = args.findIndex(arg => typeof arg === 'object' && arg.message)
69
+ const i = args.findIndex(arg => typeof arg === 'object' && arg?.message)
70
70
  if (i > 0 && args.slice(0, i).every(arg => typeof arg === 'string')) toLog.msg = args.splice(0, i).join(' ')
71
71
 
72
72
  // merge toLog with passed Error (or error-like object)
@@ -86,6 +86,9 @@ module.exports = function format(module, level, ...args) {
86
86
  // append remaining args via util.format()
87
87
  if (args.length) toLog.msg = toLog.msg ? util.format(toLog.msg, ...args) : util.format(...args)
88
88
 
89
+ // ensure type (required on kubernetes if logs are pulled instead of pushed through binding)
90
+ toLog.type ??= 'log'
91
+
89
92
  // REVISIT: should not be necessary with new protocol adapters
90
93
  // 4xx: lower to warning (if error)
91
94
  if (toLog.level && toLog.level.match(/error/i) && _is4xx(toLog)) toLog.level = 'warn'
package/lib/plugins.js CHANGED
@@ -37,8 +37,8 @@ exports.activate = async function () {
37
37
  const p = require (conf.impl)
38
38
  if(p.activate) {
39
39
  cds.log('plugins').warn(`WARNING: \n
40
- Returning an 'activate' function is deprecated and won't be
41
- supported in future releases. Please return a Promise with 'module.exports' instead.
40
+ The @sap/cds plugin ${conf.impl} contains an 'activate' function, which is deprecated and won't be
41
+ supported in future releases. Please rewrite the plugin to return a Promise within 'module.exports'.
42
42
  `)
43
43
  await p.activate(conf)
44
44
  }
package/lib/ql/SELECT.js CHANGED
@@ -201,13 +201,10 @@ const _projection4 = (fn) => {
201
201
  apply: (_, __, args) => { // handle nested projections e.g. (foo)=>{ foo.bar (b=>{ ... }) }
202
202
  const [a, b] = args
203
203
  if (!a) col.expand = ['*']
204
- else if (a.raw) {
205
- if (a[0] === '*') col.expand = ['*']
206
- else if (a[0] === '.*') col.inline = ['*']
207
- else {
208
- let {columns} = SELECT_(col.ref[col.ref.length-1] +' ', args, ' from X')
209
- Object.assign (col, columns[0])
210
- }
204
+ else if (a.raw) switch (a[0]) {
205
+ case '*': col.expand = ['*']; break
206
+ case '.*': col.inline = ['*']; break
207
+ default: Object.assign (col, SELECT_(col.ref.at(-1)+' ', args, ' from X').columns[0])
211
208
  }
212
209
  else if (Array.isArray(a)) col.expand = _columns(a)
213
210
  else if (a === '*') col.expand = ['*']
@@ -215,7 +212,10 @@ const _projection4 = (fn) => {
215
212
  else if (typeof a === 'string') col.ref.push(a)
216
213
  else if (typeof a === 'function') {
217
214
  let x = (col[/^\(?_\b/.test(a) ? 'inline' : 'expand'] = _projection4(a))
218
- if (b && b.levels) while (--b.levels) x.push({ ...col, expand: (x = [...x]) })
215
+ if (b?.levels) while (--b.levels) x.push({ ...col, expand: (x = [...x]) })
216
+ } else if (typeof b === 'function') {
217
+ let x = (col[/^\(?_\b/.test(b) ? 'inline' : 'expand'] = _projection4(b))
218
+ if (a?.depth) while (--a.depth) x.push({ ...col, expand: (x = [...x]) })
219
219
  }
220
220
  return nested
221
221
  },