@sap/cds 8.8.0 → 8.8.2
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 +24 -0
- package/lib/compile/parse.js +1 -1
- package/lib/i18n/bundles.js +17 -17
- package/lib/i18n/files.js +5 -3
- package/lib/i18n/index.js +1 -1
- package/lib/i18n/localize.js +2 -2
- package/lib/ql/cds.ql-predicates.js +2 -1
- package/lib/req/validate.js +1 -1
- package/libx/_runtime/common/generic/auth/restrict.js +1 -1
- package/libx/_runtime/common/generic/auth/utils.js +2 -1
- package/libx/_runtime/remote/Service.js +1 -1
- package/libx/odata/middleware/create.js +4 -0
- package/libx/odata/parse/afterburner.js +1 -0
- package/libx/odata/parse/multipartToJson.js +17 -10
- package/libx/odata/utils/metadata.js +28 -5
- package/libx/odata/utils/normalizeTimeData.js +11 -8
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,30 @@
|
|
|
4
4
|
- The format is based on [Keep a Changelog](https://keepachangelog.com/).
|
|
5
5
|
- This project adheres to [Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## Version 8.8.2 - 2025-03-13
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Consuming REST actions returning anonymous structures
|
|
12
|
+
- `i18n.labels/messages` were occasionally missing
|
|
13
|
+
|
|
14
|
+
## Version 8.8.1 - 2025-03-07
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- Requests violating `cds.odata.max_batch_header_size` are terminated with `431 Request Header Fields Too Large` instead of `400 - Bad Request`
|
|
19
|
+
- `cds.parse.<x>` writing directly to `stdout`
|
|
20
|
+
- Instance-based authorization for programmatic action invocations
|
|
21
|
+
- Implicit function parameter calls with Array or Object values
|
|
22
|
+
- OData: Throw an error by `POST` with payload that contains array of entity representation
|
|
23
|
+
- `cds.validate` filters out annotations according to OData V4 spec
|
|
24
|
+
- Crash for requests with invalid time data format
|
|
25
|
+
- Add missing 'and' between conditions in object notation of QL
|
|
26
|
+
- Multiline payloads in `$batch` sub requests
|
|
27
|
+
- Instance-based authorization for modeling like `$user.<property> is null`
|
|
28
|
+
- Respect `cds.odata.contextAbsoluteUrl` in new OData adapter
|
|
29
|
+
- `cds.odata.context_with_columns` also applies to singletons
|
|
30
|
+
|
|
7
31
|
## Version 8.8.0 - 2025-03-03
|
|
8
32
|
|
|
9
33
|
### Added
|
package/lib/compile/parse.js
CHANGED
|
@@ -29,7 +29,7 @@ exports.path = function path (x,...etc) {
|
|
|
29
29
|
const [,head,tail] = /^([\w._]+)(?::(\w+))?$/.exec(x)||[]
|
|
30
30
|
if (tail) return {ref:[head,...tail.split('.')]}
|
|
31
31
|
if (head) return {ref:[head]}
|
|
32
|
-
const {SELECT} = cdsc.parse.cql('SELECT from '+x)
|
|
32
|
+
const {SELECT} = cdsc.parse.cql('SELECT from '+x, undefined, { messages: [] })
|
|
33
33
|
return SELECT.from
|
|
34
34
|
}
|
|
35
35
|
|
package/lib/i18n/bundles.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
const cds = require('..'), {i18n} = cds.env
|
|
2
2
|
const I18nFiles = require ('./files')
|
|
3
|
+
const DEFAULTS = i18n.default_language
|
|
4
|
+
const FALLBACK = ''
|
|
3
5
|
|
|
4
6
|
|
|
5
7
|
class I18nBundle {
|
|
@@ -7,14 +9,14 @@ class I18nBundle {
|
|
|
7
9
|
constructor (options={}) {
|
|
8
10
|
this.files = new I18nFiles (options)
|
|
9
11
|
this.file = this.files.basename
|
|
12
|
+
this.fallback = this.#translations[FALLBACK] = Object.assign ({}, ...this.files.content4(FALLBACK))
|
|
13
|
+
this.defaults = this.#translations[DEFAULTS] = Object.assign (
|
|
14
|
+
i18n.fatjson ? {...this.fallback} : {__proto__:this.fallback}, ...this.files.content4(DEFAULTS)
|
|
15
|
+
)
|
|
10
16
|
}
|
|
11
17
|
|
|
12
18
|
#translations = {}
|
|
13
19
|
|
|
14
|
-
/** The default texts to use as fallbacks if a requested translation is not found. */
|
|
15
|
-
get defaults() { return super.defaults = this.texts4 (i18n.default_language, this.fallback) }
|
|
16
|
-
get fallback() { return super.fallback = this.texts4 ('',false) }
|
|
17
|
-
|
|
18
20
|
/** Synonym for {@link at `this.at`} */
|
|
19
21
|
get for() { return this.at }
|
|
20
22
|
|
|
@@ -23,6 +25,7 @@ class I18nBundle {
|
|
|
23
25
|
* Looks up the entry for the given key and locale.
|
|
24
26
|
* - if `locale` is omitted, the current locale is used.
|
|
25
27
|
* - if `args` are provided, fills in placeholders with them.
|
|
28
|
+
* @example cds.i18n.labels.at ('CreatedAt','de')
|
|
26
29
|
* @returns {string|undefined}
|
|
27
30
|
*/
|
|
28
31
|
at (key, locale, args) {
|
|
@@ -62,32 +65,29 @@ class I18nBundle {
|
|
|
62
65
|
|
|
63
66
|
/**
|
|
64
67
|
* Returns translated texts for a specific locale.
|
|
68
|
+
* @example cds.i18n.labels.texts4 ('de')
|
|
65
69
|
*/
|
|
66
|
-
texts4 (locale=''
|
|
70
|
+
texts4 (locale='') {
|
|
67
71
|
const $ = this.#translations; if (locale in $) return $[locale]
|
|
68
72
|
const suffix = locale.replace(/-/g,'_'); if (suffix in $) return $[locale] = $[suffix]
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
if (lang in $) return $[locale] = $[lang] // already cached -> leave method
|
|
74
|
-
else all = this.files.content4 (lang, lang) // load content for language only
|
|
73
|
+
const all = this.files.content4 (locale, suffix) // load content from all folders
|
|
74
|
+
if (!all.length) { // nothing found, try w/o region, or return defaults
|
|
75
|
+
const _ = suffix.indexOf('_')
|
|
76
|
+
return $[locale] = $[suffix] = _ < 0 ? this.defaults : this.texts4 (suffix.slice(0,_))
|
|
75
77
|
}
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
return $[locale] = $[suffix] = Object.assign (texts, ...all)
|
|
78
|
+
const texts = i18n.fatjson ? {...this.defaults} : {__proto__:this.defaults}
|
|
79
|
+
return $[locale] = $[suffix] = Object.assign (texts, ...all )
|
|
79
80
|
}
|
|
80
81
|
|
|
81
82
|
|
|
82
83
|
/**
|
|
83
84
|
* Returns all translations for an array of locales or all locales.
|
|
84
|
-
* @example { de, fr } = cds.i18n.labels.
|
|
85
|
+
* @example { de, fr } = cds.i18n.labels.translations4 ('de','fr')
|
|
85
86
|
* @param { 'all' | string[] } [locale]
|
|
86
87
|
* @returns {{ [locale:string]: Record<string,string> }}
|
|
87
88
|
*/
|
|
88
89
|
translations4 (...locales) {
|
|
89
|
-
let first = locales[0]
|
|
90
|
-
if (first == null) locales = cds.env.i18n.languages
|
|
90
|
+
let first = locales[0] || cds.env.i18n.languages
|
|
91
91
|
if (first == 'all') locales = this.files.locales()
|
|
92
92
|
else if (Array.isArray(first)) locales = first
|
|
93
93
|
return locales.reduce ((all,l) => (all[l] = this.texts4(l),all), {})
|
package/lib/i18n/files.js
CHANGED
|
@@ -36,7 +36,7 @@ class I18nFiles {
|
|
|
36
36
|
const leafs = model?.$sources.map(path.dirname) ?? roots, visited = {}
|
|
37
37
|
;[...new Set(leafs)].reverse() .forEach (function _visit (dir) {
|
|
38
38
|
if (dir in visited) return; else visited[dir] = true
|
|
39
|
-
LOG.debug ('
|
|
39
|
+
LOG.debug ('fetching', basename, 'bundles in', dir, relative_folders)
|
|
40
40
|
// is there an i18n folder in the currently visited directory?
|
|
41
41
|
for (const each of relative_folders) {
|
|
42
42
|
const f = path.join(dir,each), _exists = _folders[f] ??= exists(f)
|
|
@@ -60,6 +60,8 @@ class I18nFiles {
|
|
|
60
60
|
const matches = (_entries[f] ??= fs.readdirSync(f)) .filter (f => f.match(base))
|
|
61
61
|
if (matches.length) return files[f] = matches
|
|
62
62
|
}
|
|
63
|
+
|
|
64
|
+
LOG.debug ('found', basename, 'bundles in these folders', Object.keys(files))
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
|
|
@@ -68,7 +70,7 @@ class I18nFiles {
|
|
|
68
70
|
* @returns {entries[]} An array of entries, one for each file found.
|
|
69
71
|
*/
|
|
70
72
|
content4 (locale, suffix = locale?.replace(/-/g,'_')) {
|
|
71
|
-
const content = [], cached = I18nFiles.
|
|
73
|
+
const content = [], cached = I18nFiles[this.basename] ??= {}
|
|
72
74
|
const _suffix = suffix ? '_'+ suffix : ''
|
|
73
75
|
for (let dir in this) {
|
|
74
76
|
const all = cached[dir] ??= this.load('.json',dir) || this.load('.csv',dir) || false
|
|
@@ -87,7 +89,7 @@ class I18nFiles {
|
|
|
87
89
|
case '.json': return _load_json(file)
|
|
88
90
|
case '.csv': return _load_csv(file)
|
|
89
91
|
}}
|
|
90
|
-
finally { LOG.debug ('
|
|
92
|
+
finally { LOG.debug ('loading:', file) }
|
|
91
93
|
}
|
|
92
94
|
|
|
93
95
|
|
package/lib/i18n/index.js
CHANGED
package/lib/i18n/localize.js
CHANGED
|
@@ -62,7 +62,7 @@ function localize (input,...etc) {
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
exports.edmx4 = service => new class extends Localize { get input(){
|
|
65
|
-
const model = this.model || cds.
|
|
65
|
+
const model = this.model || cds.model
|
|
66
66
|
return super.input = cds.compile.to.edmx (model,{service})
|
|
67
67
|
}}
|
|
68
68
|
|
|
@@ -124,7 +124,7 @@ exports.json = json => {
|
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
/** @deprecated */ exports.bundle4 = function (model, locale) {
|
|
127
|
-
if (typeof model === 'string') [ model, locale ] = [ cds.
|
|
127
|
+
if (typeof model === 'string') [ model, locale ] = [ cds.model, model ]
|
|
128
128
|
const b = i18n.bundle4 (model)
|
|
129
129
|
return b.texts4 (locale)
|
|
130
130
|
}
|
|
@@ -48,6 +48,8 @@ function _qbe (o, xpr=[]) {
|
|
|
48
48
|
let count = 0
|
|
49
49
|
for (let k in o) { const x = o[k]
|
|
50
50
|
|
|
51
|
+
if (k !== 'and' && k !== 'or' && count++) xpr.push('and') //> add 'and' between conditions
|
|
52
|
+
|
|
51
53
|
if (k.startsWith('not ')) { xpr.push('not'); k = k.slice(4) }
|
|
52
54
|
switch (k) { // handle special cases like {and:{...}} or {or:{...}}
|
|
53
55
|
case 'between':
|
|
@@ -83,7 +85,6 @@ function _qbe (o, xpr=[]) {
|
|
|
83
85
|
}
|
|
84
86
|
|
|
85
87
|
const a = cds.parse.ref(k) //> turn key into a ref for the left side of the expression
|
|
86
|
-
if (count++) xpr.push('and') //> add 'and' between conditions
|
|
87
88
|
if (!x || typeof x !== 'object') xpr.push (a,'=',{val:x})
|
|
88
89
|
else if (is_array(x)) xpr.push (a,'in',{list:x.map(_val)})
|
|
89
90
|
else if (x.SELECT || x.list) xpr.push (a,'in',x)
|
package/lib/req/validate.js
CHANGED
|
@@ -48,7 +48,7 @@ class Validation {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
unknown(e,d,input) {
|
|
51
|
-
if (e.
|
|
51
|
+
if (e.match(/@.*\./)) return delete input[e] //> skip all annotations, like @odata.Type (according to OData spec annotations contain an "@" and a ".")
|
|
52
52
|
d['@open'] || cds.error (`Property "${e}" does not exist in ${d.name}`, {status:400})
|
|
53
53
|
}
|
|
54
54
|
}
|
|
@@ -296,7 +296,7 @@ const isBoundToCollection = action =>
|
|
|
296
296
|
const restrictBoundActionFunctions = async (req, resolvedApplicables, definition, srv) => {
|
|
297
297
|
if (req.target?.actions?.[req.event] && !isBoundToCollection(req.target.actions[req.event])) {
|
|
298
298
|
// Clone to avoid target modification, which would cause a different query
|
|
299
|
-
const query = cds.ql.clone(req.query)
|
|
299
|
+
const query = req.query ? cds.ql.clone(req.query) : SELECT.one.from(req.subject)
|
|
300
300
|
_addRestrictionsToRead({ query: query, target: req.target }, cds.model, resolvedApplicables)
|
|
301
301
|
const result = await (cds.env.features.compat_restrict_bound ? srv : cds.tx(req)).run(query)
|
|
302
302
|
if (!result || result.length === 0) {
|
|
@@ -126,7 +126,8 @@ const resolveUserAttrs = (where, req) => {
|
|
|
126
126
|
} else if (where[i + 2] && operators.has(where[i + 1])) {
|
|
127
127
|
where.splice(i, 3, { val: '1' }, '=', { val: '2' })
|
|
128
128
|
} else if (where[i + 1] === 'is') {
|
|
129
|
-
|
|
129
|
+
val = null
|
|
130
|
+
break
|
|
130
131
|
}
|
|
131
132
|
} else val = val?.[attr]
|
|
132
133
|
}
|
|
@@ -93,7 +93,7 @@ const _handleUnboundActionFunction = (srv, def, req, event) => {
|
|
|
93
93
|
if (def.kind === 'action') {
|
|
94
94
|
// REVISIT: only for "rest" unbound actions/functions, we enforce axios to return a buffer
|
|
95
95
|
// required by cds-mt
|
|
96
|
-
const isBinary = srv.kind === 'rest' && def?.returns?.type
|
|
96
|
+
const isBinary = srv.kind === 'rest' && def?.returns?.type?.match(/binary/i)
|
|
97
97
|
const { headers, data } = req
|
|
98
98
|
|
|
99
99
|
return srv.send({ method: 'POST', path: `/${event}`, headers, data, _binary: isBinary })
|
|
@@ -33,6 +33,10 @@ module.exports = (adapter, isUpsert) => {
|
|
|
33
33
|
|
|
34
34
|
// payload & params
|
|
35
35
|
const data = req.body
|
|
36
|
+
if (Array.isArray(data)) {
|
|
37
|
+
const msg = 'Only single entity representations are allowed'
|
|
38
|
+
throw Object.assign(new Error(msg), { statusCode: 400 })
|
|
39
|
+
}
|
|
36
40
|
normalizeTimeData(data, model, target)
|
|
37
41
|
const { keys, params } = getKeysAndParamsFromPath(from, { model })
|
|
38
42
|
// add keys from url into payload (overwriting if already present)
|
|
@@ -415,6 +415,7 @@ function _processSegments(from, model, namespace, cqn, protocol) {
|
|
|
415
415
|
if (current.params && param in current.params) acc[param] = from._params[cur]
|
|
416
416
|
return acc
|
|
417
417
|
}, {})
|
|
418
|
+
ref[i].args = _getDataFromParams(ref[i].args, current) //resolve parameter if Object or Array
|
|
418
419
|
_resolveImplicitFunctionParameters(ref[i].args)
|
|
419
420
|
}
|
|
420
421
|
}
|
|
@@ -116,28 +116,35 @@ const _parseStream = async function* (body, boundary) {
|
|
|
116
116
|
.toString()
|
|
117
117
|
.replace(/^--(.*)$/gm, (_, g) => `HEAD /${g} HTTP/1.1${g.slice(-2) === '--' ? CRLF : ''}`)
|
|
118
118
|
// correct content-length for non-HEAD requests is inserted below
|
|
119
|
-
.replace(/content-length: \d+\r\n/gim, '')
|
|
119
|
+
.replace(/content-length: \d+\r\n/gim, '') // if content-length is given it should be taken
|
|
120
120
|
.replace(/ \$/g, ' /$')
|
|
121
121
|
|
|
122
122
|
// HACKS!!!
|
|
123
123
|
// ensure URLs start with slashes
|
|
124
124
|
changed = changed.replaceAll(/\r\n(GET|PUT|POST|PATCH|DELETE) (\w)/g, `\r\n$1 /$2`)
|
|
125
125
|
// add content-length headers
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
126
|
+
changed = changed
|
|
127
|
+
.split(CRLF + CRLF)
|
|
128
|
+
.map((line, i, arr) => {
|
|
129
|
+
if (/^(PUT|POST|PATCH) /.test(line) && !/content-length/i.test(line)) {
|
|
130
|
+
const body = arr[i + 1].split('\r\nHEAD')[0]
|
|
131
|
+
if (body) return `${line}${CRLF}content-length: ${Buffer.byteLength(body)}`
|
|
132
|
+
}
|
|
133
|
+
return line
|
|
134
|
+
})
|
|
135
|
+
.join(CRLF + CRLF)
|
|
134
136
|
// remove strange "Group ID" appendix
|
|
135
137
|
changed = changed.split(`${CRLF}Group ID`)[0] + CRLF
|
|
136
138
|
|
|
137
139
|
let ret = parser.execute(Buffer.from(changed))
|
|
138
140
|
|
|
139
141
|
if (typeof ret !== 'number') {
|
|
140
|
-
if (ret.
|
|
142
|
+
if (ret.code === 'HPE_HEADER_OVERFLOW') {
|
|
143
|
+
// same error conversion as node http server
|
|
144
|
+
ret.status = 431
|
|
145
|
+
ret.code = '431'
|
|
146
|
+
ret.message = 'Request Header Fields Too Large'
|
|
147
|
+
} else if (ret.message === 'Parse Error') {
|
|
141
148
|
ret.statusCode = 400
|
|
142
149
|
ret.message = `Error while parsing batch body at position ${ret.bytesParsed}: ${ret.reason}`
|
|
143
150
|
}
|
|
@@ -1,7 +1,28 @@
|
|
|
1
|
-
const { cds2edm } = require('./index')
|
|
2
1
|
const cds = require('../../../lib')
|
|
2
|
+
const LOG = cds.log('odata')
|
|
3
|
+
const { appURL } = require('../../_runtime/common/utils/vcap')
|
|
4
|
+
const { cds2edm } = require('./index')
|
|
3
5
|
const { where2obj } = require('../../_runtime/common/utils/cqn')
|
|
4
6
|
|
|
7
|
+
const _getContextAbsoluteUrl = _req => {
|
|
8
|
+
const { contextAbsoluteUrl } = cds.env.odata
|
|
9
|
+
if (!contextAbsoluteUrl) return ''
|
|
10
|
+
|
|
11
|
+
if (typeof contextAbsoluteUrl === 'string') {
|
|
12
|
+
try {
|
|
13
|
+
const userDefinedURL = new URL(contextAbsoluteUrl, contextAbsoluteUrl).toString()
|
|
14
|
+
return (!userDefinedURL.endsWith('/') && `${userDefinedURL}/`) || userDefinedURL
|
|
15
|
+
} catch (e) {
|
|
16
|
+
e.message = `cds.odata.contextAbsoluteUrl could not be parsed as URL: ${contextAbsoluteUrl}`
|
|
17
|
+
LOG._warn && LOG.warn(e)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
const reqURL = _req && _req.get && _req.get('host') && `${_req.protocol || 'https'}://${_req.get('host')}`
|
|
21
|
+
const baseAppURL = appURL || reqURL || ''
|
|
22
|
+
const serviceUrl = `${(_req && _req.baseUrl) || ''}/`
|
|
23
|
+
return baseAppURL && new URL(serviceUrl, baseAppURL).toString()
|
|
24
|
+
}
|
|
25
|
+
|
|
5
26
|
const _isNavToDraftAdmin = path => path.length > 1 && path[path.length - 1] === 'DraftAdministrativeData'
|
|
6
27
|
|
|
7
28
|
const _lastValidRef = ref => {
|
|
@@ -14,7 +35,9 @@ const _lastValidRef = ref => {
|
|
|
14
35
|
const _toBinaryKeyValue = value => `binary'${value.toString('base64')}'`
|
|
15
36
|
|
|
16
37
|
const _odataContext = (query, options) => {
|
|
17
|
-
|
|
38
|
+
const { contextAbsoluteUrl, context_with_columns } = cds.env.odata
|
|
39
|
+
|
|
40
|
+
let path = _getContextAbsoluteUrl(query._req) + '$metadata'
|
|
18
41
|
if (query._target.kind === 'service') return path
|
|
19
42
|
|
|
20
43
|
const {
|
|
@@ -45,14 +68,14 @@ const _odataContext = (query, options) => {
|
|
|
45
68
|
if (serviceName && !isType) edmName = edmName.replace(serviceName + '.', '').replace(/\./g, '_')
|
|
46
69
|
|
|
47
70
|
// prepend "../" parent segments for relative path
|
|
48
|
-
if (ref.length > 1) path = '../'.repeat(ref.length - 1) + path
|
|
71
|
+
if (!contextAbsoluteUrl && ref.length > 1) path = '../'.repeat(ref.length - 1) + path
|
|
49
72
|
|
|
50
73
|
path += edmName
|
|
51
74
|
|
|
52
75
|
const lastRef = ref.at(-1)
|
|
53
76
|
|
|
54
77
|
if (propertyAccess || isNavToDraftAdmin) {
|
|
55
|
-
if (propertyAccess) path = '../' + path
|
|
78
|
+
if (!contextAbsoluteUrl && propertyAccess) path = '../' + path
|
|
56
79
|
|
|
57
80
|
const keyValuePairs = []
|
|
58
81
|
|
|
@@ -92,7 +115,7 @@ const _odataContext = (query, options) => {
|
|
|
92
115
|
if (propertyAccess) path += '/' + propertyAccess
|
|
93
116
|
}
|
|
94
117
|
|
|
95
|
-
if (
|
|
118
|
+
if (context_with_columns && query.SELECT && !propertyAccess) {
|
|
96
119
|
const _calculateStringFromColumn = column => {
|
|
97
120
|
if (column === '*') return
|
|
98
121
|
|
|
@@ -7,14 +7,17 @@ const _processorFn = elementInfo => {
|
|
|
7
7
|
for (const category of plain.categories) {
|
|
8
8
|
const { row, key } = elementInfo
|
|
9
9
|
if (!(row[key] == null) && row[key] !== '$now') {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
10
|
+
const dt = typeof row[key] === 'string' && new Date(row[key])
|
|
11
|
+
if (!isNaN(dt)) {
|
|
12
|
+
switch (category) {
|
|
13
|
+
case 'cds.DateTime':
|
|
14
|
+
row[key] = new Date(row[key]).toISOString().replace(/\.\d\d\d/, '')
|
|
15
|
+
break
|
|
16
|
+
case 'cds.Timestamp':
|
|
17
|
+
row[key] = normalizeTimestamp(row[key])
|
|
18
|
+
break
|
|
19
|
+
// no default
|
|
20
|
+
}
|
|
18
21
|
}
|
|
19
22
|
}
|
|
20
23
|
}
|