@sap/cds 9.2.1 → 9.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +85 -1
- package/_i18n/i18n_es.properties +3 -3
- package/_i18n/i18n_es_MX.properties +3 -3
- package/_i18n/i18n_fr.properties +2 -2
- package/_i18n/messages.properties +6 -0
- package/app/index.js +0 -1
- package/bin/deploy.js +1 -1
- package/bin/serve.js +7 -20
- package/lib/compile/cdsc.js +3 -0
- package/lib/compile/for/flows.js +102 -0
- package/lib/compile/for/nodejs.js +28 -0
- package/lib/compile/to/edm.js +11 -4
- package/lib/core/classes.js +1 -1
- package/lib/core/linked-csn.js +8 -0
- package/lib/dbs/cds-deploy.js +12 -12
- package/lib/env/cds-env.js +1 -1
- package/lib/env/cds-requires.js +21 -20
- package/lib/env/defaults.js +2 -1
- package/lib/index.js +5 -6
- package/lib/log/cds-log.js +6 -5
- package/lib/log/format/aspects/cf.js +2 -2
- package/lib/plugins.js +1 -1
- package/lib/ql/cds-ql.js +0 -3
- package/lib/req/request.js +3 -3
- package/lib/req/response.js +12 -7
- package/lib/srv/bindings.js +17 -17
- package/lib/srv/cds-connect.js +6 -9
- package/lib/srv/cds-serve.js +74 -137
- package/lib/srv/cds.Service.js +49 -0
- package/lib/srv/factory.js +4 -4
- package/lib/srv/middlewares/auth/ias-auth.js +33 -9
- package/lib/srv/middlewares/auth/index.js +3 -2
- package/lib/srv/middlewares/auth/jwt-auth.js +20 -6
- package/lib/srv/protocols/hcql.js +16 -1
- package/lib/srv/srv-dispatch.js +1 -1
- package/lib/utils/cds-utils.js +4 -8
- package/lib/utils/csv-reader.js +27 -7
- package/libx/_runtime/cds.js +0 -6
- package/libx/_runtime/common/Service.js +5 -0
- package/libx/_runtime/common/generic/crud.js +1 -1
- package/libx/_runtime/common/generic/flows.js +106 -0
- package/libx/_runtime/common/generic/paging.js +3 -3
- package/libx/_runtime/common/utils/differ.js +5 -15
- package/libx/_runtime/common/utils/resolveView.js +2 -2
- package/libx/_runtime/common/utils/rewriteAsterisks.js +10 -4
- package/libx/_runtime/fiori/lean-draft.js +76 -40
- package/libx/_runtime/messaging/enterprise-messaging.js +1 -1
- package/libx/_runtime/messaging/service.js +7 -0
- package/libx/_runtime/remote/Service.js +68 -62
- package/libx/_runtime/remote/utils/client.js +29 -216
- package/libx/_runtime/remote/utils/query.js +197 -0
- package/libx/_runtime/ucl/Service.js +180 -112
- package/libx/_runtime/ucl/queries.js +61 -0
- package/libx/odata/ODataAdapter.js +1 -4
- package/libx/odata/index.js +2 -10
- package/libx/odata/middleware/error.js +8 -1
- package/libx/odata/middleware/stream.js +1 -1
- package/libx/odata/middleware/update.js +12 -2
- package/libx/odata/parse/afterburner.js +113 -20
- package/libx/odata/parse/cqn2odata.js +1 -3
- package/libx/rest/middleware/parse.js +9 -2
- package/package.json +2 -2
- package/server.js +2 -0
- package/srv/app-service.js +1 -0
- package/srv/db-service.js +1 -0
- package/srv/msg-service.js +1 -0
- package/srv/remote-service.js +1 -0
- package/srv/ucl-service.cds +32 -0
- package/srv/ucl-service.js +1 -0
- package/lib/ql/resolve.js +0 -45
- package/libx/common/assert/type-strict.js +0 -109
- package/libx/common/assert/utils.js +0 -60
|
@@ -8,6 +8,9 @@ const normalizeTimestamp = require('../../_runtime/common/utils/normalizeTimesta
|
|
|
8
8
|
const { rewriteExpandAsterisk } = require('../../_runtime/common/utils/rewriteAsterisks')
|
|
9
9
|
const resolveStructured = require('../../_runtime/common/utils/resolveStructured')
|
|
10
10
|
|
|
11
|
+
// Same regex as peggy parser
|
|
12
|
+
const RELAXED_UUID_REGEX = /^[0-9a-z]{8}-?[0-9a-z]{4}-?[0-9a-z]{4}-?[0-9a-z]{4}-?[0-9a-z]{12}$/i
|
|
13
|
+
|
|
11
14
|
function _getDefinition(definition, name, namespace) {
|
|
12
15
|
return (
|
|
13
16
|
definition?.definitions?.[name] ||
|
|
@@ -185,6 +188,13 @@ function _convertVal(value, element) {
|
|
|
185
188
|
case 'cds.Timestamp':
|
|
186
189
|
return normalizeTimestamp(value)
|
|
187
190
|
|
|
191
|
+
case 'cds.UUID':
|
|
192
|
+
if (!RELAXED_UUID_REGEX.test(value)) {
|
|
193
|
+
const msg = `Element "${element.name}" does not contain a valid UUID`
|
|
194
|
+
throw Object.assign(new Error(msg), { statusCode: 400 })
|
|
195
|
+
}
|
|
196
|
+
return value
|
|
197
|
+
|
|
188
198
|
default:
|
|
189
199
|
return value
|
|
190
200
|
}
|
|
@@ -528,6 +538,10 @@ function _processSegments(from, model, namespace, cqn, protocol) {
|
|
|
528
538
|
|
|
529
539
|
const AGGR_DFLT = '@Aggregation.default'
|
|
530
540
|
const CSTM_AGGR = '@Aggregation.CustomAggregate'
|
|
541
|
+
const SMTCS_CC = '@Semantics.currencyCode'
|
|
542
|
+
const SMTCS_UOM = '@Semantics.unitOfMeasure'
|
|
543
|
+
const SMTCS_AMT_CC = '@Semantics.amount.currencyCode'
|
|
544
|
+
const SMTCS_AMT_UOM = '@Semantics.amount.unitOfMeasure'
|
|
531
545
|
|
|
532
546
|
function _addKeys(columns, target) {
|
|
533
547
|
let hasAggregatedColumn = false,
|
|
@@ -586,17 +600,20 @@ const _structProperty = (ref, target) => {
|
|
|
586
600
|
}
|
|
587
601
|
|
|
588
602
|
function _processColumns(cqn, target, protocol) {
|
|
603
|
+
// Recursively process columns for nested SELECTs
|
|
589
604
|
if (cqn.SELECT.from.SELECT) _processColumns(cqn.SELECT.from, target)
|
|
590
605
|
|
|
591
606
|
let columns = cqn.SELECT.columns
|
|
592
607
|
|
|
608
|
+
// Error if groupBy is present but no columns are selected
|
|
593
609
|
if (columns && !columns.length && cqn.SELECT.groupBy) {
|
|
594
|
-
cds.error('Explicit select must include at least one column available in the result set of groupby
|
|
610
|
+
throw cds.error('Explicit select must include at least one column available in the result set of groupby', {
|
|
595
611
|
code: '400',
|
|
596
612
|
statusCode: 400
|
|
597
613
|
})
|
|
598
614
|
}
|
|
599
615
|
|
|
616
|
+
// If columns exist and no groupBy, handle asterisk and expand logic
|
|
600
617
|
if (columns && !cqn.SELECT.groupBy) {
|
|
601
618
|
let entity
|
|
602
619
|
if (target.kind === 'entity') entity = target
|
|
@@ -606,32 +623,108 @@ function _processColumns(cqn, target, protocol) {
|
|
|
606
623
|
_removeUnneededColumnsIfHasAsterisk(columns)
|
|
607
624
|
rewriteExpandAsterisk(columns, entity)
|
|
608
625
|
|
|
609
|
-
//
|
|
626
|
+
// For OData, add missing key fields to columns
|
|
610
627
|
if (protocol?.match(/odata/i)) _addKeys(columns, entity)
|
|
611
628
|
}
|
|
612
629
|
|
|
613
630
|
if (!Array.isArray(columns)) return
|
|
614
631
|
|
|
615
|
-
|
|
632
|
+
// Iterate columns to set default aggregation function if needed
|
|
616
633
|
for (let i = 0; i < columns.length; i++) {
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
)
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
+
const processedColumn = columns[i]
|
|
635
|
+
|
|
636
|
+
// Skip if column is not an object or has a ref (not an aggregation)
|
|
637
|
+
if (typeof processedColumn !== 'object' || processedColumn.ref) continue
|
|
638
|
+
|
|
639
|
+
if (!processedColumn.args?.length) continue
|
|
640
|
+
if (!processedColumn.args[0].ref?.length) continue
|
|
641
|
+
const processedColumnRef = processedColumn.args[0].ref
|
|
642
|
+
|
|
643
|
+
// Extract relevant element characteristics
|
|
644
|
+
let aggregatedPropertyName, aggregatedElement
|
|
645
|
+
for (let refIdx = 0; refIdx < processedColumnRef.length; refIdx++) {
|
|
646
|
+
aggregatedPropertyName = processedColumnRef[refIdx]
|
|
647
|
+
if (aggregatedPropertyName.id) aggregatedPropertyName = aggregatedPropertyName.id
|
|
648
|
+
if (aggregatedElement && aggregatedElement.isAssociation) target = aggregatedElement._target
|
|
649
|
+
aggregatedElement = target.elements[aggregatedPropertyName]
|
|
650
|
+
}
|
|
651
|
+
const prefixRef = processedColumnRef.slice(0, processedColumnRef.length - 1)
|
|
652
|
+
const aggregatedElementRef = [...prefixRef, aggregatedPropertyName]
|
|
653
|
+
const isCurrencyCodeOrUnitOfMeasure = !!(aggregatedElement[SMTCS_CC] || aggregatedElement[SMTCS_UOM])
|
|
654
|
+
processedColumn.as = processedColumn.as || aggregatedPropertyName
|
|
655
|
+
|
|
656
|
+
// Specifically handle aggregating semantic amounts
|
|
657
|
+
if (isCurrencyCodeOrUnitOfMeasure) {
|
|
658
|
+
columns[i] = {
|
|
659
|
+
xpr: [
|
|
660
|
+
'case',
|
|
661
|
+
'when',
|
|
662
|
+
{ xpr: [{ func: 'max', args: [{ ref: aggregatedElementRef }] }, '=', { val: null }] },
|
|
663
|
+
'then',
|
|
664
|
+
{ val: '' },
|
|
665
|
+
'else',
|
|
666
|
+
{
|
|
667
|
+
xpr: [
|
|
668
|
+
'case',
|
|
669
|
+
'when',
|
|
670
|
+
{
|
|
671
|
+
xpr: [
|
|
672
|
+
{ func: 'min', args: [{ ref: aggregatedElementRef }] },
|
|
673
|
+
'=',
|
|
674
|
+
{ func: 'max', args: [{ ref: aggregatedElementRef }] }
|
|
675
|
+
]
|
|
676
|
+
},
|
|
677
|
+
'then',
|
|
678
|
+
{ func: 'min', args: [{ ref: aggregatedElementRef }] },
|
|
679
|
+
'else',
|
|
680
|
+
{ val: null },
|
|
681
|
+
'end'
|
|
682
|
+
]
|
|
683
|
+
},
|
|
684
|
+
'end'
|
|
685
|
+
],
|
|
686
|
+
as: processedColumn.as
|
|
634
687
|
}
|
|
688
|
+
|
|
689
|
+
continue
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// Determine default aggregation function if necessary
|
|
693
|
+
const customAggregate = target[`${CSTM_AGGR}#${aggregatedPropertyName}`]
|
|
694
|
+
if (processedColumn.func === null) {
|
|
695
|
+
processedColumn.func = aggregatedElement?.[AGGR_DFLT]?.['#']?.toLowerCase()
|
|
696
|
+
if (!customAggregate)
|
|
697
|
+
throw cds.error(`Result type for custom aggregation of property "${aggregatedPropertyName}" not found`)
|
|
698
|
+
if (!processedColumn.func)
|
|
699
|
+
throw cds.error(`Default aggregation for property "${aggregatedPropertyName}" not found`)
|
|
700
|
+
if (processedColumn.func === 'count_distinct') processedColumn.func = 'countdistinct'
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// Process Semantics Amount - Currency Code / Unit of Measure - if present
|
|
704
|
+
const semanticsAmountElementName = aggregatedElement?.[SMTCS_AMT_CC] ?? aggregatedElement?.[SMTCS_AMT_UOM]
|
|
705
|
+
if (!semanticsAmountElementName) continue
|
|
706
|
+
if (!target.elements[semanticsAmountElementName])
|
|
707
|
+
throw cds.error(`Referenced semantics amount element not found: ${semanticsAmountElementName}`)
|
|
708
|
+
const semanticsAmountElementRef = [...prefixRef, semanticsAmountElementName]
|
|
709
|
+
|
|
710
|
+
columns[i] = {
|
|
711
|
+
xpr: [
|
|
712
|
+
'case',
|
|
713
|
+
'when',
|
|
714
|
+
{
|
|
715
|
+
xpr: [
|
|
716
|
+
{ func: 'min', args: [{ ref: semanticsAmountElementRef }] },
|
|
717
|
+
'=',
|
|
718
|
+
{ func: 'max', args: [{ ref: semanticsAmountElementRef }] }
|
|
719
|
+
]
|
|
720
|
+
},
|
|
721
|
+
'then',
|
|
722
|
+
{ func: processedColumn.func, args: [{ ref: aggregatedElementRef }] },
|
|
723
|
+
'else',
|
|
724
|
+
{ val: null },
|
|
725
|
+
'end'
|
|
726
|
+
],
|
|
727
|
+
as: processedColumn.as
|
|
635
728
|
}
|
|
636
729
|
}
|
|
637
730
|
}
|
|
@@ -598,7 +598,7 @@ const _delete = (cqn, kind, model) => {
|
|
|
598
598
|
return { method: 'DELETE', path: `${url}${keys}` }
|
|
599
599
|
}
|
|
600
600
|
|
|
601
|
-
|
|
601
|
+
module.exports.cqn2odata = (cqn, { kind, model, method }) => {
|
|
602
602
|
if (cqn.SELECT) return _select(cqn, kind, model)
|
|
603
603
|
if (cqn.INSERT) return _insert(cqn, kind, model)
|
|
604
604
|
if (cqn.UPDATE) return _update(cqn, kind, model, method)
|
|
@@ -606,5 +606,3 @@ function cqn2odata(cqn, { kind, model, method }) {
|
|
|
606
606
|
|
|
607
607
|
throw new Error('Unknown CQN object cannot be translated to URL: ' + JSON.stringify(cqn))
|
|
608
608
|
}
|
|
609
|
-
|
|
610
|
-
module.exports = cqn2odata
|
|
@@ -23,7 +23,7 @@ exports.parse = function (req, res, next) {
|
|
|
23
23
|
if (typeof definition === 'string') {
|
|
24
24
|
definition =
|
|
25
25
|
service.model.definitions[definition] ||
|
|
26
|
-
service.model.definitions[definition.split(':$:')[0]].actions[definition.split(':$:')[1]]
|
|
26
|
+
service.model.definitions[definition.split(':$:')[0]].actions?.[definition.split(':$:')[1]]
|
|
27
27
|
}
|
|
28
28
|
delete query.__target
|
|
29
29
|
|
|
@@ -79,7 +79,14 @@ exports.parse = function (req, res, next) {
|
|
|
79
79
|
}
|
|
80
80
|
cds.error(errorMsg, { code: 400 })
|
|
81
81
|
}
|
|
82
|
-
if (!one)
|
|
82
|
+
if (!one) {
|
|
83
|
+
if (req.method === 'PATCH') throw cds.error ({ status: 405, code: `INVALID_${req.method}` })
|
|
84
|
+
const entity = definition, keys = {}, data = req.body || {}
|
|
85
|
+
for (let k in entity.keys) keys[k] = data[k]
|
|
86
|
+
|| entity.keys[k]['@cds.on.insert']?.['='] === '$user' && cds.context?.user?.id
|
|
87
|
+
|| cds.error ({ status: 405, code: `INVALID_${req.method}` })
|
|
88
|
+
from.ref[0] = { id: from.ref[0], where: cds.ql.predicate (keys) }
|
|
89
|
+
}
|
|
83
90
|
query = UPDATE(_target)
|
|
84
91
|
break
|
|
85
92
|
case 'DELETE':
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sap/cds",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.3.1",
|
|
4
4
|
"description": "SAP Cloud Application Programming Model - CDS for Node.js",
|
|
5
5
|
"homepage": "https://cap.cloud.sap/",
|
|
6
6
|
"keywords": [
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"node": ">=20"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@sap/cds-compiler": "^6",
|
|
35
|
+
"@sap/cds-compiler": "^6.1",
|
|
36
36
|
"@sap/cds-fiori": "^2",
|
|
37
37
|
"js-yaml": "^4.1.0"
|
|
38
38
|
},
|
package/server.js
CHANGED
|
@@ -36,6 +36,8 @@ module.exports = async function cds_server (options) {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
const app = cds.app = o.app || express()
|
|
39
|
+
// disable x-powered-by on express instance created by us
|
|
40
|
+
if (process.env.NODE_ENV === 'production' && !o.app) app.disable('x-powered-by')
|
|
39
41
|
cds.emit ('bootstrap', app)
|
|
40
42
|
|
|
41
43
|
// mount static resources and cors middleware
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = require('../libx/_runtime/common/Service')
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = require('@cap-js/db-service').DatabaseService
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = require('../libx/_runtime/messaging/service')
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = require('../libx/_runtime/remote/Service')
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
@rest
|
|
2
|
+
@path : '/ucl/spii/v1'
|
|
3
|
+
@requires: 'any'
|
|
4
|
+
@impl : '@sap/cds/srv/ucl-service.js'
|
|
5
|
+
service UCLService {
|
|
6
|
+
@open
|
|
7
|
+
@cds.persistence.skip
|
|
8
|
+
entity tenantMappings {
|
|
9
|
+
key tenantId : String;
|
|
10
|
+
context : Map;
|
|
11
|
+
receiverTenant : Map;
|
|
12
|
+
assignedTenant : Map;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/*
|
|
16
|
+
action assign(tenantId: String, context: Map, receiverTenant: Map, assignedTenant: Map) returns {
|
|
17
|
+
state : String enum {
|
|
18
|
+
CONFIG_PENDING = 'CONFIG_PENDING';
|
|
19
|
+
READY = 'READY';
|
|
20
|
+
};
|
|
21
|
+
configuration : Map;
|
|
22
|
+
};
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/*
|
|
26
|
+
action unassign(tenantId: String, context: Map, receiverTenant: Map, assignedTenant: Map) returns {
|
|
27
|
+
state : String enum {
|
|
28
|
+
READY = 'READY';
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
*/
|
|
32
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = require('../libx/_runtime/ucl/Service')
|
package/lib/ql/resolve.js
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
const { resolveView, getTransition } = require('../../libx/_runtime/common/utils/resolveView')
|
|
2
|
-
const cds = require('..')
|
|
3
|
-
|
|
4
|
-
const PERSISTENCE_TABLE = '@cds.persistence.table'
|
|
5
|
-
const _isPersistenceTable = target =>
|
|
6
|
-
Object.prototype.hasOwnProperty.call(target, PERSISTENCE_TABLE) && target[PERSISTENCE_TABLE]
|
|
7
|
-
|
|
8
|
-
// REVISIT revert after cds-dbs pr
|
|
9
|
-
// REVISIT: Remove once we get rid of old db
|
|
10
|
-
const _abortDB = resolve.abortDB = target => !!(_isPersistenceTable(target)|| !target.query?._target)
|
|
11
|
-
// _service seems to be inherited in projections, so do not consider prototype chain
|
|
12
|
-
const _defaultAbort = tx => e => e._service?.name === tx.definition?.name
|
|
13
|
-
|
|
14
|
-
function resolve(query, tx, abortCondition) {
|
|
15
|
-
const ctx = cds.context
|
|
16
|
-
const abort = abortCondition ?? (typeof tx === 'function' ? tx : undefined)
|
|
17
|
-
const _tx = typeof tx === 'function' ? ctx?.tx : tx
|
|
18
|
-
const model = ctx?.model ?? _tx.model
|
|
19
|
-
|
|
20
|
-
return resolveView(query, model, _tx, abort || _defaultAbort(_tx))
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
resolve.resolve4db = (query, tx) => resolve(query, tx, _abortDB)
|
|
24
|
-
|
|
25
|
-
// REVISIT: Remove once we get rid of composition tree
|
|
26
|
-
resolve.table = target => {
|
|
27
|
-
if (target.query?._target && !_isPersistenceTable(target)) {
|
|
28
|
-
return resolve.table(target.query._target)
|
|
29
|
-
}
|
|
30
|
-
return target
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// REVISIT: Remove argument `skipForbiddenViewCheck` once we get rid of composition tree
|
|
34
|
-
resolve.transitions = (query, tx, abortCondition, skipForbiddenViewCheck) => {
|
|
35
|
-
const target = query && typeof query === 'object' ? cds.infer.target(query) || query?._target : undefined
|
|
36
|
-
const abort = abortCondition ?? (typeof tx === 'function' ? tx : undefined)
|
|
37
|
-
const _tx = typeof tx === 'function' ? cds.context?.tx : tx
|
|
38
|
-
return getTransition(target, _tx, skipForbiddenViewCheck, undefined, {
|
|
39
|
-
abort: abort ?? (tx.isDatabaseService ? _abortDB : _defaultAbort(tx))
|
|
40
|
-
})
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
resolve.transitions4db = (query, tx, skipForbiddenViewCheck) => resolve.transitions(query, tx, _abortDB, skipForbiddenViewCheck)
|
|
44
|
-
|
|
45
|
-
module.exports = resolve
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
const { cds } = global
|
|
2
|
-
|
|
3
|
-
const { Readable } = require('stream')
|
|
4
|
-
|
|
5
|
-
const { getNormalizedDecimal, getTarget, isBase64String } = require('./utils')
|
|
6
|
-
|
|
7
|
-
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i //> "i" is acutally not OK, but we'll leave as is for now to avoid breaking changes
|
|
8
|
-
const RELAXED_UUID_REGEX = /^[0-9a-z]{8}-?[0-9a-z]{4}-?[0-9a-z]{4}-?[0-9a-z]{4}-?[0-9a-z]{12}$/i
|
|
9
|
-
|
|
10
|
-
const ISO_DATE_PART1 =
|
|
11
|
-
'[1-9]\\d{3}-(?:(?:0[1-9]|1[0-2])-(?:0[1-9]|1\\d|2[0-8])|(?:0[13-9]|1[0-2])-(?:29|30)|(?:0[13578]|1[02])-31)'
|
|
12
|
-
const ISO_DATE_PART2 = '(?:[1-9]\\d(?:0[48]|[2468][048]|[13579][26])|(?:[2468][048]|[13579][26])00)-02-29'
|
|
13
|
-
const ISO_DATE = `(?:${ISO_DATE_PART1}|${ISO_DATE_PART2})`
|
|
14
|
-
const ISO_TIME_NO_MILLIS = '(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d'
|
|
15
|
-
const ISO_TIME = `${ISO_TIME_NO_MILLIS}(?:\\.\\d{1,9})?`
|
|
16
|
-
const ISO_DATE_TIME = `${ISO_DATE}T${ISO_TIME_NO_MILLIS}(?:Z|[+-][01]\\d:?[0-5]\\d)`
|
|
17
|
-
const ISO_TIMESTAMP = `${ISO_DATE}T${ISO_TIME}(?:Z|[+-][01]\\d:?[0-5]\\d)`
|
|
18
|
-
const ISO_DATE_REGEX = new RegExp(`^${ISO_DATE}$`, 'i')
|
|
19
|
-
const ISO_TIME_NO_MILLIS_REGEX = new RegExp(`^${ISO_TIME_NO_MILLIS}$`, 'i')
|
|
20
|
-
const ISO_DATE_TIME_REGEX = new RegExp(`^${ISO_DATE_TIME}$`, 'i')
|
|
21
|
-
const ISO_TIMESTAMP_REGEX = new RegExp(`^${ISO_TIMESTAMP}$`, 'i')
|
|
22
|
-
|
|
23
|
-
const _checkString = value => typeof value === 'string'
|
|
24
|
-
|
|
25
|
-
const _checkNumber = value => typeof value === 'number' && !Number.isNaN(value)
|
|
26
|
-
|
|
27
|
-
const _oldCheckDecimal = (value, element) => {
|
|
28
|
-
const [left, right] = String(value).split('.')
|
|
29
|
-
return (
|
|
30
|
-
_checkNumber(value) &&
|
|
31
|
-
(!element.precision || left.length <= element.precision - (element.scale || 0)) &&
|
|
32
|
-
(!element.scale || ((right || '').length <= element.scale && parseFloat(right) !== 0))
|
|
33
|
-
)
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// REVISIT: only use a cheaper check if not in strictDecimal mode?
|
|
37
|
-
const _checkDecimal = (v, ele, errs, path, k) => {
|
|
38
|
-
if (!errs) return _oldCheckDecimal(v, ele)
|
|
39
|
-
|
|
40
|
-
const { precision, scale } = ele
|
|
41
|
-
let val = getNormalizedDecimal(v)
|
|
42
|
-
if (precision != null && scale != null) {
|
|
43
|
-
let isValid = true
|
|
44
|
-
if (!val.match(/\./)) val += '.0'
|
|
45
|
-
if (precision === scale) {
|
|
46
|
-
if (!val.match(new RegExp(`^-?0\\.\\d{0,${scale}}$`, 'g'))) isValid = false
|
|
47
|
-
} else if (scale === 0) {
|
|
48
|
-
if (!val.match(new RegExp(`^-?\\d{1,${precision - scale}}\\.0{0,1}$`, 'g'))) isValid = false
|
|
49
|
-
} else if (!val.match(new RegExp(`^-?\\d{1,${precision - scale}}\\.\\d{0,${scale}}$`, 'g'))) {
|
|
50
|
-
isValid = false
|
|
51
|
-
}
|
|
52
|
-
if (!isValid) {
|
|
53
|
-
const args = [v, `Decimal(${precision},${scale})`]
|
|
54
|
-
const target = getTarget(path, k)
|
|
55
|
-
errs.push(new cds.error('ASSERT_DATA_TYPE', { args, target, statusCode: 400, code: '400' }))
|
|
56
|
-
}
|
|
57
|
-
} else if (precision != null) {
|
|
58
|
-
if (!val.match(new RegExp(`^-?\\d{1,${precision}}$`, 'g'))) {
|
|
59
|
-
const args = [v, `Decimal(${precision})`]
|
|
60
|
-
const target = getTarget(path, k)
|
|
61
|
-
errs.push(new cds.error('ASSERT_DATA_TYPE', { args, target, statusCode: 400, code: '400' }))
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const _checkInt = value => _checkNumber(value) && parseInt(value, 10) === value
|
|
67
|
-
|
|
68
|
-
const _checkInt64 = value => typeof value === 'string' ? value.match(/^\d+$/) : _checkInt(value)
|
|
69
|
-
|
|
70
|
-
const _checkBoolean = value => typeof value === 'boolean'
|
|
71
|
-
|
|
72
|
-
const _checkBuffer = value => Buffer.isBuffer(value) || value.type === 'Buffer' || isBase64String(value)
|
|
73
|
-
|
|
74
|
-
const _checkStreamOrBuffer = value => value instanceof Readable || _checkBuffer(value)
|
|
75
|
-
|
|
76
|
-
const _checkUUID = value => _checkString(value) && UUID_REGEX.test(value)
|
|
77
|
-
|
|
78
|
-
const _checkRelaxedUUID = value => _checkString(value) && RELAXED_UUID_REGEX.test(value)
|
|
79
|
-
|
|
80
|
-
const _checkISODate = value => (_checkString(value) && ISO_DATE_REGEX.test(value)) || value instanceof Date
|
|
81
|
-
|
|
82
|
-
const _checkISOTime = value => _checkString(value) && ISO_TIME_NO_MILLIS_REGEX.test(value)
|
|
83
|
-
|
|
84
|
-
const _checkISODateTime = value => (_checkString(value) && ISO_DATE_TIME_REGEX.test(value)) || value instanceof Date
|
|
85
|
-
|
|
86
|
-
const _checkISOTimestamp = value => (_checkString(value) && ISO_TIMESTAMP_REGEX.test(value)) || value instanceof Date
|
|
87
|
-
|
|
88
|
-
module.exports = {
|
|
89
|
-
'cds.UUID': _checkUUID,
|
|
90
|
-
'relaxed.UUID': _checkRelaxedUUID,
|
|
91
|
-
'cds.Boolean': _checkBoolean,
|
|
92
|
-
'cds.Integer': _checkInt,
|
|
93
|
-
'cds.UInt8': _checkInt,
|
|
94
|
-
'cds.Int16': _checkInt,
|
|
95
|
-
'cds.Int32': _checkInt,
|
|
96
|
-
'cds.Integer64': _checkInt64,
|
|
97
|
-
'cds.Int64': _checkInt64,
|
|
98
|
-
'cds.Decimal': _checkDecimal,
|
|
99
|
-
'cds.DecimalFloat': _checkNumber,
|
|
100
|
-
'cds.Double': _checkNumber,
|
|
101
|
-
'cds.Date': _checkISODate,
|
|
102
|
-
'cds.Time': _checkISOTime,
|
|
103
|
-
'cds.DateTime': _checkISODateTime,
|
|
104
|
-
'cds.Timestamp': _checkISOTimestamp,
|
|
105
|
-
'cds.String': _checkString,
|
|
106
|
-
'cds.Binary': _checkBuffer,
|
|
107
|
-
'cds.LargeString': _checkString,
|
|
108
|
-
'cds.LargeBinary': _checkStreamOrBuffer
|
|
109
|
-
}
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
const getNormalizedDecimal = val => {
|
|
2
|
-
let v = `${val}`
|
|
3
|
-
const cgs = v.match(/^(\d*\.*\d*)e([+|-]*)(\d*)$/)
|
|
4
|
-
if (cgs) {
|
|
5
|
-
let [l, r = ''] = cgs[1].split('.')
|
|
6
|
-
const dir = cgs[2] || '+'
|
|
7
|
-
const exp = Number(cgs[3])
|
|
8
|
-
if (dir === '+') {
|
|
9
|
-
// move decimal point to the right
|
|
10
|
-
r = r.padEnd(exp, '0')
|
|
11
|
-
l += r.substring(0, exp)
|
|
12
|
-
r = r.slice(exp)
|
|
13
|
-
v = `${l}${r ? '.' + r : ''}`
|
|
14
|
-
} else {
|
|
15
|
-
// move decimal point to the left
|
|
16
|
-
l = l.padStart(exp, '0')
|
|
17
|
-
r = l.substring(0, exp) + r
|
|
18
|
-
l = l.slice(exp)
|
|
19
|
-
v = `${l ? l : '0'}.${r}`
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
return v
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function getTarget(path, k) {
|
|
26
|
-
return path.length && path[path.length - 1].match(/\[\d+\]$/) ? path.join('/') : path.concat(k).join('/')
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// non-strict mode also allows url-safe base64 strings
|
|
30
|
-
function isBase64String(string, strict = false) {
|
|
31
|
-
if (typeof string !== 'string') return false
|
|
32
|
-
|
|
33
|
-
if (strict && string.length % 4 !== 0) return false
|
|
34
|
-
|
|
35
|
-
let length = string.length
|
|
36
|
-
if (string.endsWith('==')) length -= 2
|
|
37
|
-
else if (string.endsWith('=')) length -= 1
|
|
38
|
-
|
|
39
|
-
let char
|
|
40
|
-
for (let i = 0; i < length; i++) {
|
|
41
|
-
char = string[i]
|
|
42
|
-
if (char >= 'A' && char <= 'Z') continue
|
|
43
|
-
else if (char >= 'a' && char <= 'z') continue
|
|
44
|
-
else if (char >= '0' && char <= '9') continue
|
|
45
|
-
else if (char === '+' || char === '/') continue
|
|
46
|
-
else if (!strict && (char === '-' || char === '_')) continue
|
|
47
|
-
return false
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
return true
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
module.exports = {
|
|
57
|
-
getNormalizedDecimal,
|
|
58
|
-
getTarget,
|
|
59
|
-
isBase64String
|
|
60
|
-
}
|