@sap/cds 8.4.2 → 8.5.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 +35 -1
- package/_i18n/messages.properties +99 -0
- package/bin/serve.js +2 -2
- package/lib/compile/cdsc.js +9 -4
- package/lib/compile/to/srvinfo.js +4 -4
- package/lib/core/entities.js +1 -0
- package/lib/core/types.js +1 -1
- package/lib/dbs/cds-deploy.js +4 -1
- package/lib/env/defaults.js +7 -6
- package/lib/env/schemas/cds-rc.js +132 -22
- package/lib/i18n/bundles.js +111 -0
- package/lib/i18n/files.js +134 -0
- package/lib/i18n/index.js +63 -0
- package/lib/i18n/localize.js +101 -237
- package/lib/i18n/resources.js +150 -0
- package/lib/index.js +1 -0
- package/lib/log/format/aspects/cls.js +6 -1
- package/lib/log/format/json.js +1 -1
- package/lib/ql/CREATE.js +1 -0
- package/lib/ql/DELETE.js +1 -0
- package/lib/ql/DROP.js +1 -0
- package/lib/ql/INSERT.js +9 -8
- package/lib/ql/Query.js +18 -8
- package/lib/ql/SELECT.js +1 -0
- package/lib/ql/UPDATE.js +2 -1
- package/lib/ql/UPSERT.js +1 -1
- package/lib/ql/Whereable.js +3 -3
- package/lib/ql/cds-ql.js +12 -18
- package/lib/req/user.js +1 -0
- package/lib/req/validate.js +12 -3
- package/lib/srv/factory.js +2 -2
- package/lib/{auth → srv/middlewares/auth}/basic-auth.js +1 -1
- package/lib/{auth → srv/middlewares/auth}/dummy-auth.js +1 -1
- package/lib/srv/middlewares/auth/ias-auth.js +96 -0
- package/lib/{auth → srv/middlewares/auth}/index.js +2 -2
- package/lib/srv/middlewares/auth/jwt-auth.js +62 -0
- package/lib/{auth → srv/middlewares/auth}/mocked-users.js +1 -1
- package/lib/srv/middlewares/auth/xssec.js +7 -0
- package/lib/srv/middlewares/index.js +1 -1
- package/lib/utils/cds-utils.js +15 -19
- package/lib/utils/tar.js +2 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +2 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +1 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +1 -1
- package/libx/_runtime/common/error/frontend.js +2 -6
- package/libx/_runtime/common/error/log.js +7 -8
- package/libx/_runtime/common/error/utils.js +3 -7
- package/libx/_runtime/common/generic/auth/capabilities.js +1 -1
- package/libx/_runtime/common/generic/input.js +41 -6
- package/libx/_runtime/common/i18n/index.js +8 -15
- package/libx/_runtime/common/utils/compareJson.js +10 -1
- package/libx/_runtime/common/utils/resolveView.js +1 -1
- package/libx/_runtime/fiori/lean-draft.js +77 -26
- package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +5 -1
- package/libx/_runtime/messaging/kafka.js +1 -1
- package/libx/odata/index.js +3 -0
- package/libx/odata/middleware/create.js +8 -6
- package/libx/odata/middleware/update.js +24 -21
- package/libx/odata/parse/afterburner.js +15 -2
- package/libx/odata/parse/grammar.peggy +24 -7
- package/libx/odata/parse/parser.js +1 -1
- package/libx/odata/utils/postProcess.js +4 -1
- package/libx/rest/RestAdapter.js +2 -1
- package/libx/rest/middleware/error.js +0 -50
- package/package.json +1 -1
- package/lib/auth/ias-auth.js +0 -68
- package/lib/auth/ias-claims.js +0 -34
- package/lib/auth/jwt-auth.js +0 -70
- package/libx/_runtime/common/i18n/messages.properties +0 -99
- package/libx/_runtime/common/utils/require.js +0 -9
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
const cds = require('..'), {i18n} = cds.env
|
|
2
|
+
const I18nFiles = require ('./files')
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class I18nBundle {
|
|
6
|
+
|
|
7
|
+
constructor (options={}) {
|
|
8
|
+
this.files = new I18nFiles (options)
|
|
9
|
+
this.file = this.files.basename
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
#translations = {}
|
|
13
|
+
|
|
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
|
+
/** Synonym for {@link at `this.at`} */
|
|
19
|
+
get for() { return this.at }
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Looks up the entry for the given key and locale.
|
|
24
|
+
* - if `locale` is omitted, the current locale is used.
|
|
25
|
+
* - if `args` are provided, fills in placeholders with them.
|
|
26
|
+
* @returns {string|undefined}
|
|
27
|
+
*/
|
|
28
|
+
at (key, locale, args) {
|
|
29
|
+
if (typeof locale !== 'string') [ args, locale ] = [ locale, cds.context?.locale ?? i18n.default_language ]
|
|
30
|
+
if (typeof key === 'object') key = this.key4(key)
|
|
31
|
+
let t = this.texts4 (locale) [key]
|
|
32
|
+
if (t && args) t = t.replace (/{(\w+)}/g, (_,k) => args[k])
|
|
33
|
+
return t
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Calls this.{@link at}() for all specified locales and returns an dictionary of results.
|
|
39
|
+
* @example cds.i18n.labels.all('CreatedBy')
|
|
40
|
+
*/
|
|
41
|
+
all (key, locales, args) {
|
|
42
|
+
if (!key) return { ...this.translations() } // eslint-disable-line no-constant-binary-expression
|
|
43
|
+
const all={}, translations = this.translations4 (locales)
|
|
44
|
+
for (let locale in translations) {
|
|
45
|
+
let t = translations[locale][key]
|
|
46
|
+
if (t && args) t = t.replace (/{(\w+)}/g, (_,k) => args[k])
|
|
47
|
+
all[locale] = t
|
|
48
|
+
}
|
|
49
|
+
return all
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Used by {@link at `this.at()`} to determine the i18n key for a given CSN definition.
|
|
55
|
+
*/
|
|
56
|
+
key4 (d) {
|
|
57
|
+
const anno = d['@Common.Label'] || d['@title'] || d['@UI.HeaderInfo.TypeName']
|
|
58
|
+
if (anno) return /{i18n>([^}]+)}/.exec(anno)?.[1] || anno
|
|
59
|
+
else return d.name || d.type // if any
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Returns translated texts for a specific locale.
|
|
65
|
+
*/
|
|
66
|
+
texts4 (locale='', _defaults) {
|
|
67
|
+
const $ = this.#translations; if (locale in $) return $[locale]
|
|
68
|
+
const suffix = locale.replace(/-/g,'_'); if (suffix in $) return $[locale] = $[suffix]
|
|
69
|
+
// nothing cached yet, load content and create translations ...
|
|
70
|
+
let all = this.files.content4 (locale, suffix) // load content from all folders
|
|
71
|
+
if (all.length === 0 && suffix.includes('_')) { // nothing found, try w/o region ...
|
|
72
|
+
const lang = suffix.match(/(\w+)_/)[1] // i.e. en_UK => en
|
|
73
|
+
if (lang in $) return $[locale] = $[lang] // already cached -> leave method
|
|
74
|
+
else all = this.files.content4 (lang, lang) // load content for language only
|
|
75
|
+
}
|
|
76
|
+
const texts = Object.create ((_defaults ?? this.defaults) || Object.prototype)
|
|
77
|
+
return $[locale] = $[suffix] = Object.assign (texts, ...all)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Returns all translations for an array of locales or all locales.
|
|
83
|
+
* @example { de, fr } = cds.i18n.labels.translations('de','fr')
|
|
84
|
+
* @param { 'all' | string[] } [locale]
|
|
85
|
+
* @returns {{ [locale:string]: Record<string,string> }}
|
|
86
|
+
*/
|
|
87
|
+
translations4 (...locales) {
|
|
88
|
+
let first = locales[0]
|
|
89
|
+
if (first == null) locales = cds.env.i18n.languages
|
|
90
|
+
if (first == 'all') locales = this.files.locales()
|
|
91
|
+
else if (Array.isArray(first)) locales = first
|
|
92
|
+
return locales.reduce ((all,l) => (all[l] = this.texts4(l),all), {})
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Returns a proxy to lazily access all translations by locales as object properties.
|
|
98
|
+
* @example { de, fr } = cds.i18n.labels.translations()
|
|
99
|
+
*/
|
|
100
|
+
translations() {
|
|
101
|
+
const b = this, files = b.files, pd = { configurable: true, enumerable: true }
|
|
102
|
+
return new Proxy (this.#translations, {
|
|
103
|
+
*[Symbol.iterator](){ for (let l of files.locales()) yield [ l, b.texts4(l) ]},
|
|
104
|
+
ownKeys(){ return files.locales() }, getOwnPropertyDescriptor(){ return pd },
|
|
105
|
+
has(t,p) { return files.locales().includes(p) },
|
|
106
|
+
get(t,p) { return this[p] ?? b.texts4(p) },
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
module.exports = I18nBundle
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
const cds = require('../index')
|
|
2
|
+
const LOG = cds.log('i18n')
|
|
3
|
+
const { path, fs } = cds.utils
|
|
4
|
+
const { existsSync: exists } = fs
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Instances of this class are used to fetch and read i18n resources from the file system.
|
|
9
|
+
* The constructor fetches all i18n files from the existing i18n folders and adds them to
|
|
10
|
+
* the instance in a files-by-folders fashion.
|
|
11
|
+
* @example
|
|
12
|
+
* new cds.i18n.Files
|
|
13
|
+
* new cds.i18n.Files ({ roots: [ cds.home, cds.root+'/cap/sflight' ] })
|
|
14
|
+
*/
|
|
15
|
+
class I18nFiles {
|
|
16
|
+
|
|
17
|
+
constructor (options) {
|
|
18
|
+
|
|
19
|
+
// resolve options with defaults from cds.env.i18n config
|
|
20
|
+
const {i18n} = cds.env, {
|
|
21
|
+
file = i18n.file, basename = file,
|
|
22
|
+
folders = i18n.folders,
|
|
23
|
+
roots = [ i18n.root || cds.root, cds.home ],
|
|
24
|
+
model = cds.model,
|
|
25
|
+
} = options || i18n
|
|
26
|
+
|
|
27
|
+
// prepare the things we need below...
|
|
28
|
+
const files = this; this.#options = { roots, folders, basename }
|
|
29
|
+
const base = RegExp(`${basename}[._]`)
|
|
30
|
+
const _folders = I18nFiles.folders ??= {}
|
|
31
|
+
const _entries = I18nFiles.entries ??= {}
|
|
32
|
+
|
|
33
|
+
// fetch relatively specified i18n.folders in the neighborhood of sources...
|
|
34
|
+
const relative_folders = folders.filter (f => f[0] !== '/')
|
|
35
|
+
if (relative_folders.length) {
|
|
36
|
+
const leafs = model?.$sources.map(path.dirname) ?? roots, visited = {}
|
|
37
|
+
;[...new Set(leafs)].reverse() .forEach (function _visit (dir) {
|
|
38
|
+
if (dir in visited) return; else visited[dir] = true
|
|
39
|
+
LOG.debug ('searching for i18n files in the neighborhood of', dir)
|
|
40
|
+
// is there an i18n folder in the currently visited directory?
|
|
41
|
+
for (const each of relative_folders) {
|
|
42
|
+
const f = path.join(dir,each), _exists = _folders[f] ??= exists(f)
|
|
43
|
+
if (_exists && _add_entries4(f)) return // stop at first match from i18n.folders
|
|
44
|
+
}
|
|
45
|
+
// else recurse up the folder hierarchy till reaching package roots ...
|
|
46
|
+
if (leafs === roots || roots.includes(dir) || exists(path.join(dir,'package.json'))) return
|
|
47
|
+
else _visit (path.dirname(dir))
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// fetch fully specified i18n.folders, i.e., those starting with /
|
|
52
|
+
const specific_folders = folders.filter (f => f[0] === '/')
|
|
53
|
+
for (const f of specific_folders) {
|
|
54
|
+
const _exists = _folders[f] ??= exists(f)
|
|
55
|
+
_add_entries4 (_exists ? f : path.join(cds.root,f))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// helper to add matching files from found folder, if any
|
|
59
|
+
function _add_entries4 (f) {
|
|
60
|
+
const matches = (_entries[f] ??= fs.readdirSync(f)) .filter (f => f.match(base))
|
|
61
|
+
if (matches.length) return files[f] = matches
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Loads content from all files for the given locale.
|
|
68
|
+
* @returns {entries[]} An array of entries, one for each file found.
|
|
69
|
+
*/
|
|
70
|
+
content4 (locale, suffix = locale?.replace(/-/g,'_')) {
|
|
71
|
+
const content = [], cached = I18nFiles.content ??= {}
|
|
72
|
+
const _suffix = suffix ? '_'+ suffix : ''
|
|
73
|
+
for (let dir in this) {
|
|
74
|
+
const all = cached[dir] ??= this.load('.json',dir) || this.load('.csv',dir) || false
|
|
75
|
+
if (all) { if (locale in all) content.push (all[locale]); continue }
|
|
76
|
+
const props = this.load ('.properties', dir, _suffix)
|
|
77
|
+
if (props) content.push (props)
|
|
78
|
+
}
|
|
79
|
+
return content
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
load (ext, dir, _suffix='') {
|
|
83
|
+
const fn = `${this.basename}${_suffix}${ext}`; if (!this[dir].includes(fn)) return
|
|
84
|
+
const file = path.join (dir, fn)
|
|
85
|
+
try { switch (ext) {
|
|
86
|
+
case '.properties': return _load_properties(file);
|
|
87
|
+
case '.json': return _load_json(file);
|
|
88
|
+
case '.csv': return _load_csv(file)
|
|
89
|
+
}}
|
|
90
|
+
finally { LOG.debug ('read:', file) }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Determines the locales for which translation files and content are available.
|
|
96
|
+
* @returns {string[]}
|
|
97
|
+
*/
|
|
98
|
+
locales() {
|
|
99
|
+
return this.#locales ??= (()=>{
|
|
100
|
+
const unique_locales = new Set()
|
|
101
|
+
for (let [ folder, files ] of Object.entries(this)) {
|
|
102
|
+
for (let file of files) {
|
|
103
|
+
const { name, ext } = path.parse (file); switch (ext) {
|
|
104
|
+
case '.properties': unique_locales.add(/(?:_(\w+))?$/.exec(name)?.[1]||''); break
|
|
105
|
+
case '.json': for (let locale in _load_json(path.join(folder,file))) unique_locales.add(locale); break
|
|
106
|
+
case '.csv': return _load_csv (path.join(folder,file))[0].slice(1)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return [...unique_locales]
|
|
111
|
+
})()
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
#options
|
|
115
|
+
#locales
|
|
116
|
+
|
|
117
|
+
get basename(){ return this.#options.basename }
|
|
118
|
+
get options(){ return this.#options }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
const _load_properties = cds.load.properties
|
|
123
|
+
const _load_json = require
|
|
124
|
+
const _load_csv = file => {
|
|
125
|
+
const csv = cds.load.csv(file); if (!csv) return
|
|
126
|
+
const [ header, ...rows ] = csv, all = {}
|
|
127
|
+
header.slice(1).forEach ((lang,i) => {
|
|
128
|
+
const entries = all[lang] = {}
|
|
129
|
+
for (let row of rows) if (row[i]) entries[row[0]] = row[i]
|
|
130
|
+
})
|
|
131
|
+
return all
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
module.exports = I18nFiles
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const I18nBundle = require ('./bundles')
|
|
2
|
+
const I18nFiles = require ('./files')
|
|
3
|
+
const cds = require('../index')
|
|
4
|
+
|
|
5
|
+
class I18nFacade {
|
|
6
|
+
|
|
7
|
+
Facade = I18nFacade
|
|
8
|
+
Bundle = I18nBundle
|
|
9
|
+
Files = I18nFiles
|
|
10
|
+
|
|
11
|
+
/** Shortcuts to config options */
|
|
12
|
+
get folders() { return cds.env.i18n.folders }
|
|
13
|
+
get file() { return cds.env.i18n.file }
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The default bundle for runtime messages.
|
|
17
|
+
*/
|
|
18
|
+
get messages() {
|
|
19
|
+
// ensure we always find our factory defaults as fallback
|
|
20
|
+
const factory_defaults = cds.utils.path.resolve (__dirname,'../../_i18n')
|
|
21
|
+
const folders = [ ...cds.env.i18n.folders, factory_defaults ]
|
|
22
|
+
return super.messages = this.bundle4 ('messages', { folders })
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The default bundle for UI labels and texts.
|
|
28
|
+
*/
|
|
29
|
+
get labels() {
|
|
30
|
+
return super.labels = this.bundle4 (cds.context?.model || cds.model)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Lazily constructs, caches and returns a bundle for the given subject.
|
|
35
|
+
* @param {string|object} [file] - a CSN model, or a string used as the bundle's basename
|
|
36
|
+
* @param {object} [options] - additional options to pass to the bundle constructor
|
|
37
|
+
* @returns {I18nBundle}
|
|
38
|
+
*/
|
|
39
|
+
bundle4 (file, options) {
|
|
40
|
+
if (_is_string(file)) return super[file] ??= new I18nBundle ({ basename: file, ...options })
|
|
41
|
+
if (_is_model(file)) return _cached(file).texts ??= new I18nBundle ({ model: file })
|
|
42
|
+
else return new I18nBundle (options = file)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
// -----------------------------------------------------------------------------------------------
|
|
47
|
+
// following are convenience methods, rather useful in cds repl
|
|
48
|
+
|
|
49
|
+
files4 (options) {
|
|
50
|
+
if (typeof options === 'string') options = { roots: [ cds.home, cds.utils.path.resolve(cds.root,options) ] }
|
|
51
|
+
return new I18nFiles (options)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
folders4 (options) {
|
|
55
|
+
return Object.keys (this.files4(options))
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const _cached = m => m._cached ??= Object.defineProperty (m,'_cached',{writable:true}) && {}
|
|
60
|
+
const _is_model = x => typeof x === 'object' && '$sources' in x
|
|
61
|
+
const _is_string = x => typeof x === 'string'
|
|
62
|
+
|
|
63
|
+
module.exports = exports = new I18nFacade
|