@sap/cds 9.0.4 → 9.2.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 +68 -0
- package/bin/deploy.js +29 -0
- package/bin/serve.js +1 -5
- package/lib/compile/etc/csv.js +11 -6
- package/lib/compile/for/lean_drafts.js +29 -7
- package/lib/compile/load.js +8 -5
- package/lib/compile/to/hdbtabledata.js +1 -1
- package/lib/dbs/cds-deploy.js +5 -34
- package/lib/env/cds-env.js +2 -1
- package/lib/env/cds-requires.js +4 -1
- package/lib/env/defaults.js +0 -11
- package/lib/env/schemas/cds-rc.js +218 -6
- package/lib/index.js +38 -38
- package/lib/log/cds-error.js +12 -11
- package/lib/log/format/json.js +1 -1
- package/lib/ql/SELECT.js +31 -0
- package/lib/ql/resolve.js +1 -1
- package/lib/req/context.js +1 -1
- package/lib/req/request.js +1 -1
- package/lib/req/validate.js +17 -19
- package/lib/srv/cds.Service.js +18 -28
- package/lib/srv/middlewares/auth/ias-auth.js +29 -2
- package/lib/srv/middlewares/auth/jwt-auth.js +11 -1
- package/lib/srv/middlewares/auth/xssec.js +1 -1
- package/lib/srv/srv-models.js +1 -1
- package/lib/srv/srv-tx.js +2 -2
- package/lib/utils/cds-utils.js +35 -2
- package/lib/utils/csv-reader.js +1 -1
- package/lib/utils/inflect.js +2 -2
- package/lib/utils/tar.js +60 -23
- package/lib/utils/version.js +18 -0
- package/libx/_runtime/cds.js +1 -1
- package/libx/_runtime/common/aspects/any.js +1 -23
- package/libx/_runtime/common/generic/crud.js +1 -3
- package/libx/_runtime/common/generic/input.js +113 -52
- package/libx/_runtime/common/generic/sorting.js +1 -1
- package/libx/_runtime/common/generic/temporal.js +0 -6
- package/libx/_runtime/common/utils/draft.js +1 -1
- package/libx/_runtime/common/utils/entityFromCqn.js +1 -1
- package/libx/_runtime/common/utils/propagateForeignKeys.js +1 -1
- package/libx/_runtime/common/utils/resolveView.js +2 -2
- package/libx/_runtime/common/utils/structured.js +2 -2
- package/libx/_runtime/common/utils/templateProcessor.js +0 -5
- package/libx/_runtime/common/utils/vcap.js +1 -1
- package/libx/_runtime/fiori/lean-draft.js +529 -143
- package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +3 -2
- package/libx/_runtime/messaging/service.js +1 -1
- package/libx/_runtime/remote/utils/client.js +2 -1
- package/libx/common/assert/utils.js +2 -12
- package/libx/common/utils/streaming.js +4 -9
- package/libx/http/location.js +1 -0
- package/libx/odata/ODataAdapter.js +47 -43
- package/libx/odata/index.js +1 -1
- package/libx/odata/middleware/batch.js +6 -2
- package/libx/odata/middleware/create.js +1 -1
- package/libx/odata/middleware/error.js +27 -17
- package/libx/odata/middleware/operation.js +15 -21
- package/libx/odata/middleware/stream.js +1 -1
- package/libx/odata/parse/afterburner.js +22 -8
- package/libx/odata/parse/cqn2odata.js +16 -10
- package/libx/odata/parse/grammar.peggy +185 -134
- package/libx/odata/parse/parser.js +1 -1
- package/libx/odata/utils/index.js +1 -36
- package/libx/odata/utils/metadata.js +34 -1
- package/libx/odata/utils/odataBind.js +2 -1
- package/libx/odata/utils/result.js +22 -20
- package/libx/queue/index.js +7 -4
- package/libx/rest/RestAdapter.js +1 -2
- package/libx/rest/middleware/create.js +5 -2
- package/package.json +2 -2
- package/server.js +1 -1
- package/bin/deploy/to-hana.js +0 -1
- package/lib/utils/check-version.js +0 -9
- package/lib/utils/unit.js +0 -19
- package/libx/_runtime/cds-services/util/assert.js +0 -181
- package/libx/_runtime/types/api.js +0 -129
- package/libx/common/assert/validation.js +0 -109
package/libx/queue/index.js
CHANGED
|
@@ -49,6 +49,8 @@ const _safeJSONParse = string => {
|
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
const _targetName = (name, opts) => (opts.targetPrefix ? opts.targetPrefix + name : name)
|
|
53
|
+
|
|
52
54
|
// Note: This function can also run for each tenant on startup
|
|
53
55
|
//
|
|
54
56
|
// tx1: Fetch messages which are not in process and are not locked (SELECT FOR UPDATE)
|
|
@@ -85,7 +87,7 @@ const processTasks = (service, tenant, _opts = {}) => {
|
|
|
85
87
|
const spawn = _begin(async () => {
|
|
86
88
|
let selectedTasks
|
|
87
89
|
const currTime = Date.now()
|
|
88
|
-
const _timeout = cds.utils.
|
|
90
|
+
const _timeout = cds.utils.ms4(opts.timeout)
|
|
89
91
|
|
|
90
92
|
let currMinWaitingTime
|
|
91
93
|
|
|
@@ -95,7 +97,7 @@ const processTasks = (service, tenant, _opts = {}) => {
|
|
|
95
97
|
}
|
|
96
98
|
|
|
97
99
|
const tasksQuery = SELECT.from(tasksEntity)
|
|
98
|
-
.where({ target: name })
|
|
100
|
+
.where({ target: _targetName(name, opts) })
|
|
99
101
|
.orderBy(opts.parallel ? ['status', 'timestamp', 'ID'] : ['timestamp', 'status', 'ID'])
|
|
100
102
|
.limit(opts.chunkSize)
|
|
101
103
|
.forUpdate()
|
|
@@ -232,7 +234,7 @@ const processTasks = (service, tenant, _opts = {}) => {
|
|
|
232
234
|
toBeUpdated.push(task)
|
|
233
235
|
} else toBeDeleted.push(task)
|
|
234
236
|
} else {
|
|
235
|
-
LOG.error(`${service.name}: Emit failed:`, e)
|
|
237
|
+
cds.repl || LOG.error(`${service.name}: Emit failed:`, e)
|
|
236
238
|
task.updateData = { attempts: task.attempts + 1 }
|
|
237
239
|
toBeUpdated.push(task)
|
|
238
240
|
return false
|
|
@@ -294,6 +296,7 @@ const processTasks = (service, tenant, _opts = {}) => {
|
|
|
294
296
|
const _newMsgFrom = msg => {
|
|
295
297
|
const _fromSend = msg instanceof cds.Request
|
|
296
298
|
const newMsg = { ...msg }
|
|
299
|
+
if (msg.entity) newMsg.entity = msg.entity // needed for proper `h.for(msg)` handling
|
|
297
300
|
newMsg._fromSend = _fromSend
|
|
298
301
|
if (!newMsg.queue) return newMsg
|
|
299
302
|
if (!newMsg.queue.after && !newMsg.queue.every) return newMsg
|
|
@@ -428,7 +431,7 @@ const _createTask = (name, msg, context, taskOpts) => {
|
|
|
428
431
|
}
|
|
429
432
|
const taskMsg = {
|
|
430
433
|
ID: cds.utils.uuid(),
|
|
431
|
-
target: name,
|
|
434
|
+
target: _targetName(name, taskOpts),
|
|
432
435
|
timestamp: _get100NanosecondTimestampISOString(msg.queue?.after), // needs to be different for each emit
|
|
433
436
|
msg: JSON.stringify(_msg)
|
|
434
437
|
}
|
package/libx/rest/RestAdapter.js
CHANGED
|
@@ -72,7 +72,7 @@ module.exports = class RestAdapter extends HttpAdapter {
|
|
|
72
72
|
// if authentication or something else within the processing of a cds.Request terminates the request, no need to continue
|
|
73
73
|
if (res.headersSent) return next()
|
|
74
74
|
|
|
75
|
-
const { result, status
|
|
75
|
+
const { result, status } = outcome
|
|
76
76
|
|
|
77
77
|
// post process
|
|
78
78
|
if (result) {
|
|
@@ -83,7 +83,6 @@ module.exports = class RestAdapter extends HttpAdapter {
|
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
if (status && res.statusCode === 200) res.status(status) //> only set status if not yet modified
|
|
86
|
-
if (location && !res.get('location')) res.set('location', location)
|
|
87
86
|
|
|
88
87
|
if (req.method === 'HEAD') res.type('json').set('content-length', JSON.stringify(result).length).end()
|
|
89
88
|
else res.send(typeof result === 'number' ? result.toString() : result)
|
|
@@ -25,10 +25,13 @@ exports.create = async function (req, res) {
|
|
|
25
25
|
// > single insert
|
|
26
26
|
const cdsReq = this.request4({ query, params, req, res })
|
|
27
27
|
result = await this.service.dispatch(cdsReq)
|
|
28
|
-
|
|
28
|
+
if (!res.hasHeader('location')) {
|
|
29
|
+
location = location4(cdsReq.target, this.service, result, true)
|
|
30
|
+
if (location) res.set('location', location)
|
|
31
|
+
}
|
|
29
32
|
}
|
|
30
33
|
|
|
31
|
-
return { result, status: 201
|
|
34
|
+
return { result, status: 201 }
|
|
32
35
|
}
|
|
33
36
|
|
|
34
37
|
const cds = require('../../_runtime/cds')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sap/cds",
|
|
3
|
-
"version": "9.0
|
|
3
|
+
"version": "9.2.0",
|
|
4
4
|
"description": "SAP Cloud Application Programming Model - CDS for Node.js",
|
|
5
5
|
"homepage": "https://cap.cloud.sap/",
|
|
6
6
|
"keywords": [
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"license": "SEE LICENSE IN LICENSE",
|
|
12
12
|
"main": "lib/index.js",
|
|
13
13
|
"bin": {
|
|
14
|
-
"cds-deploy": "
|
|
14
|
+
"cds-deploy": "bin/deploy.js",
|
|
15
15
|
"cds-serve": "bin/serve.js"
|
|
16
16
|
},
|
|
17
17
|
"files": [
|
package/server.js
CHANGED
package/bin/deploy/to-hana.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
throw new Error('This application uses @sap/cds version >= 7, which is not compatible with the installed @sap/cds-dk version 6. Either update @sap/cds-dk to version 7 or downgrade @sap/cds to version 6 instead.')
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
let version = /(\d+)(?:\.(\d+).*)?/ .exec (require('../../package.json').engines.node)
|
|
2
|
-
let given = /(\d+)(?:\.(\d+).*)?/ .exec (process.version)
|
|
3
|
-
if (+given[1] < +version[1] || given[1] == version[1] && +given[2] < +version[2]) {
|
|
4
|
-
process.stderr.write (`
|
|
5
|
-
Node.js version ${version[0]} or higher is required for @sap/cds.
|
|
6
|
-
Current version ${given[0]} does not satisfy this.
|
|
7
|
-
\n`)
|
|
8
|
-
process.exit(1)
|
|
9
|
-
}
|
package/lib/utils/unit.js
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
module.exports.time2ms = timeout => {
|
|
2
|
-
const match = timeout.match(/^([0-9]+)(w|d|h|hrs|min)$/)
|
|
3
|
-
if (!match) return
|
|
4
|
-
|
|
5
|
-
const [, val, t] = match
|
|
6
|
-
switch (t) {
|
|
7
|
-
case 'w':
|
|
8
|
-
return val * 1000 * 3600 * 24 * 7
|
|
9
|
-
case 'd':
|
|
10
|
-
return val * 1000 * 3600 * 24
|
|
11
|
-
case 'h':
|
|
12
|
-
case 'hrs':
|
|
13
|
-
return val * 1000 * 3600
|
|
14
|
-
case 'min':
|
|
15
|
-
return val * 1000 * 60
|
|
16
|
-
default:
|
|
17
|
-
return val
|
|
18
|
-
}
|
|
19
|
-
}
|
|
@@ -1,181 +0,0 @@
|
|
|
1
|
-
const cds = require('../../cds')
|
|
2
|
-
const LOG = cds.log('app')
|
|
3
|
-
const templatePathSerializer = require('../../common/utils/templateProcessorPathSerializer')
|
|
4
|
-
|
|
5
|
-
const typeCheckers = require('../../../common/assert/type-strict')
|
|
6
|
-
const { 'cds.Decimal': checkDecimal } = typeCheckers
|
|
7
|
-
const { checkMandatory, checkEnum, checkRange, checkFormat } = require('../../../common/assert/validation')
|
|
8
|
-
|
|
9
|
-
const ASSERT_RANGE = 'ASSERT_RANGE'
|
|
10
|
-
const ASSERT_FORMAT = 'ASSERT_FORMAT'
|
|
11
|
-
const ASSERT_DATA_TYPE = 'ASSERT_DATA_TYPE'
|
|
12
|
-
const ASSERT_ENUM = 'ASSERT_ENUM'
|
|
13
|
-
const ASSERT_NOT_NULL = 'ASSERT_NOT_NULL'
|
|
14
|
-
|
|
15
|
-
const _enumValues = element => {
|
|
16
|
-
return Object.keys(element).map(enumKey => {
|
|
17
|
-
const enum_ = element[enumKey]
|
|
18
|
-
const enumValue = enum_ && enum_.val
|
|
19
|
-
|
|
20
|
-
if (enumValue !== undefined) {
|
|
21
|
-
if (enumValue['=']) return enumValue['=']
|
|
22
|
-
if (enum_ && enum_.literal && enum_.literal === 'number') return Number(enumValue)
|
|
23
|
-
return enumValue
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
return enumKey
|
|
27
|
-
})
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// REVISIT: this needs a cleanup!
|
|
31
|
-
const assertError = (code, element, value, key, path) => {
|
|
32
|
-
let args
|
|
33
|
-
|
|
34
|
-
if (typeof code === 'object') {
|
|
35
|
-
args = code.args
|
|
36
|
-
code = code.code
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const { name, type, precision, scale } = element
|
|
40
|
-
const error = new Error()
|
|
41
|
-
const errorEntry = {
|
|
42
|
-
code,
|
|
43
|
-
message: code,
|
|
44
|
-
target: path ?? element.name ?? key,
|
|
45
|
-
args: args ?? [name ?? key]
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const assertError = Object.assign(error, errorEntry)
|
|
49
|
-
Object.assign(assertError, {
|
|
50
|
-
entity: element.parent && element.parent.name,
|
|
51
|
-
element: name, // > REVISIT: when is error.element needed?
|
|
52
|
-
type: element.items ? element.items._type : type,
|
|
53
|
-
status: 400,
|
|
54
|
-
value
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
if (element.enum) assertError.enum = _enumValues(element)
|
|
58
|
-
if (precision) assertError.precision = precision
|
|
59
|
-
if (scale) assertError.scale = scale
|
|
60
|
-
|
|
61
|
-
if (element.target) {
|
|
62
|
-
// REVISIT: when does this case apply?
|
|
63
|
-
assertError.target = element.target
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
return assertError
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* @param {import('../../types/api').InputConstraints} constraints
|
|
71
|
-
*/
|
|
72
|
-
const checkInputConstraints = ({ element, value, errors, key, pathSegmentsInfo }) => {
|
|
73
|
-
if (!element) return errors
|
|
74
|
-
|
|
75
|
-
let path
|
|
76
|
-
if (pathSegmentsInfo?.length) path = templatePathSerializer(element.name || key, pathSegmentsInfo)
|
|
77
|
-
|
|
78
|
-
// not nice, but best option for keeping new cds.assert() clean
|
|
79
|
-
if (element._isMandatory) {
|
|
80
|
-
const mandatoryErrors = []
|
|
81
|
-
checkMandatory(value, element, mandatoryErrors, [], key)
|
|
82
|
-
if (mandatoryErrors.length) {
|
|
83
|
-
errors.push(...mandatoryErrors.map(() => assertError({ code: ASSERT_NOT_NULL }, element, value, key, path)))
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
if (value == null) return errors
|
|
88
|
-
|
|
89
|
-
// not nice, but best option for keeping new cds.assert() clean
|
|
90
|
-
const enumErrors = []
|
|
91
|
-
checkEnum(value, element, enumErrors, [], key)
|
|
92
|
-
if (enumErrors.length) {
|
|
93
|
-
errors.push(...enumErrors.map(e => assertError({ code: ASSERT_ENUM, args: e.args }, element, value, key, path)))
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// not nice, but best option for keeping new cds.assert() clean
|
|
97
|
-
const rangeErrors = []
|
|
98
|
-
checkRange(value, element, rangeErrors, [], key)
|
|
99
|
-
if (rangeErrors.length) {
|
|
100
|
-
errors.push(...rangeErrors.map(e => assertError({ code: ASSERT_RANGE, args: e.args }, element, value, key, path)))
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// not nice, but best option for keeping new cds.assert() clean
|
|
104
|
-
const formatErrors = []
|
|
105
|
-
checkFormat(value, element, formatErrors, [], key)
|
|
106
|
-
if (formatErrors.length) {
|
|
107
|
-
errors.push(...formatErrors.map(e => assertError({ code: ASSERT_FORMAT, args: e.args }, element, value, key, path)))
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
if (element.type === 'cds.Decimal') {
|
|
111
|
-
// not nice, but best option for keeping new cds.assert() clean
|
|
112
|
-
const decimalErrors = []
|
|
113
|
-
checkDecimal(value, element, decimalErrors, [], key)
|
|
114
|
-
if (decimalErrors.length) {
|
|
115
|
-
errors.push(
|
|
116
|
-
...decimalErrors.map(e => assertError({ code: ASSERT_DATA_TYPE, args: e.args }, element, value, key, path))
|
|
117
|
-
)
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
return errors
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Check whether the target entity referenced by the association (the reference's target) exists and assert an error if
|
|
126
|
-
* the the reference's target doesn't exist.
|
|
127
|
-
*
|
|
128
|
-
* In other words, use this annotation to check whether a non-null foreign key input in a table has a corresponding
|
|
129
|
-
* primary key (also known as a parent key) in the associated/referenced target table (also known as a parent table).
|
|
130
|
-
*
|
|
131
|
-
* @param {import('../../types/api').assertTargetMap} assertMap
|
|
132
|
-
* @param {array} errors An array to appends the possible errors.
|
|
133
|
-
* @see {@link https://cap.cloud.sap/docs/guides/providing-services#assert-target @assert.target} for
|
|
134
|
-
* further information.
|
|
135
|
-
*/
|
|
136
|
-
const assertTargets = async (assertMap, errors) => {
|
|
137
|
-
const { targets: targetsMap, allTargets } = assertMap
|
|
138
|
-
if (targetsMap.size === 0) return
|
|
139
|
-
|
|
140
|
-
const targets = Array.from(targetsMap.values())
|
|
141
|
-
const transactions = targets.map(({ keys, entity }) => {
|
|
142
|
-
const where = Object.assign({}, ...keys)
|
|
143
|
-
return cds.db.exists(entity, where).forShareLock()
|
|
144
|
-
})
|
|
145
|
-
const targetsExistsResults = await Promise.allSettled(transactions)
|
|
146
|
-
|
|
147
|
-
targetsExistsResults.forEach((txPromise, index) => {
|
|
148
|
-
const isPromiseRejected = txPromise.status === 'rejected'
|
|
149
|
-
const shouldAssertError = (txPromise.status === 'fulfilled' && txPromise.value == null) || isPromiseRejected
|
|
150
|
-
if (!shouldAssertError) return
|
|
151
|
-
|
|
152
|
-
const target = targets[index]
|
|
153
|
-
const { element } = target.assocInfo
|
|
154
|
-
|
|
155
|
-
if (isPromiseRejected) {
|
|
156
|
-
LOG._debug &&
|
|
157
|
-
LOG.debug(
|
|
158
|
-
`The transaction to check the @assert.target constraint for foreign key "${element.name}" failed`,
|
|
159
|
-
txPromise.reason
|
|
160
|
-
)
|
|
161
|
-
|
|
162
|
-
throw new Error(txPromise.reason.message)
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
allTargets
|
|
166
|
-
.filter(t => t.key === target.key)
|
|
167
|
-
.forEach(target => {
|
|
168
|
-
const { row, pathSegmentsInfo } = target.assocInfo
|
|
169
|
-
const key = target.foreignKey.name
|
|
170
|
-
let path
|
|
171
|
-
if (pathSegmentsInfo?.length) path = templatePathSerializer(key, pathSegmentsInfo)
|
|
172
|
-
const error = assertError('ASSERT_TARGET', target.foreignKey, row[key], key, path)
|
|
173
|
-
errors.push(error)
|
|
174
|
-
})
|
|
175
|
-
})
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
module.exports = {
|
|
179
|
-
checkInputConstraints,
|
|
180
|
-
assertTargets
|
|
181
|
-
}
|
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
// Columns
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* @typedef {object} ColumnRef
|
|
5
|
-
* @property {string[]} ref
|
|
6
|
-
* @property {function} func
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* @typedef {Array<ColumnRef>} ColumnRefs
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
// Input constraints
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* @typedef {object} InputConstraints
|
|
17
|
-
* @property {object} element
|
|
18
|
-
* @property {*} value
|
|
19
|
-
* @property {Array} errors
|
|
20
|
-
* @property {string} [key]
|
|
21
|
-
* @property {pathSegmentInfo[]} [pathSegmentsInfo]
|
|
22
|
-
* @property {string} event
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
// ON condition
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* @typedef {object} ONConditionAliases
|
|
29
|
-
* @property {string} select
|
|
30
|
-
* @property {string} join
|
|
31
|
-
*/
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* @typedef {object} ONConditionOptions
|
|
35
|
-
* @property {string | Array} [associationNames]
|
|
36
|
-
* @property {object} [csn]
|
|
37
|
-
* @property {ONConditionAliases} [aliases]
|
|
38
|
-
* @property {boolean} [resolveView=true]
|
|
39
|
-
*/
|
|
40
|
-
|
|
41
|
-
// Template processor
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* @typedef {object} TemplateProcessorInfo
|
|
45
|
-
* @property {entity} target
|
|
46
|
-
* @property {Map} elements
|
|
47
|
-
*/
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* @typedef {object} TemplateProcessorPathOptions
|
|
51
|
-
* @property {object} [draftKeys]
|
|
52
|
-
* @property {function} [rowUUIDGenerator]
|
|
53
|
-
* @property {string[]} [segments=[]] - Path segments to relate the error message.
|
|
54
|
-
* @property {boolean} [includeKeyValues=false] Indicates whether the key values are included in the path segments
|
|
55
|
-
* The path segments are used to build the error target (a relative resource path)
|
|
56
|
-
*/
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* @typedef {object} TemplateProcessor
|
|
60
|
-
* @property {Function} processFn
|
|
61
|
-
* @property {object} data
|
|
62
|
-
* @property {TemplateProcessorInfo} template
|
|
63
|
-
* @property {boolean} [isRoot=true]
|
|
64
|
-
* @property {TemplateProcessorPathOptions} [pathOptions=null]
|
|
65
|
-
*/
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* @typedef {object} pathSegmentInfo
|
|
69
|
-
* @property {string} key
|
|
70
|
-
* @property {string[]} keyNames
|
|
71
|
-
* @property {object} row
|
|
72
|
-
* @property {object} elements
|
|
73
|
-
* @property {string[]} draftKeys
|
|
74
|
-
*/
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* @typedef {object} templateElementInfo
|
|
78
|
-
* @property {object} row
|
|
79
|
-
* @property {string} key
|
|
80
|
-
* @property {object} element
|
|
81
|
-
* @property {boolean} plain
|
|
82
|
-
* @property {entity} target
|
|
83
|
-
* @property {boolean} isRoot
|
|
84
|
-
* @property {string[] | Array<pathSegmentInfo>} [pathSegmentsInfo]
|
|
85
|
-
*/
|
|
86
|
-
|
|
87
|
-
// Search
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* @typedef {object} searchContainsArg
|
|
91
|
-
* @property {ColumnRefs} [list] The columns to
|
|
92
|
-
* be searched
|
|
93
|
-
* @property {string} [val] The search string
|
|
94
|
-
*/
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* @typedef {Array<searchContainsArg>} searchContainsArgs
|
|
98
|
-
*/
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* @typedef {object} searchContainsExp
|
|
102
|
-
* @property {string} func='contains' The function name
|
|
103
|
-
* @property {searchContainsArgs} args
|
|
104
|
-
*/
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* @typedef {object} search2cqnOptions
|
|
108
|
-
* @property {ColumnRefs} [columns] The columns to be searched
|
|
109
|
-
* @property {string} locale The user locale
|
|
110
|
-
*/
|
|
111
|
-
|
|
112
|
-
// Assert targets map
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* @typedef {object} assertTargetMap
|
|
116
|
-
* @property {Map<string, targetMaps>} targets
|
|
117
|
-
* @property {targetMaps[]} allTargets
|
|
118
|
-
*/
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* @typedef {object} targetMaps
|
|
122
|
-
* @property {string} key
|
|
123
|
-
* @property {entity} entity
|
|
124
|
-
* @property {object} keys
|
|
125
|
-
* @property {object} foreignKey
|
|
126
|
-
* @property {templateElementInfo} assocInfo
|
|
127
|
-
*/
|
|
128
|
-
|
|
129
|
-
module.exports = {}
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
const { cds } = global
|
|
2
|
-
|
|
3
|
-
const {
|
|
4
|
-
'cds.Date': checkISODate,
|
|
5
|
-
'cds.Time': checkISOTime,
|
|
6
|
-
'cds.DateTime': checkISODateTime,
|
|
7
|
-
'cds.Timestamp': checkISOTimestamp,
|
|
8
|
-
'cds.String': checkString
|
|
9
|
-
} = require('./type-strict')
|
|
10
|
-
const { getTarget, resolveCDSType } = require('./utils')
|
|
11
|
-
|
|
12
|
-
const _isNavigationColumn = (col, as) => col.ref?.length > 1 && (col.as === as || col.ref[col.ref.length - 1] === as)
|
|
13
|
-
|
|
14
|
-
// REVISIT: mandatory is actually not the same as not null or empty string
|
|
15
|
-
const _isNotFilled = val => val === null || val === undefined || (typeof val === 'string' && val.trim() === '')
|
|
16
|
-
|
|
17
|
-
const _getEnumElement = ele => ((ele['@assert.range'] && ele.enum) ? ele.enum : undefined)
|
|
18
|
-
|
|
19
|
-
const _enumValues = ele => {
|
|
20
|
-
return Object.keys(ele).map(enumKey => {
|
|
21
|
-
const enum_ = ele[enumKey]
|
|
22
|
-
const enumValue = enum_ && enum_.val
|
|
23
|
-
if (enumValue !== undefined) {
|
|
24
|
-
if (enumValue['=']) return enumValue['=']
|
|
25
|
-
if (enum_ && enum_.literal && enum_.literal === 'number') return Number(enumValue)
|
|
26
|
-
return enumValue
|
|
27
|
-
}
|
|
28
|
-
return enumKey
|
|
29
|
-
})
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const _checkDateValue = (val, r1, r2) => {
|
|
33
|
-
const dateVal = new Date(val)
|
|
34
|
-
return (dateVal - new Date(r1)) * (dateVal - new Date(r2)) <= 0
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const _toDate = val => `2000-01-01T${val}Z`
|
|
38
|
-
|
|
39
|
-
const _checkInRange = (val, range, type) => {
|
|
40
|
-
switch (type) {
|
|
41
|
-
case 'cds.Date':
|
|
42
|
-
return checkISODate(val) && _checkDateValue(val, range[0], range[1])
|
|
43
|
-
case 'cds.DateTime':
|
|
44
|
-
return checkISODateTime(val) && _checkDateValue(val, range[0], range[1])
|
|
45
|
-
case 'cds.Timestamp':
|
|
46
|
-
return checkISOTimestamp(val) && _checkDateValue(val, range[0], range[1])
|
|
47
|
-
case 'cds.Time':
|
|
48
|
-
return checkISOTime(val) && _checkDateValue(_toDate(val), _toDate(range[0]), _toDate(range[1]))
|
|
49
|
-
default:
|
|
50
|
-
return (val - range[0]) * (val - range[1]) <= 0
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// process.env.CDS_ASSERT_FORMAT_FLAGS is not official!
|
|
55
|
-
const _checkRegExpFormat = (val, format) =>
|
|
56
|
-
checkString(val) && val.match(new RegExp(format, process.env.CDS_ASSERT_FORMAT_FLAGS || 'u'))
|
|
57
|
-
|
|
58
|
-
const checkMandatory = (v, ele, errs, path, k) => {
|
|
59
|
-
// REVISIT: correct to not complain?
|
|
60
|
-
// do not complain about missing foreign keys in children
|
|
61
|
-
if (path.length && ele['@odata.foreignKey4']) return
|
|
62
|
-
|
|
63
|
-
// TODO: which case is this?
|
|
64
|
-
// do not complain about ???
|
|
65
|
-
if (ele.parent?.query?.SELECT?.columns?.find(col => _isNavigationColumn(col, ele.name))) return
|
|
66
|
-
|
|
67
|
-
if (_isNotFilled(v)) {
|
|
68
|
-
const target = getTarget(path, k)
|
|
69
|
-
errs.push(new cds.error('ASSERT_NOT_NULL', { target, statusCode: 400, code: '400' }))
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const checkEnum = (v, ele, errs, path, k) => {
|
|
74
|
-
const enumElements = _getEnumElement(ele)
|
|
75
|
-
const enumValues = enumElements && _enumValues(enumElements)
|
|
76
|
-
if (enumElements && !enumValues.some(ev => ev == v)) { //> use == for automatic type coercion
|
|
77
|
-
const args =
|
|
78
|
-
typeof v === 'string'
|
|
79
|
-
? ['"' + v + '"', enumValues.map(ele => '"' + ele + '"').join(', ')]
|
|
80
|
-
: [v, enumValues.join(', ')]
|
|
81
|
-
const target = getTarget(path, k)
|
|
82
|
-
errs.push(new cds.error('ASSERT_ENUM', { args, target, statusCode: 400, code: '400' }))
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const checkRange = (v, ele, errs, path, k) => {
|
|
87
|
-
const rangeElements = ele['@assert.range'] && !_getEnumElement(ele) ? ele['@assert.range'] : undefined
|
|
88
|
-
if (rangeElements && !_checkInRange(v, rangeElements, resolveCDSType(ele))) {
|
|
89
|
-
const args = [v, ...ele['@assert.range']]
|
|
90
|
-
const target = getTarget(path, k)
|
|
91
|
-
errs.push(new cds.error('ASSERT_RANGE', { args, target, statusCode: 400, code: '400' }))
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const checkFormat = (v, ele, errs, path, k) => {
|
|
96
|
-
const formatElements = ele['@assert.format']
|
|
97
|
-
if (formatElements && !_checkRegExpFormat(v, formatElements)) {
|
|
98
|
-
const args = [v, formatElements]
|
|
99
|
-
const target = getTarget(path, k)
|
|
100
|
-
errs.push(new cds.error('ASSERT_FORMAT', { args, target, statusCode: 400, code: '400' }))
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
module.exports = {
|
|
105
|
-
checkMandatory,
|
|
106
|
-
checkEnum,
|
|
107
|
-
checkRange,
|
|
108
|
-
checkFormat
|
|
109
|
-
}
|