@sap/cds 6.3.1 → 6.4.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 (114) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/apis/cds.d.ts +1 -1
  3. package/apis/core.d.ts +118 -90
  4. package/apis/cqn.d.ts +11 -2
  5. package/apis/internal/inference.d.ts +7 -2
  6. package/apis/ql.d.ts +45 -11
  7. package/apis/serve.d.ts +8 -1
  8. package/apis/services.d.ts +303 -305
  9. package/bin/build/buildTaskEngine.js +28 -36
  10. package/bin/build/buildTaskFactory.js +32 -81
  11. package/bin/build/buildTaskHandler.js +3 -2
  12. package/bin/build/buildTaskProvider.js +2 -2
  13. package/bin/build/buildTaskProviderFactory.js +5 -14
  14. package/bin/build/constants.js +0 -1
  15. package/bin/build/provider/buildTaskHandlerEdmx.js +7 -6
  16. package/bin/build/provider/buildTaskHandlerFeatureToggles.js +6 -5
  17. package/bin/build/provider/buildTaskHandlerInternal.js +9 -30
  18. package/bin/build/provider/buildTaskProviderInternal.js +70 -58
  19. package/bin/build/provider/fiori/index.js +6 -5
  20. package/bin/build/provider/hana/2migration.js +20 -3
  21. package/bin/build/provider/hana/2tabledata.js +1 -0
  22. package/bin/build/provider/hana/index.js +40 -17
  23. package/bin/build/provider/java/index.js +10 -10
  24. package/bin/build/provider/mtx/index.js +25 -16
  25. package/bin/build/provider/mtx/resourcesTarBuilder.js +22 -27
  26. package/bin/build/provider/mtx-extension/index.js +3 -2
  27. package/bin/build/provider/mtx-sidecar/index.js +16 -15
  28. package/bin/build/provider/nodejs/index.js +14 -56
  29. package/bin/build/util.js +56 -16
  30. package/bin/deploy/to-hana/cfUtil.js +4 -1
  31. package/bin/deploy/to-hana/gitUtil.js +1 -1
  32. package/bin/deploy/to-hana/hana.js +45 -38
  33. package/bin/deploy/to-hana/hdiDeployUtil.js +8 -9
  34. package/bin/deploy/to-hana/mtaUtil.js +13 -14
  35. package/bin/mtx/in-cds.js +3 -1
  36. package/bin/serve.js +1 -1
  37. package/bin/version.js +2 -1
  38. package/lib/compile/cds-compile.js +1 -0
  39. package/lib/compile/cdsc.js +1 -0
  40. package/lib/compile/etc/_localized.js +2 -2
  41. package/lib/compile/for/lean_drafts.js +83 -0
  42. package/lib/compile/for/nodejs.js +1 -0
  43. package/lib/compile/minify.js +2 -1
  44. package/lib/compile/parse.js +2 -1
  45. package/lib/compile/to/gql.js +1 -1
  46. package/lib/compile/to/sql.js +11 -1
  47. package/lib/core/entities.js +1 -1
  48. package/lib/core/index.js +8 -9
  49. package/lib/core/infer.js +1 -0
  50. package/lib/dbs/cds-deploy.js +97 -41
  51. package/lib/env/cds-env.js +9 -10
  52. package/lib/env/cds-requires.js +8 -2
  53. package/lib/env/defaults.js +0 -4
  54. package/lib/env/schemas/cds-rc.json +38 -0
  55. package/lib/ql/SELECT.js +10 -4
  56. package/lib/srv/bindings.js +1 -1
  57. package/lib/srv/factory.js +1 -1
  58. package/lib/srv/protocols/index.js +3 -1
  59. package/lib/srv/srv-methods.js +1 -1
  60. package/lib/utils/cds-utils.js +11 -0
  61. package/lib/utils/inflect.js +13 -12
  62. package/lib/utils/tar.js +53 -10
  63. package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +2 -2
  64. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +1 -1
  65. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +1 -1
  66. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/delete.js +1 -1
  67. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +1 -15
  68. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +1 -1
  69. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +1 -1
  70. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/errors/UriSyntaxError.js +1 -1
  71. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriParser.js +6 -1
  72. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/utils/BufferedWriter.js +1 -1
  73. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/validator/ConditionalRequestValidator.js +0 -12
  74. package/libx/_runtime/cds-services/adapter/odata-v4/utils/oDataConfiguration.js +1 -7
  75. package/libx/_runtime/cds-services/adapter/odata-v4/utils/result.js +4 -0
  76. package/libx/_runtime/cds-services/services/Service.js +23 -1
  77. package/libx/_runtime/cds-services/util/assert.js +0 -41
  78. package/libx/_runtime/common/composition/data.js +5 -1
  79. package/libx/_runtime/common/generic/auth/utils.js +3 -3
  80. package/libx/_runtime/common/generic/input.js +4 -24
  81. package/libx/_runtime/common/generic/paging.js +3 -3
  82. package/libx/_runtime/common/utils/csn.js +21 -15
  83. package/libx/_runtime/common/utils/draft.js +2 -1
  84. package/libx/_runtime/common/utils/resolveView.js +25 -4
  85. package/libx/_runtime/common/utils/rewriteAsterisks.js +3 -1
  86. package/libx/_runtime/common/utils/rowUUIDGenerator.js +21 -0
  87. package/libx/_runtime/common/utils/templateProcessor.js +12 -15
  88. package/libx/_runtime/common/utils/templateProcessorPathSerializer.js +23 -0
  89. package/libx/_runtime/db/expand/expandCQNToJoin.js +29 -12
  90. package/libx/_runtime/db/generic/input.js +7 -13
  91. package/libx/_runtime/db/sql-builder/UpsertBuilder.js +47 -0
  92. package/libx/_runtime/db/sql-builder/index.js +2 -0
  93. package/libx/_runtime/db/sql-builder/sqlFactory.js +9 -0
  94. package/libx/_runtime/db/utils/columns.js +4 -2
  95. package/libx/_runtime/fiori/generic/read.js +1 -12
  96. package/libx/_runtime/fiori/lean-draft.js +657 -0
  97. package/libx/_runtime/fiori/utils/handler.js +1 -1
  98. package/libx/_runtime/hana/pool.js +16 -1
  99. package/libx/_runtime/messaging/enterprise-messaging-utils/getTenantInfo.js +2 -1
  100. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +1 -1
  101. package/libx/_runtime/messaging/enterprise-messaging.js +2 -3
  102. package/libx/_runtime/messaging/outbox/utils.js +109 -70
  103. package/libx/_runtime/messaging/service.js +16 -7
  104. package/libx/_runtime/remote/Service.js +15 -2
  105. package/libx/_runtime/remote/utils/client.js +41 -11
  106. package/libx/_runtime/sqlite/Service.js +3 -0
  107. package/libx/_runtime/sqlite/convertDraftAdminPathExpression.js +56 -0
  108. package/libx/_runtime/sqlite/customBuilder/CustomUpsertBuilder.js +59 -0
  109. package/libx/_runtime/sqlite/customBuilder/index.js +5 -0
  110. package/libx/_runtime/sqlite/execute.js +1 -1
  111. package/libx/_runtime/types/api.js +2 -2
  112. package/libx/rest/RestAdapter.js +15 -13
  113. package/package.json +1 -1
  114. package/server.js +1 -0
