@sap/cds 8.4.1 → 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.
Files changed (71) hide show
  1. package/CHANGELOG.md +41 -1
  2. package/_i18n/messages.properties +99 -0
  3. package/bin/serve.js +2 -2
  4. package/lib/compile/cdsc.js +9 -4
  5. package/lib/compile/to/srvinfo.js +4 -4
  6. package/lib/core/classes.js +5 -1
  7. package/lib/core/entities.js +1 -0
  8. package/lib/core/types.js +1 -1
  9. package/lib/dbs/cds-deploy.js +4 -1
  10. package/lib/env/defaults.js +7 -6
  11. package/lib/env/schemas/cds-rc.js +132 -22
  12. package/lib/i18n/bundles.js +111 -0
  13. package/lib/i18n/files.js +134 -0
  14. package/lib/i18n/index.js +63 -0
  15. package/lib/i18n/localize.js +101 -237
  16. package/lib/i18n/resources.js +150 -0
  17. package/lib/index.js +1 -0
  18. package/lib/log/format/aspects/cls.js +6 -1
  19. package/lib/log/format/json.js +1 -1
  20. package/lib/ql/CREATE.js +1 -0
  21. package/lib/ql/DELETE.js +1 -0
  22. package/lib/ql/DROP.js +1 -0
  23. package/lib/ql/INSERT.js +9 -8
  24. package/lib/ql/Query.js +18 -8
  25. package/lib/ql/SELECT.js +1 -0
  26. package/lib/ql/UPDATE.js +2 -1
  27. package/lib/ql/UPSERT.js +1 -1
  28. package/lib/ql/Whereable.js +3 -3
  29. package/lib/ql/cds-ql.js +12 -18
  30. package/lib/req/user.js +1 -0
  31. package/lib/req/validate.js +12 -3
  32. package/lib/srv/factory.js +2 -2
  33. package/lib/{auth → srv/middlewares/auth}/basic-auth.js +1 -1
  34. package/lib/{auth → srv/middlewares/auth}/dummy-auth.js +1 -1
  35. package/lib/srv/middlewares/auth/ias-auth.js +96 -0
  36. package/lib/{auth → srv/middlewares/auth}/index.js +2 -2
  37. package/lib/srv/middlewares/auth/jwt-auth.js +62 -0
  38. package/lib/{auth → srv/middlewares/auth}/mocked-users.js +1 -1
  39. package/lib/srv/middlewares/auth/xssec.js +7 -0
  40. package/lib/srv/middlewares/index.js +1 -1
  41. package/lib/utils/cds-utils.js +15 -19
  42. package/lib/utils/tar.js +2 -2
  43. package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +2 -2
  44. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +1 -0
  45. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +1 -1
  46. package/libx/_runtime/common/error/frontend.js +2 -6
  47. package/libx/_runtime/common/error/log.js +7 -8
  48. package/libx/_runtime/common/error/utils.js +3 -7
  49. package/libx/_runtime/common/generic/auth/capabilities.js +1 -1
  50. package/libx/_runtime/common/generic/input.js +41 -6
  51. package/libx/_runtime/common/i18n/index.js +8 -15
  52. package/libx/_runtime/common/utils/compareJson.js +10 -1
  53. package/libx/_runtime/common/utils/resolveView.js +1 -1
  54. package/libx/_runtime/fiori/lean-draft.js +77 -26
  55. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +5 -1
  56. package/libx/_runtime/messaging/kafka.js +1 -1
  57. package/libx/odata/index.js +3 -0
  58. package/libx/odata/middleware/create.js +8 -6
  59. package/libx/odata/middleware/update.js +24 -21
  60. package/libx/odata/parse/afterburner.js +15 -2
  61. package/libx/odata/parse/grammar.peggy +24 -7
  62. package/libx/odata/parse/parser.js +1 -1
  63. package/libx/odata/utils/postProcess.js +4 -1
  64. package/libx/rest/RestAdapter.js +2 -1
  65. package/libx/rest/middleware/error.js +0 -50
  66. package/package.json +1 -1
  67. package/lib/auth/ias-auth.js +0 -68
  68. package/lib/auth/ias-claims.js +0 -34
  69. package/lib/auth/jwt-auth.js +0 -70
  70. package/libx/_runtime/common/i18n/messages.properties +0 -99
  71. 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