@sap/cds 7.6.3 → 7.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.
- package/CHANGELOG.md +38 -1
- package/_i18n/i18n.properties +3 -0
- package/app/index.js +18 -12
- package/bin/serve.js +51 -19
- package/common.cds +16 -0
- package/lib/auth/ias-auth.js +2 -2
- package/lib/auth/index.js +1 -1
- package/lib/auth/jwt-auth.js +1 -1
- package/lib/compile/cdsc.js +23 -11
- package/lib/compile/for/nodejs.js +2 -2
- package/lib/compile/for/odata.js +4 -0
- package/lib/compile/load.js +7 -2
- package/lib/compile/to/sql.js +3 -0
- package/lib/dbs/cds-deploy.js +197 -220
- package/lib/env/defaults.js +2 -1
- package/lib/index.js +8 -2
- package/lib/linked/types.js +1 -0
- package/lib/log/format/json.js +1 -1
- package/lib/plugins.js +2 -2
- package/lib/ql/Query.js +1 -1
- package/lib/ql/SELECT.js +8 -8
- package/lib/req/context.js +22 -13
- package/lib/req/request.js +10 -4
- package/lib/srv/cds-connect.js +9 -3
- package/lib/srv/cds-serve.js +5 -3
- package/lib/srv/middlewares/ctx-model.js +1 -1
- package/lib/srv/protocols/odata-v4.js +38 -9
- package/lib/srv/srv-api.js +98 -140
- package/lib/srv/srv-models.js +2 -2
- package/lib/srv/srv-tx.js +1 -0
- package/lib/utils/cds-utils.js +32 -23
- package/lib/utils/data.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +1 -3
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +0 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +18 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/index.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/utils.js +7 -3
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriParser.js +2 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/http/HttpHeaderReader.js +4 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/index.js +5 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +71 -25
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +10 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +6 -1
- package/libx/_runtime/cds-services/util/assert.js +50 -240
- package/libx/_runtime/cds.js +5 -0
- package/libx/_runtime/common/aspects/any.js +53 -45
- package/libx/_runtime/common/generic/input.js +14 -10
- package/libx/_runtime/common/generic/paging.js +1 -1
- package/libx/_runtime/common/utils/cqn2cqn4sql.js +1 -1
- package/libx/_runtime/common/utils/keys.js +1 -1
- package/libx/_runtime/common/utils/quotingStyles.js +1 -1
- package/libx/_runtime/common/utils/resolveStructured.js +4 -1
- package/libx/_runtime/common/utils/rewriteAsterisks.js +5 -12
- package/libx/_runtime/common/utils/stream.js +2 -16
- package/libx/_runtime/common/utils/streamProp.js +16 -6
- package/libx/_runtime/common/utils/ucsn.js +1 -0
- package/libx/_runtime/db/utils/columns.js +6 -1
- package/libx/_runtime/fiori/generic/activate.js +11 -3
- package/libx/_runtime/fiori/generic/edit.js +8 -2
- package/libx/_runtime/fiori/lean-draft.js +99 -30
- package/libx/_runtime/hana/execute.js +2 -5
- package/libx/_runtime/messaging/service.js +6 -2
- package/libx/common/assert/index.js +232 -0
- package/libx/common/assert/type.js +109 -0
- package/libx/common/assert/utils.js +125 -0
- package/libx/common/assert/validation.js +109 -0
- package/libx/odata/index.js +5 -5
- package/libx/odata/middleware/create.js +83 -0
- package/libx/odata/middleware/delete.js +38 -0
- package/libx/odata/middleware/error.js +8 -0
- package/libx/odata/{metadata.js → middleware/metadata.js} +8 -6
- package/libx/odata/middleware/operation.js +78 -0
- package/libx/odata/middleware/parse.js +11 -0
- package/libx/odata/{read.js → middleware/read.js} +42 -20
- package/libx/odata/{service-document.js → middleware/service-document.js} +2 -1
- package/libx/odata/middleware/stream.js +237 -0
- package/libx/odata/middleware/update.js +165 -0
- package/libx/odata/{afterburner.js → parse/afterburner.js} +79 -29
- package/libx/odata/{cqn2odata.js → parse/cqn2odata.js} +5 -3
- package/libx/odata/{parseToCqn.js → parse/parseToCqn.js} +3 -6
- package/libx/odata/{utils.js → utils/index.js} +91 -9
- package/libx/outbox/index.js +5 -4
- package/libx/rest/RestAdapter.js +0 -1
- package/libx/rest/middleware/operation.js +6 -4
- package/libx/rest/middleware/parse.js +20 -2
- package/package.json +1 -1
- package/server.js +43 -71
- package/libx/odata/create.js +0 -44
- package/libx/odata/delete.js +0 -25
- package/libx/odata/error.js +0 -12
- package/libx/odata/update.js +0 -110
- /package/libx/odata/{grammar.peggy → parse/grammar.peggy} +0 -0
- /package/libx/odata/{parser.js → parse/parser.js} +0 -0
- /package/libx/odata/{result.js → utils/result.js} +0 -0
package/lib/dbs/cds-deploy.js
CHANGED
|
@@ -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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
159
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
package/lib/env/defaults.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/lib/linked/types.js
CHANGED
package/lib/log/format/json.js
CHANGED
|
@@ -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
|
|
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)
|
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
|
-
|
|
41
|
-
supported in future releases.
|
|
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/Query.js
CHANGED
|
@@ -34,7 +34,7 @@ class Query {
|
|
|
34
34
|
const srv = this._srv || cds.db || cds.error `Can't execute query as no primary database is connected.`
|
|
35
35
|
const q = new AsyncResource('await cds.query')
|
|
36
36
|
// Temporary solution for cds.stream in .then. Remove with the next major release.
|
|
37
|
-
return (r,e) => q.runInAsyncScope (srv.run, srv, this) .then(rt => { rt = this._stream && rt ? Object.values(rt)[0] : rt; r(rt) }, e)
|
|
37
|
+
return (r,e) => q.runInAsyncScope (srv.run, srv, this) .then(rt => { rt = this._stream && rt ? Object.values(rt)[0] : rt; return r(rt) }, e)
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
_target4 (...args) {
|
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
|
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
|
},
|