@@ -1,4 +1,4 @@
1
- const cds = require('../index'), { local, inspect } = cds.utils
1
+ const cds = require('../index'), { local, inspect } = cds.utils, { UPSERT } = cds.ql
2
2
  const DEBUG = cds.debug('deploy')
3
3
 
4
4
  const colors = !!process.stdout.isTTY && !!process.stderr.isTTY
@@ -14,7 +14,7 @@ const term = {
14
14
  * deploy create tables and views in case of a SQL database, then fills
15
15
  * in initial data, if present.
16
16
  */
17
- exports = module.exports = function cds_deploy (model,options) { return {
17
+ exports = module.exports = function cds_deploy (model,options,csvs) { return {
18
18
 
19
19
  /** @param {cds.Service} db */
20
20
  async to (db, o = options || cds.options || {}) {
@@ -40,10 +40,10 @@ exports = module.exports = function cds_deploy (model,options) { return {
40
40
 
41
41
  // create tables & views...
42
42
  const any = await exports.create (db,model,o)
43
- if (!any) return db
43
+ if (!any && !csvs) return db
44
44
 
45
45
  // fill in initial data...
46
- await exports.init (db,model, file => LOG(
46
+ await exports.init (db,model,o,csvs, file => LOG(
47
47
  term.info(` > init from ${local(file)}`)
48
48
  ))
49
49
 
@@ -105,55 +105,113 @@ exports.exclude_external_entities_in = function (csn) { // NOSONAR
105
105
  }
106
106
  }
107
107
 
108
-
109
- exports.create = async function (db, csn=db.model, o) {
110
- if (!db.deploy || o.lean) {
111
- const creates = cds.compile.to.sql (csn,o); if (!creates || creates.length === 0) return
112
- const drops = creates.map (each => {
108
+ function getSqls(db, csn, o, beforeCsn) {
109
+ const schemaEvo = (db.options?.schema_evolution === 'auto' || o.schema_evolution === 'auto')
110
+ const creds = db.options?.credentials
111
+ const in_memory = (creds?.url || creds?.database) === ':memory:';
112
+ if (!in_memory && schemaEvo) {
113
+ const { afterImage: afterCsn, drops, createsAndAlters: creas } = cds.compile.to.sql.delta (csn, o, beforeCsn);
114
+ if(beforeCsn === undefined) {
115
+ // If this is the first deployment done with automatic schema evolution, generate everything as if it was a drop create
116
+ // but set the afterCsn in the db so we can calculate a delta going forward
117
+ return { afterCsn, drops: creas.map (each => {
118
+ let [, kind, entity] = each.match(/^CREATE (TABLE|VIEW) "?([^\s"(]+)/im) || []
119
+ return `DROP ${kind} IF EXISTS ${entity};`
120
+ }).reverse(), creas };
121
+ }
122
+ return { afterCsn, drops, creas };
123
+ } else {
124
+ const creas = cds.compile.to.sql(csn, o);
125
+ const drops = creas.map (each => {
113
126
  let [, kind, entity] = each.match(/^CREATE (TABLE|VIEW) "?([^\s"(]+)/im) || []
114
127
  return `DROP ${kind} IF EXISTS ${entity};`
115
- }).reverse()
116
- if (o.dry) {
117
- console.log(); for (let each of drops) console.log(each)
118
- console.log(); for (let each of creates) console.log(each,'\n')
119
- return
120
- } else return db.run (async tx => {
121
- await tx.run(drops)
122
- await tx.run(creates)
123
- return true
124
- })
128
+ }).reverse();
129
+ return { afterCsn: {}, drops, creas };
125
130
  }
126
- else return db.deploy (csn,o)
127
131
  }
128
132
 
133
+ exports.create = async function (db, csn=db.model, o) {
134
+ const schemaEvo = (db.options?.schema_evolution === 'auto' || o.schema_evolution === 'auto')
135
+ if(db.deploy && !schemaEvo) {
136
+ // reset CSN state saved in db - if there is any
137
+ await db.run('DROP table if exists cds_Model;');
138
+ return db.deploy(csn, o);
139
+ }
129
140
 
130
- exports.init = (db, csn=db.model, log=()=>{}) => db.run (async tx => {
141
+ const in_memory = db.options?.credentials?.url === ':memory:';
142
+ let beforeCsn
143
+ if (!in_memory && schemaEvo) try {
144
+ const [{ csn }] = await db.read('cds.Model')
145
+ beforeCsn = JSON.parse(csn);
146
+ } catch(e) {
147
+ if (e.message.includes('no such table: cds_Model')) {
148
+ await db.run('CREATE table cds_Model ( csn CLOB )')
149
+ await db.insert({csn:''}).into('cds.Model')
150
+ }
151
+ }
152
+
153
+ const { afterCsn, drops, creas } = getSqls(db, csn, o, beforeCsn);
154
+
155
+ if (!creas || creas.length === 0) return
156
+ if (o.dry) {
157
+ console.log(); for (let each of drops) console.log(each,'\n')
158
+ console.log(); for (let each of creas) console.log(each,'\n')
159
+ return
160
+ } else return db.run (async tx => {
161
+ await tx.run(drops)
162
+ await tx.run(creas)
163
+ if (!in_memory && schemaEvo) {
164
+ await tx.update('cds.Model').with({ csn: JSON.stringify(afterCsn) })
165
+ }
166
+ return true
167
+ })
168
+ }
169
+
170
+
171
+ exports.init = (db, csn=db.model, o, csvs, log=()=>{}) => db.run (async tx => {
131
172
  const resources = await exports.resources(csn, {testdata: cds.env.features.test_data})
132
173
  const inits=[]
133
- for (let [file,e] of Object.entries(resources)) {
134
- if (e === '*') { // init.js/ts
135
- let x = await cds.utils._import(file); if (!x) continue
136
- if (x.default) x = x.default // default ESM export
137
- inits.push (!x.then && typeof x === 'function' ? x(db,csn) : x)
138
- log (file)
139
- } else { // from .csv or .json
140
- const INSERT_into = _from_csv_or_json [path.extname(file)]
141
- const src = await read(file,'utf8'); if (!src) continue
142
- const q = INSERT_into (e,src); if (!q) continue
174
+ const schemaEvo = (db.options?.schema_evolution === 'auto' || o?.schema_evolution === 'auto')
175
+
176
+
177
+ if (csvs) {
178
+ const ccsn = cds.compile.for['nodejs'](csn) // compile to calculate keys for newly added entities
179
+ for(let [e,src] of Object.entries(csvs)) {
180
+ const q = INSERT_from_csv (e,src,schemaEvo); if (!q) continue
143
181
  if (db.kind === 'better-sqlite') _add_missing_pks2(q)
144
- log (file,e)
182
+ q._target = ccsn.definitions[e]
145
183
  inits.push (tx.run(q) .catch (e => {
146
184
  throw Object.assign (e, { message: 'in cds.deploy(): ' + e.message +'\n'+ inspect(q) })
147
185
  }))
148
186
  }
187
+ } else {
188
+ for (let [file,e] of Object.entries(resources)) {
189
+ if (e === '*') { // init.js/ts
190
+ let x = await cds.utils._import(file); if (!x) continue
191
+ if (x.default) x = x.default // default ESM export
192
+ inits.push (!x.then && typeof x === 'function' ? x(db,csn) : x)
193
+ log (file)
194
+ } else { // from .csv or .json
195
+ const INSERT_into = _from_csv_or_json [path.extname(file)]
196
+ const src = await read(file,'utf8'); if (!src) continue
197
+ const q = INSERT_into (e,src,schemaEvo); if (!q) continue
198
+ if (db.kind === 'better-sqlite') _add_missing_pks2(q)
199
+ if (cds.requires['cds.xt.ModelProviderService']?.kind === 'in-sidecar') q._target = csn.definitions[e]
200
+ log (file,e)
201
+ inits.push (tx.run(q) .catch (e => {
202
+ throw Object.assign (e, { message: 'in cds.deploy(): ' + e.message +'\n'+ inspect(q) })
203
+ }))
204
+ }
205
+ }
149
206
  }
207
+
150
208
  await Promise.all (inits)
151
209
 
152
210
  function _add_missing_pks2 (q) {
153
- const {columns,rows} = 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
211
+ 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
154
212
  if (columns) {
155
213
  const entity = csn.definitions[q._target.name], {uuid} = cds.utils
156
- for (let k in entity.keys) if (!columns.includes(k)) {
214
+ for (let k in entity.keys) if (!columns.includes(k) && !entity.keys[k].isAssociation) {
157
215
  columns.push(k)
158
216
  const t = entity.keys[k]._type, pk = t === 'cds.UUID' ? uuid : index => index+1
159
217
  rows.forEach ((row,index) => row.push(pk(index)))
@@ -216,7 +274,7 @@ const _entity4 = (file,csn) => {
216
274
  if (!entity) {
217
275
  if (/(.+)[._]texts_?/.test(name)) { // 'Books.texts', 'Books.texts_de'
218
276
  const base = csn.definitions [RegExp.$1]
219
- return base && _entity4 (base.elements.texts.target,csn)
277
+ return base?.elements?.texts && _entity4 (base.elements.texts.target, csn)
220
278
  }
221
279
  else return DEBUG && DEBUG (`warning: ${name} not in model`)
222
280
  }
@@ -228,16 +286,14 @@ const _entity4 = (file,csn) => {
228
286
  return entity.name ? entity : { name, __proto__:entity }
229
287
  }
230
288
 
231
- const INSERT_from_csv = (entity, csv) => {
289
+ const INSERT_from_csv = (entity, csv, schemaEvo) => {
232
290
  let [ cols, ...rows ] = cds.parse.csv (csv)
233
- if (rows.length > 0) return INSERT.into (entity) .columns (cols) .rows (rows)
234
- // if (rows.length > 0) return UPSERT.into (entity) .columns (cols) .rows (rows)
291
+ if (rows.length > 0) return (schemaEvo ? UPSERT : INSERT).into (entity) .columns (cols) .rows (rows)
235
292
  }
236
293
 
237
- const INSERT_from_json = (entity, json) => {
294
+ const INSERT_from_json = (entity, json, schemaEvo) => {
238
295
  let records = JSON.parse (json)
239
- if (records.length > 0) return INSERT.into (entity) .entries (records)
240
- // if (records.length > 0) return UPSERT.into (entity) .entries (records)
296
+ if (records.length > 0) return (schemaEvo ? UPSERT : INSERT).into (entity) .entries (records)
241
297
  }
242
298
 
243
299
  const _from_csv_or_json = { '.json': INSERT_from_json, '.csv': INSERT_from_csv, }
@@ -64,19 +64,16 @@ class Config {
64
64
  }
65
65
  }
66
66
 
67
- // 4. link dependent services (through kind/use)
68
- this._link_required_services()
69
-
70
- // 5. add process env
67
+ // 4. add process env before linking to allow things like CDS_requires_db=sql
71
68
  this._add_process_env(_context, _home)
72
69
 
73
- // 6. link dependent services again -> e.g. to allow things like CDS_requires_db=sql
70
+ // 5. link dependent services
74
71
  this._link_required_services()
75
72
 
76
- // 7. complete service configurations from cloud service bindings
73
+ // 6. complete service configurations from cloud service bindings
77
74
  this._add_cloud_service_bindings(process.env)
78
75
 
79
- // 8. Add compatibility for mtx
76
+ // 7. Add compatibility for mtx
80
77
  if (this.requires && this.requires.db) {
81
78
  if (this.requires.multitenancy !== undefined) {
82
79
  Object.defineProperty(this.requires.db, 'multiTenant', { value: !!this.requires.multitenancy })
@@ -84,7 +81,7 @@ class Config {
84
81
  else if (this.requires.db.multiTenant !== undefined) this.requires.multitenancy = this.requires.db.multiTenant
85
82
  }
86
83
 
87
- // 9. apply presets
84
+ // 8. apply presets
88
85
  presets (this)
89
86
 
90
87
  // Only if feature is enabled
@@ -230,10 +227,12 @@ class Config {
230
227
  if (!config) config = {}
231
228
  const pref_ = RegExp('^'+prefix+'[._]','i')
232
229
  for (let p in env) if (!(p in my) && pref_.test(p)) {
233
- const key = /[a-z]/.test(p) ? p : p.toLowerCase() //> CDS_FOO_BAR -> cds_foo_bar
234
- const path = key.slice(prefix.length+1) .split (key[prefix.length]) //> ['foo','bar']
230
+ const pEsc = p.replace(/__/g, '!!') // escaping of _ by __ : protect __ so that it's not split below
231
+ const key = /[a-z]/.test(pEsc) ? pEsc : pEsc.toLowerCase() //> CDS_FOO_BAR -> cds_foo_bar
232
+ let path = key.slice(prefix.length+1) .split (key[prefix.length]) //> ['foo','bar']
235
233
  for (var o=config,next;;) {
236
234
  next = path.shift()
235
+ next = next.replace(/!!/g, '_') // undo !! protection and reduce __ to _
237
236
  if (!path.length) break
238
237
  if (!path[0]) next = next+'-'+path.shift()+path.shift() // foo__bar -> foo-bar
239
238
  o = o[next] || (o[next] = {})
@@ -7,6 +7,10 @@ exports = module.exports = {
7
7
  middlewares: true
8
8
  },
9
9
 
10
+ "[schevo]": {
11
+ db: { schema_evolution: 'auto' },
12
+ },
13
+
10
14
  db: undefined,
11
15
  messaging: undefined,
12
16
  multitenancy: undefined,
@@ -251,8 +255,10 @@ require = (id) => { // eslint-disable-line no-global-assign
251
255
  }
252
256
 
253
257
  // REVISIT: we should have a real modular plugin technique for cds.env
254
- // eslint-disable-next-line cds/no-missing-dependencies
255
- const _mtxs = !process.env.OLD_MTX && require('@sap/cds-mtxs/lib/env-requires') || {
258
+ const utils = require ('../utils/cds-utils')
259
+ const _mtxs = !utils._oldMtx()
260
+ // eslint-disable-next-line cds/no-missing-dependencies
261
+ && require('@sap/cds-mtxs/lib/env-requires') || {
256
262
  "toggles": {
257
263
  model: "@sap/cds/srv/mtx"
258
264
  },
@@ -12,10 +12,6 @@ module.exports = {
12
12
  },
13
13
 
14
14
  features: {
15
- "[schevo]": {
16
- schema_evolution: true,
17
- },
18
- schema_evolution: false,
19
15
  folders: 'fts/*', // where to find feature toggles -> switch on by default when released
20
16
  cls: major > 12 || major == 12 && minor >= 18,
21
17
  live_reload: !production,
@@ -237,6 +237,10 @@
237
237
  }
238
238
  ]
239
239
  },
240
+ "middlewares": {
241
+ "type": "boolean",
242
+ "description": "Enables new middlewares support (experimental)."
243
+ },
240
244
  "multitenancy": {
241
245
  "type": "boolean",
242
246
  "description": "Shortcut to enable multitenancy."
@@ -360,6 +364,40 @@
360
364
  ]
361
365
  }
362
366
  }
367
+ },
368
+ "protocols": {
369
+ "type": "object",
370
+ "description": "List protocols to enable in addition to the default protocol adaptors.",
371
+ "additionalProperties": true,
372
+ "patternProperties": {
373
+ ".+": {
374
+ "oneOf": [
375
+ {
376
+ "type": "boolean",
377
+ "description": "Enables a built-in protocol adapter with defaults"
378
+ },
379
+ {
380
+ "type": "string",
381
+ "description": "The endpoint to serve this protocol at."
382
+ },
383
+ {
384
+ "type": "object",
385
+ "default": {},
386
+ "additionalProperties": true,
387
+ "properties": {
388
+ "path": {
389
+ "type": "string",
390
+ "description": "The endpoint to serve this protocol at."
391
+ },
392
+ "impl": {
393
+ "type": "string",
394
+ "description": "The module pathname of the protocol adapter implementation."
395
+ }
396
+ }
397
+ }
398
+ ]
399
+ }
400
+ }
363
401
  }
364
402
  },
365
403
  "$defs": {
package/lib/ql/SELECT.js CHANGED
@@ -6,16 +6,22 @@ module.exports = class Query extends Whereable {
6
6
  static _api() {
7
7
  const $ = Object.assign
8
8
  return $((..._) => new this()._select_or_from(..._), {
9
- one: $((...x) => new this({one:true})._select_or_from(...x),{
10
- columns: (..._) => new this({one:true}).columns(..._),
11
- from: (..._) => new this({one:true}).from(..._),
9
+ localized: $((...x) => new this({localized:true})._select_or_from(...x),{
10
+ columns: (..._) => new this({localized:true}).columns(..._),
11
+ from: (..._) => new this({localized:true}).from(..._),
12
12
  }),
13
13
  distinct: $((...x) => new this({distinct:true})._select_or_from(...x),{
14
14
  columns: (..._) => new this({distinct:true}).columns(..._),
15
15
  from: (..._) => new this({distinct:true}).from(..._),
16
16
  }),
17
+ one: $((...x) => new this({one:true})._select_or_from(...x),{
18
+ columns: (..._) => new this({one:true}).columns(..._),
19
+ from: (..._) => new this({one:true}).from(..._),
20
+ }),
21
+ from: $((..._) => new this().from(..._), {
22
+ localized: (..._) => new this({localized:true}).from(..._)
23
+ }),
17
24
  columns: (..._) => new this().columns(..._),
18
- from: (..._) => new this().from(..._),
19
25
  })
20
26
  }
21
27
 
@@ -12,7 +12,7 @@ module.exports = class Bindings {
12
12
  static get registry(){ return registry }
13
13
 
14
14
  static then(r,e) {
15
- const LOG = cds.log('serve', { prefix:'cds' })
15
+ const LOG = cds.log('cds.serve', { prefix:'cds' })
16
16
  const bindings = new Bindings
17
17
  cds.prependOnceListener ('connect', ()=> LOG._info && LOG.info ('connect using bindings from:', { registry }))
18
18
  cds.once('listening', ({url})=> bindings.export (cds.service.providers, url))
@@ -1,6 +1,6 @@
1
1
  const cds = require('..'), { path, isfile } = cds.utils
2
2
  const paths = Array.from (new Set ([ cds.root, ...require.resolve.paths('x') ]))
3
- const DEBUG = cds.debug('srv.factory'); DEBUG && DEBUG ({ 'cds.root':cds.root, paths })
3
+ const DEBUG = cds.debug('cds.service.factory',); DEBUG && DEBUG ({ 'cds.root':cds.root, paths })
4
4
 
5
5
  /** @typedef {import('./srv-api')} Service @type { (()=>Service) & (new()=>Service) } */
6
6
  const ServiceFactory = function (name, model, options) { //NOSONAR
@@ -10,7 +10,9 @@ class ProtocolAdapter {
10
10
  for (let [k,o] of Object.entries(protocols)) if (typeof o === 'string') protocols[k] = {path:o}
11
11
  if (!protocols.odata) protocols.odata = { impl: join(__dirname,'odata-v4') }
12
12
  if (!protocols.rest) protocols.rest = { impl: join(__dirname,'rest') }
13
- return this.protocols = protocols
13
+
14
+ // odata must always be first for fallback
15
+ return this.protocols = { odata: protocols.odata, ...protocols }
14
16
  }
15
17
 
16
18
  /**
@@ -1,5 +1,5 @@
1
1
  const cds = require('..')
2
- const LOG = cds.log('cds-app-service-methods')
2
+ const LOG = cds.log('cds.serve',{label:'cds'})
3
3
 
4
4
 
5
5
  module.exports = (srv) => {
@@ -176,6 +176,17 @@ exports.find = function find (base, patterns='*', filter=()=>true) {
176
176
  return files
177
177
  }
178
178
 
179
+ // TODO find a better place
180
+ exports._oldMtx = function _oldMtx() {
181
+ function _hasMtxDependency() {
182
+ try {
183
+ return require(join(cds.root, 'package.json'))?.dependencies?.['@sap/cds-mtx']
184
+ } catch(e) {
185
+ return false
186
+ }
187
+ }
188
+ return (_hasMtxDependency() || process.env.OLD_MTX?.toLowerCase() === 'true') ?? false
189
+ }
179
190
 
180
191
  /**
181
192
  * Internal utility to load a file through ESM or CommonJs. TODO find a better place.
@@ -1,24 +1,25 @@
1
- const last = /\w+$/
2
1
 
3
- exports.singular4 = (dn,stripped) => {
2
+ this.singular4 = (dn,stripped) => {
4
3
  let n = dn.name || dn; if (stripped) n = n.match(last)[0]
5
4
  return dn['@singular'] || (
6
- /.*species|news$/i.test(n) ? n :
7
- /.*ess$/.test(n) ? n : // Address
8
- /.*ees$/.test(n) ? n.slice(0, -1) : // Employees --> Employee
9
- /.*[sz]es$/.test(n) ? n.slice(0, -2) :
10
- /.*[^aeiou]ies$/.test(n) ? n.slice(0, -3) + 'y' : // Deliveries --> Delivery
11
- /.*s$/.test(n) ? n.slice(0, -1) :
5
+ /species|news$/i.test(n) ? n :
6
+ /ess$/.test(n) ? n : // Address
7
+ /ees$/.test(n) ? n.slice(0, -1) : // Employees --> Employee
8
+ /[sz]es$/.test(n) ? n.slice(0, -2) :
9
+ /[^aeiou]ies$/.test(n) ? n.slice(0, -3) + 'y' : // Deliveries --> Delivery
10
+ /s$/.test(n) ? n.slice(0, -1) :
12
11
  n
13
12
  )
14
13
  }
15
14
 
16
- exports.plural4 = (dn,stripped) => {
15
+ this.plural4 = (dn,stripped) => {
17
16
  let n = dn.name || dn; if (stripped) n = n.match(last)[0]
18
17
  return dn['@plural'] || (
19
- /.*analysis|status|species|news$/i.test(n) ? n :
20
- /.*[^aeiou]y$/.test(n) ? n.slice(0,-1) + 'ies' :
21
- /.*(s|x|z|ch|sh)$/.test(n) ? n + 'es' :
18
+ /analysis|status|species|sheep|news$/i.test(n) ? n :
19
+ /[^aeiou]y$/.test(n) ? n.slice(0,-1) + 'ies' :
20
+ /(s|x|z|ch|sh)$/.test(n) ? n + 'es' :
22
21
  n + 's'
23
22
  )
24
23
  }
24
+
25
+ const last = /\w+$/
package/lib/utils/tar.js CHANGED
@@ -5,7 +5,7 @@ const spawn = /\btar\b/.test(process.env.DEBUG) ? (cmd, args, options) => {
5
5
  return child_process.spawn(cmd, args, options)
6
6
  } : child_process.spawn
7
7
 
8
- const cds = require('../index'), { fs, path, mkdirp } = cds.utils
8
+ const cds = require('../index'), { fs, path, mkdirp, exists, rimraf } = cds.utils
9
9
  const _resolve = (...x) => path.resolve (cds.root,...x)
10
10
 
11
11
  // tar does not work properly on Windows (by npm/jest tests) w/o this change
@@ -15,6 +15,21 @@ const win = path => {
15
15
  if (Array.isArray(path)) return path.map(el => win(el))
16
16
  }
17
17
 
18
+ // Copy files to temp dir on Windows and pack temp dir.
19
+ // cli tar has a size limit on Windows.
20
+ const createTemp = async (root, files) => {
21
+ const temp = await fs.promises.mkdtemp(`${fs.realpathSync(require('os').tmpdir())}${path.sep}tar-`)
22
+ for (const file of files) {
23
+ const fname = path.relative(root, file)
24
+ const destination = path.join(temp, fname)
25
+ const dirname = path.dirname(destination)
26
+ if (!await exists(dirname)) await fs.promises.mkdir(dirname, { recursive: true })
27
+ await fs.promises.copyFile(file, destination)
28
+ }
29
+
30
+ return temp
31
+ }
32
+
18
33
  /**
19
34
  * Creates a tar archive, to an in-memory Buffer, or piped to write stream or file.
20
35
  * @example ```js
@@ -39,15 +54,31 @@ const win = path => {
39
54
  * - `.then()` collects the tar output into an in-memory `Buffer`
40
55
  * - `.to()` is a convenient shortcut to pipe the output into a write stream
41
56
  */
42
- exports.create = (dir='.', ...args) => {
57
+ exports.create = async (dir='.', ...args) => {
43
58
 
44
59
  if (typeof dir === 'string') dir = _resolve(dir)
45
60
  if (Array.isArray(dir)) [ dir, ...args ] = [ cds.root, dir, ...args ]
46
- if (Array.isArray(args[0])) args.push (...args.shift().map (f => path.isAbsolute(f) ? path.relative(dir,f) : f))
47
- else args.push('.')
48
- const c = process.platform === 'linux'
49
- ? spawn (`tar c -C ${dir}`, args, { shell:true })
50
- : spawn (`tar cf - -C ${win(dir)}`, args, { shell:true })
61
+
62
+ let c, temp
63
+ if (process.platform === 'win32') {
64
+ const spawnDir = (dir, args) => {
65
+ if (args.some(arg => arg === '-f')) return spawn ('tar', ['c', '-C', win(dir), ...win(args)])
66
+ else return spawn ('tar', ['cf', '-', '-C', win(dir), ...win(args)])
67
+ }
68
+ args.push('.')
69
+ if (Array.isArray(args[0])) {
70
+ temp = await createTemp(dir, args[0])
71
+ args.shift()
72
+ c = spawnDir(temp, args)
73
+ } else {
74
+ c = spawnDir(dir, args)
75
+ }
76
+ } else {
77
+ if (Array.isArray(args[0])) args.push (...args.shift().map (f => path.isAbsolute(f) ? path.relative(dir,f) : f))
78
+ else args.push('.')
79
+
80
+ c = spawn ('tar', ['c', '-C', dir, ...args])
81
+ }
51
82
 
52
83
  return {__proto__:c, // returning a thenable + fluent ChildProcess...
53
84
 
@@ -57,9 +88,21 @@ exports.create = (dir='.', ...args) => {
57
88
  * @example const buffer = await tar.c('src/dir')
58
89
  */
59
90
  then (r,e) {
60
- const bb=[]; c.stdout.on('data', b => bb.push(b))
61
- c.on('close', ()=>r(Buffer.concat(bb)))
91
+ const bb=[]
92
+ const eb=[]
93
+ c.stdout.on('data', b => bb.push(b))
94
+ c.stderr.on('data', b => eb.push(b))
95
+ c.on('close', (code) => {
96
+ if (code === 0) {
97
+ return r(Buffer.concat(bb))
98
+ }
99
+ e(new Error('tar: ' + Buffer.concat(eb)))
100
+ })
62
101
  c.on('error', e)
102
+ if (process.platform === 'win32') {
103
+ c.on('close', async () => temp && exists(temp) && await rimraf(temp))
104
+ c.on('error', async () => temp && exists(temp) && await rimraf(temp))
105
+ }
63
106
  },
64
107
 
65
108
  /**
@@ -107,7 +150,7 @@ exports.extract = (archive, ...args) => ({
107
150
  to (...dest) {
108
151
  if (typeof dest === 'string') dest = _resolve(...dest)
109
152
  const input = typeof archive !== 'string' || archive == '-' ? '-' : _resolve(archive)
110
- const x = spawn(`tar xf ${win(input)} -C ${win(dest)}`, args, { shell:true })
153
+ const x = spawn('tar', ['xf', win(input), '-C', win(dest), ...args])
111
154
  if (archive === '-') return x.stdin
112
155
  if (Buffer.isBuffer(archive)) archive = require('stream').Readable.from (archive)
113
156
  if (typeof archive !== 'string') archive.pipe (x.stdin)
@@ -162,10 +162,10 @@ class ODataRequest extends cds.Request {
162
162
  * super
163
163
  */
164
164
  const { user } = req
165
-
165
+ const tenant = req.tenant || user?.tenant
166
166
  // REVISIT: public API for query options (express style req.query already in use)?
167
167
  const _queryOptions = odataReq.getQueryOptions()
168
- super({ event, target, data, query, user, method, headers, req, res, _queryOptions })
168
+ super({ event, target, data, query, user, method, headers, req, res, _queryOptions, tenant })
169
169
  }
170
170
 
171
171
  /*
@@ -82,7 +82,7 @@ const action = service => {
82
82
  await tx.rollback(e).catch(() => {})
83
83
  }
84
84
  } finally {
85
- req.messages && odataRes.setHeader('sap-messages', getSapMessages(req.messages, req._.req))
85
+ req.messages && odataRes.setHeader('sap-messages', getSapMessages(req.messages, req.http.req))
86
86
 
87
87
  if (err) next(err)
88
88
  else next(null, toODataResult(result, req))
@@ -68,7 +68,7 @@ const create = service => {
68
68
  await tx.rollback(e).catch(() => {})
69
69
  }
70
70
  } finally {
71
- req.messages && odataRes.setHeader('sap-messages', getSapMessages(req.messages, req._.req))
71
+ req.messages && odataRes.setHeader('sap-messages', getSapMessages(req.messages, req.http.req))
72
72
 
73
73
  if (err) next(err)
74
74
  else next(null, toODataResult(result, req))
@@ -49,7 +49,7 @@ const del = service => {
49
49
  await tx.rollback(e).catch(() => {})
50
50
  }
51
51
  } finally {
52
- req.messages && odataRes.setHeader('sap-messages', getSapMessages(req.messages, req._.req))
52
+ req.messages && odataRes.setHeader('sap-messages', getSapMessages(req.messages, req.http.req))
53
53
 
54
54
  if (err) next(err)
55
55
  else next(null, null)
@@ -20,20 +20,6 @@ const metadata = service => {
20
20
 
21
21
  try {
22
22
  const { 'cds.xt.ModelProviderService': mps } = cds.services
23
- // REVISIT: The following block should replaced with the one commented bellow after the next release of cds-mtxs.
24
- // Currently lkg tests fail w/o the try-catch.
25
- let edmx
26
- try {
27
- if (mps) edmx = await mps.getEdmx({ tenant, model: service.model, service: service.definition.name, locale })
28
- // eslint-disable-next-line no-empty
29
- } catch (_) {}
30
- if (!edmx)
31
- edmx = cds.localize(
32
- service.model,
33
- locale,
34
- cds.compile.to.edmx(service.model, { service: service.definition.name })
35
- )
36
- /*
37
23
  let edmx = mps
38
24
  ? await mps.getEdmx({ tenant, model: service.model, service: service.definition.name, locale })
39
25
  : cds.localize(
@@ -41,7 +27,7 @@ const metadata = service => {
41
27
  locale,
42
28
  // REVISIT: we could cache this in model._cached
43
29
  cds.compile.to.edmx(service.model, { service: service.definition.name })
44
- ) */
30
+ )
45
31
  return next(null, toODataResult(edmx))
46
32
  } catch (e) {
47
33
  if (LOG._error) {
@@ -491,7 +491,7 @@ const read = service => {
491
491
  await tx.rollback(e).catch(() => {})
492
492
  }
493
493
  } finally {
494
- req.messages && odataRes.setHeader('sap-messages', getSapMessages(req.messages, req._.req))
494
+ req.messages && odataRes.setHeader('sap-messages', getSapMessages(req.messages, req.http.req))
495
495
 
496
496
  if (err) next(err)
497
497
  else next(null, result, additional)