@sap/cds 8.4.2 → 8.5.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.
Files changed (73) hide show
  1. package/CHANGELOG.md +42 -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/entities.js +1 -0
  7. package/lib/core/types.js +1 -1
  8. package/lib/dbs/cds-deploy.js +5 -2
  9. package/lib/env/defaults.js +7 -6
  10. package/lib/env/schemas/cds-rc.js +132 -22
  11. package/lib/i18n/bundles.js +111 -0
  12. package/lib/i18n/files.js +134 -0
  13. package/lib/i18n/index.js +63 -0
  14. package/lib/i18n/localize.js +101 -237
  15. package/lib/i18n/resources.js +150 -0
  16. package/lib/index.js +1 -0
  17. package/lib/log/format/aspects/cls.js +6 -1
  18. package/lib/log/format/json.js +1 -1
  19. package/lib/ql/CREATE.js +1 -0
  20. package/lib/ql/DELETE.js +1 -0
  21. package/lib/ql/DROP.js +1 -0
  22. package/lib/ql/INSERT.js +9 -8
  23. package/lib/ql/Query.js +18 -8
  24. package/lib/ql/SELECT.js +1 -0
  25. package/lib/ql/UPDATE.js +2 -1
  26. package/lib/ql/UPSERT.js +1 -1
  27. package/lib/ql/Whereable.js +3 -3
  28. package/lib/ql/cds-ql.js +12 -18
  29. package/lib/req/user.js +1 -0
  30. package/lib/req/validate.js +12 -3
  31. package/lib/srv/factory.js +2 -2
  32. package/lib/{auth → srv/middlewares/auth}/basic-auth.js +1 -1
  33. package/lib/{auth → srv/middlewares/auth}/dummy-auth.js +1 -1
  34. package/lib/srv/middlewares/auth/ias-auth.js +96 -0
  35. package/lib/{auth → srv/middlewares/auth}/index.js +2 -2
  36. package/lib/srv/middlewares/auth/jwt-auth.js +62 -0
  37. package/lib/{auth → srv/middlewares/auth}/mocked-users.js +1 -1
  38. package/lib/srv/middlewares/auth/xssec.js +7 -0
  39. package/lib/srv/middlewares/index.js +1 -1
  40. package/lib/utils/cds-utils.js +15 -19
  41. package/lib/utils/tar.js +2 -2
  42. package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +2 -2
  43. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +1 -0
  44. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +1 -1
  45. package/libx/_runtime/common/error/frontend.js +2 -6
  46. package/libx/_runtime/common/error/log.js +7 -8
  47. package/libx/_runtime/common/error/utils.js +3 -7
  48. package/libx/_runtime/common/generic/auth/capabilities.js +1 -1
  49. package/libx/_runtime/common/generic/input.js +41 -6
  50. package/libx/_runtime/common/i18n/index.js +8 -15
  51. package/libx/_runtime/common/utils/compareJson.js +10 -1
  52. package/libx/_runtime/common/utils/resolveView.js +1 -1
  53. package/libx/_runtime/fiori/lean-draft.js +77 -26
  54. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +5 -1
  55. package/libx/_runtime/messaging/kafka.js +1 -1
  56. package/libx/odata/ODataAdapter.js +2 -1
  57. package/libx/odata/index.js +3 -0
  58. package/libx/odata/middleware/batch.js +4 -0
  59. package/libx/odata/middleware/create.js +8 -6
  60. package/libx/odata/middleware/update.js +24 -21
  61. package/libx/odata/parse/afterburner.js +16 -3
  62. package/libx/odata/parse/grammar.peggy +24 -7
  63. package/libx/odata/parse/parser.js +1 -1
  64. package/libx/odata/utils/index.js +1 -0
  65. package/libx/odata/utils/postProcess.js +4 -1
  66. package/libx/rest/RestAdapter.js +2 -1
  67. package/libx/rest/middleware/error.js +0 -50
  68. package/package.json +1 -1
  69. package/lib/auth/ias-auth.js +0 -68
  70. package/lib/auth/ias-claims.js +0 -34
  71. package/lib/auth/jwt-auth.js +0 -70
  72. package/libx/_runtime/common/i18n/messages.properties +0 -99
  73. package/libx/_runtime/common/utils/require.js +0 -9
@@ -1,271 +1,135 @@
1
- const cds = require('..')
2
- const {existsSync, readdirSync} = require ('fs')
3
- const {join,dirname,resolve,parse,sep} = require ('path')
4
-
5
- const DEBUG = cds.debug('i18n')
6
- const _node_modules = cds.env.cdsc.moduleLookupDirectories.map(d => sep+d.slice(0, -1))
7
-
8
- module.exports = Object.assign (localize, {
9
- localize, lookup, bundles4, folders4, folder4, bundle4, files4, allLocales4
10
- })
11
-
12
- /**
13
- * Can be used like that:
14
- * @example cds.i18n.lookup('CreatedAt','de')
15
- */
16
- function lookup (key, locale, model = cds.context?.model || cds.model) {
17
- let bundle = bundle4 (model, locale)
18
- return bundle?.[key]
19
- }
1
+ const cds = require('..'), {i18n} = cds
20
2
 
21
3
  /**
22
- * Can be used like that:
23
- * @example cds.localize(edmx,'de')
4
+ * Fluent API to localize {i18n>...} placeholders in arbitrary strings.
5
+ * - All fluent methods are optional and can be chained in any order.
6
+ * @example
7
+ * let all = cds.localize ('<b>{i18n>CreatedBy}:</b> ...')
8
+ * .from (cds.model)
9
+ * .for ('de','en')
10
+ * .with (cds.i18n.labels)
11
+ * .with (cds.i18n.labels.translations4('de','en'))
12
+ * .using (s => s)
13
+ * [...all] //> [ [ 'de', '<b>Angelegt von:</b> ...' ], [ 'en', '<b>Created By:</b> ...' ] ]
24
14
  */
25
- function localize (aString, locale, model = cds.context?.model || cds.model, ext={}) {
15
+ class Localize {
26
16
 
27
- // support for legacy signature
28
- if (typeof aString === 'object') [ aString, model ] = [ model, aString ]
29
-
30
- // in case of multiple locales, return a generator
31
- if (Array.isArray(locale) || locale == 'all' || locale == '*') return (function*(){
32
- let localized, bundles = bundles4 (model, locale)
33
- if (bundles?.[Symbol.iterator]) for (let [lang,each] of bundles) {
34
- localized = _localize_with (each)
35
- yield [ localized, {lang} ]
36
- }
37
- if (!localized) yield [ aString, {lang:''} ]
38
- })()
17
+ constructor (input) {
18
+ if (input) this.input = input
19
+ }
39
20
 
40
- // otherwise return a single localized string
41
- let bundle = bundle4 (model, locale)
42
- return _localize_with (bundle)
21
+ from (model) {
22
+ this.model = model
23
+ return this
24
+ }
43
25
 
44
- function _localize_with (bundle) {
45
- if (!bundle || !aString) return aString
46
- if (typeof aString === 'object') aString = JSON.stringify(aString)
47
- const escape = aString.startsWith('<?xml') ? escapeXmlAttr : /^[{[]/.test(aString) ? escapeJson : v=>v
48
- return aString.replace (/{i18n>([^}]+)}/g, (_, key) => {
49
- const val = ext[key] || bundle[key]
50
- return !val ? key : escape(val)
51
- })
26
+ for (...locales) {
27
+ this.locales = Array.isArray(locales[0]) ? locales[0] : locales
28
+ return this
52
29
  }
53
- }
54
30
 
31
+ with (bundle, overlay) {
32
+ if (overlay) this.overlay = overlay
33
+ this.bundle = bundle
34
+ return this
35
+ }
55
36
 
37
+ using (replacer) {
38
+ this.replacer = replacer
39
+ return this
40
+ }
56
41
 
57
- /**
58
- * Returns all property bundles, i.e. one for each available translation language,
59
- * for the given model.
60
- */
61
- function bundles4 (model, locales = cds.env.i18n.languages) {
42
+ *[Symbol.iterator]() {
43
+ const { input, bundle = i18n.bundle4(this.model), overlay={}, replacer=s=>s } = this
44
+ const all = Object.entries (bundle.translations4?.(this.locales||'all') ?? bundle)
45
+ const placeholders = /{i18n>([^}]+)}/g
46
+ if (all.length) for (let [ lang, texts ] of all) yield [
47
+ lang, input.replace (placeholders, (_,k) => overlay[k] || replacer(texts[k]) || k)
48
+ ]
49
+ else yield [ '', input ]
50
+ }
51
+ }
62
52
 
63
- const folders = folders4 (model); if (folders.length === 0) return
64
- const {i18n} = cds.env
65
53
 
66
- if (locales.split) locales = locales.split(',')
67
- if (locales == 'all' || locales == '*') { // NOTE: using == to match 'all' as well as ['all']
68
- locales = allLocales4 (folders); if (!locales) return {}
69
- if (!locales.includes(i18n.fallback_bundle)) locales.push (i18n.fallback_bundle)
70
- }
54
+ // -----------------------------------------------------------------------------------------------
55
+ // Facade API
71
56
 
72
- DEBUG?.('Languages:', locales)
57
+ module.exports = exports = localize
73
58
 
74
- return (function*(){
75
- for (let each of locales) {
76
- let bundle = bundle4 (model, each); if(!bundle) continue
77
- DEBUG?.(bundle)
78
- yield [ each, bundle ]
79
- }
80
- })()
59
+ function localize (input) {
60
+ if (arguments.length > 1) return exports.legacy (...arguments)
61
+ return new Localize (input)
81
62
  }
82
63
 
83
-
84
- function files4 (folders_or_model) {
85
- const {i18n} = cds.env
86
- const folders = folders_or_model.map ? folders_or_model : folders4(folders_or_model)
87
- const files = folders.map (folder => readdirSync(folder)
88
- .filter (e => e.startsWith(i18n.file))
89
- .map(i18nFile => join (folder, i18nFile))
90
- ).flat()
91
- if (files.length === 0) {
92
- DEBUG?.('No languages for folders:', folders)
93
- return null
64
+ exports.edmx4 = service => new class extends Localize { get input(){
65
+ const model = this.model || cds.context?.model || cds.model
66
+ return super.input = cds.compile.to.edmx (model,{service})
67
+ }}
68
+
69
+ exports.edmx = edmx => {
70
+ if (typeof edmx === 'object') edmx = cds.compile.to.edmx (edmx)
71
+ const _xml_escapes = {
72
+ '"' : '&quot;',
73
+ '<' : '&lt;',
74
+ '>' : '&gt;',
75
+ '&' : '&amp;', // if not followed by amp; quot; lt; gt; apos; or #
76
+ '\n' : '&#xa;',
77
+ '\r' : '',
94
78
  }
95
- return files
79
+ const _2b_escaped = /["<>\n\r]|&(?!quot;|amp;|lt;|gt;|apos;|#)/g
80
+ const _xml_replacer = s => s?.replace (_2b_escaped, m => _xml_escapes[m])
81
+ return localize (edmx) .using (_xml_replacer)
96
82
  }
97
83
 
98
- /**
99
- * Return locales for all bundles found in given folders derived from .json, .properties or .csv files.
100
- *
101
- * TODO - .csv file handling seems to be questionable - do we need to check all .csv files additionally for locales ???
102
- */
103
- function allLocales4 (folders_or_model) {
104
- const files = files4(folders_or_model)
105
- if (!files) return null
106
- if (files[0].endsWith('.csv')) {
107
- return cds.load.csv (files[0])[0].slice(1)
108
- } else {
109
- const locales = new Set()
110
- files.forEach(file => {
111
- const parsed = parse(file)
112
- if (parsed.ext === '.json') {
113
- Object.keys(require(file)).forEach(locale => locales.add(locale))
114
- }
115
- else if (parsed.ext === '.properties') {
116
- const match = /_(\w+)$/.exec(parsed.name)
117
- if (match) locales.add(match[1])
118
- }
119
- })
120
- return Array.from(locales)
121
- }
84
+ exports.json = json => {
85
+ if (typeof json === 'object') json = JSON.stringify(json)
86
+ const _json_replacer = s => s?.replace(/"/g, '\\"')
87
+ return localize(json) .using (_json_replacer)
122
88
  }
123
89
 
124
- /**
125
- * Returns the effective bundle stack for the given language and model folders.
126
- * Expected bundle stack for languages en and '' + 2 model layers:
127
- [en] model/_i18n
128
- [] model/_i18n
129
- [en] model/node_modules/reuse-model/_i18n
130
- [] model/node_modules/reuse-model/_i18n
131
- */
132
- function bundle4 (model, locale) {
133
90
 
134
- if (typeof model === 'string') [ model, locale ] = [ cds.context?.model || cds.model, model ]
135
91
 
136
- const bundles = model.texts || Object.defineProperty (model,'texts',{value:{}}).texts
137
- if (locale in bundles) return bundles[locale]
92
+ // -----------------------------------------------------------------------------------------------
93
+ // Legacy API, not used anymore in @sap/cds...
138
94
 
139
- const folders = folders4(model)
140
- if (!folders.length) return bundles[locale] = {}
95
+ /** @deprecated */ exports.legacy = function (input, locales, model, ext) {
141
96
 
142
- const {i18n} = cds.env
143
- let bundle = Object.create(null)
144
- bundle.toJSON = jsonWithAllProps // allows JSON.stringify with all inherited props
97
+ // Support for legacy params signature with model as first argument, and edm string as third
98
+ if (typeof input === 'object') [ input, model ] = [ model, input ]; if (!input) return
99
+ if (typeof input !== 'string') input = JSON.stringify (input)
100
+ if (locales == '*') locales = 'all' // NOTE: '*' is deprecated; using == to match '*' and ['*']
145
101
 
146
- let locales = (
147
- locale === i18n.fallback_bundle ? [ i18n.fallback_bundle ] :
148
- locale === i18n.default_language ? [ i18n.fallback_bundle, i18n.default_language ] :
149
- [ i18n.fallback_bundle, i18n.default_language, locale ]
102
+ // Construct fluent API instance from arguments
103
+ const fluent = (
104
+ input[0] === '<' ? exports.edmx (input) :
105
+ input[0] === '{' ? exports.json (input) :
106
+ localize (input)
150
107
  )
151
- for (let each of locales) {
152
- const b = bundle = Object.create(bundle)
153
- for (let folder of folders) {
154
- const file = join (folder, i18n.file), suffix = each ? '_' + each : ''
155
- const next = bundle4[file + suffix] ??= (
156
- loadFromJSON (file, each) ||
157
- cds.load.properties (file + suffix.replace('-','_')) || // e.g. en-UK --> en_UK
158
- cds.load.properties (file + suffix.match(/\w+/)) || // e.g. en_UK --> en
159
- loadFromCSV (file, each)
160
- )
161
- Object.assign (b, next)
162
- }
163
- }
164
-
165
- return bundles[locale] = bundle
166
- }
167
-
168
- /**
169
- * Returns an array of all existing _i18n folders for the models
170
- * that are merged into the given one..
171
- */
172
- function folders4 (model) {
173
- if (model._i18nfolders) return model._i18nfolders
174
- // Order of model.$sources is expected to be sorted along usage levels, e.g.
175
- // foo/bar.cds
176
- // foo/node_modules/reuse-level-1/model.cds
177
- // foo/node_modules/reuse-level-2/model.cds
178
- const folders = [] // using an array here to not screw up the folder order
179
- const srcFolders = new Set ((model.$sources||[]).map(dirname))
180
- srcFolders.forEach(src => {
181
- const folder = folder4 (src)
182
- if (folder && !folders.includes(folder)) {
183
- folders.push(folder)
184
- }
185
- })
186
- Object.defineProperty (model, '_i18nfolders', {value:folders.reverse()})
187
- return folders
188
- }
189
-
190
- /**
191
- * Returns the location of an existing _i18n folder next to or in the
192
- * folder hierarchy above the given path, if any.
193
- */
194
- function folder4 (loc) {
195
- // already cached from a former lookup?
196
- if (loc in folder4) return folder4[loc]
197
- // check whether a <loc>/_i18n exists
198
- const {i18n} = cds.env
199
- for (let each of i18n.folders) {
200
- const f = join (loc, each)
201
- if (existsSync(f)) return folder4[loc] = f
202
- }
203
- //> no --> search up the folder hierarchy up to cds.root, cds.home, or some .../node_modules/<package>
204
- let next = dirname(loc)
205
- if (_node_modules.some(m => next.includes(m))) {
206
- if (_node_modules.some(m => next.endsWith(m))) return folder4[loc] = null
207
- } else {
208
- if (!(
209
- next.startsWith(cds.root) ||
210
- next.startsWith(cds.home) ||
211
- i18n.root && next.startsWith(i18n.root)
212
- )) return folder4[loc] = null
213
- }
214
- if (!next || next === loc) return folder4[loc] = null
215
- // console.debug(next)
216
- return folder4[loc] = folder4(next)
108
+ if (locales) fluent.locales = locales
109
+ if (model) fluent.model = model
110
+ if (ext) fluent.overlay = ext
111
+
112
+ // Return the first result if a single locale was requested, otherwise return all
113
+ if (!Array.isArray(locales) && locales != 'all')
114
+ for (let [,txt] of fluent) return txt
115
+ else return function*(){
116
+ for (let [lang,txt] of fluent) yield [ txt, {lang}]
117
+ }()
217
118
  }
218
119
 
219
-
220
- function loadFromJSON (res, lang=cds.env.i18n.default_language) {
221
- let cached = loadFromJSON[res]
222
- if (!cached) try {
223
- cached = loadFromJSON[res] = require (resolve (cds.root,res+'.json'))
224
- } catch(e) {
225
- if (e.code !== 'MODULE_NOT_FOUND') throw e
226
- else cached = loadFromJSON[res] = {}
227
- }
228
- return cached[lang] || cached[lang.match(/\w+/)?.[0]]
120
+ /** @deprecated */ exports.bundles4 = function (model, locales = cds.env.i18n.languages) {
121
+ const b = i18n.bundle4 (model)
122
+ const all = b.translations4 (locales)
123
+ return Object.entries (all)
229
124
  }
230
125
 
231
- function loadFromCSV (res, lang=cds.env.i18n.default_language) {
232
- let csv = cds.load.csv(res+'.csv'); if (!csv) return
233
- let [header, ...rows] = csv
234
- if (lang === '*') return header.slice(1).reduce ((all,lang,i) => {
235
- all[lang] = _bundle(i); return all
236
- },{})
237
- let col = header.indexOf (lang)
238
- if (col < 0) col = header.indexOf ((lang.match(/\w+/)||[])[0])
239
- if (col > 0) return _bundle (col)
240
- function _bundle (col) {
241
- const b={}; for (let row of rows) if (row[col]) b[row[0]] = row[col]
242
- return Object.defineProperty (b, '_source', {value:res+'.csv'+'#'+lang})
243
- }
244
- }
245
-
246
- // TODO use compiler API for XML escaping
247
- function escapeXmlAttr (str) {
248
- // first regex: replace & if not followed by apos; or quot; or gt; or lt; or amp; or #
249
- // Do not always escape > as it is a marker for {i18n>...} translated string values
250
- let result = str;
251
- if (typeof str === 'string') {
252
- result = str.replace(/&(?!(?:apos|quot|[gl]t|amp);|#)/g, '&amp;')
253
- .replace(/</g, '&lt;')
254
- .replace(/"/g, '&quot;')
255
- .replace(/\r\n|\n/g, '&#xa;');
256
- if (!result.startsWith('{i18n>')) result = result.replace(/>/g, '&gt;')
257
- }
258
- return result;
126
+ /** @deprecated */ exports.bundle4 = function (model, locale) {
127
+ if (typeof model === 'string') [ model, locale ] = [ cds.context?.model || cds.model, model ]
128
+ const b = i18n.bundle4 (model)
129
+ return b.texts4 (locale)
259
130
  }
260
131
 
261
- const escapeJson = str => str.replace(/"/g, '\\"')
262
-
263
- function jsonWithAllProps() {
264
- const res = {}
265
- for (let key in this) {
266
- if (typeof this[key] !== 'function') {
267
- res[key] = this[key]
268
- }
269
- }
270
- return res
132
+ /** @deprecated */ exports.folders4 = function (model) {
133
+ const b = i18n.bundle4 (model)
134
+ return Object.keys (b.files)
271
135
  }
@@ -0,0 +1,150 @@
1
+ const cds = require('..'), { path } = cds.utils
2
+ const LOG = cds.log('i18n')
3
+
4
+ /**
5
+ * Instances of this class are used to fetch and read i18n resources from the file system.
6
+ */
7
+ class I18nResources {
8
+
9
+ constructor (options) {
10
+ for (let each in options) super[each] = options[each]
11
+ }
12
+
13
+
14
+ /**
15
+ * The folders to search for i18n files in. By default a shortcut to i18n.folders
16
+ * config but can be specified in constructor.
17
+ */
18
+ get folders() {
19
+ return super.folders = cds.env.i18n.folders
20
+ }
21
+
22
+
23
+ /**
24
+ * Returns the files basename to read properties from; default is `'i18n'`.
25
+ * @returns {string}
26
+ */
27
+ get file() {
28
+ return super.file ??= cds.env.i18n.file
29
+ }
30
+
31
+
32
+ /**
33
+ * Fetches all i18n files matching {@link file basename} in {@link folders} up to {@link roots}.
34
+ * @returns {Record<string,string[]>} a dictionary of files by folders.
35
+ */
36
+ get files() {
37
+
38
+ // prepare the things we need below...
39
+ const { existsSync: exists, readdirSync: readdir } = cds.utils.fs
40
+ const _folders = I18nResources.folders ??= {}
41
+ const _entries = I18nResources.entries ??= {}
42
+ const basename = RegExp(`${this.file}[._]`)
43
+ const files_by_folders = {} // the result to be returned
44
+
45
+ // fetch relatively specified i18n.folders in the neighborhood of sources...
46
+ const relative_folders = this.folders.filter (f => f[0] !== '/')
47
+ if (relative_folders.length) {
48
+ const visited = {}, roots = this.roots, $sources = this.model?.$sources
49
+ const $sourcedirs = $sources ? [ ...new Set($sources.map(path.dirname)) ].reverse() : [ cds.home, cds.root ]
50
+ $sourcedirs.forEach (function _visit (dir) {
51
+ if (dir in visited) return; else visited[dir] = true
52
+ LOG.debug ('searching for i18n files in the neighborhood of', dir)
53
+ // is there an i18n folder in the currently visited directory?
54
+ for (let each of relative_folders) {
55
+ const f = path.join(dir,each), _exists = _folders[f] ??= exists(f)
56
+ if (_exists && _add_entries4(f)) return // stop at first match from i18n.folders
57
+ }
58
+ // else recurse up the folder hierarchy till reaching package roots ...
59
+ if (roots.includes(dir) || exists(path.join(dir,'package.json'))) return
60
+ else _visit (path.dirname(dir))
61
+ })
62
+ }
63
+
64
+ // fetch fully specified i18n.folders, i.e., those starting with /
65
+ const specific_folders = this.folders.filter (f => f[0] === '/')
66
+ for (let f of specific_folders) {
67
+ const _exists = _folders[f] ??= exists(f)
68
+ _add_entries4 (_exists ? f : path.join(cds.root,f))
69
+ }
70
+
71
+ // helper to add matching files from found folder, if any
72
+ function _add_entries4 (f) {
73
+ const files = (_entries[f] ??= readdir(f)) .filter (f => f.match(basename))
74
+ if (files.length) return files_by_folders[f] = files
75
+ }
76
+
77
+ // finally return the collected files by folders
78
+ super.folders = Object.keys (files_by_folders)
79
+ return super.files = files_by_folders
80
+ }
81
+
82
+
83
+ /**
84
+ * The root directories up to which to search for {@link files `i18n.files`}.
85
+ */
86
+ get roots() {
87
+ return super.roots = [ cds.env.i18n.root || cds.root ]
88
+ }
89
+
90
+
91
+ /**
92
+ * Returns all locales for which translations are available in this.{@link files}.
93
+ * @returns {string[]}
94
+ */
95
+ get locales() {
96
+ const unique_locales = new Set(), {path} = cds.utils
97
+ for (let [folder,files] of Object.entries(this.files)) {
98
+ for (let file of files) {
99
+ const { name, ext } = path.parse (file); switch (ext) {
100
+ case '.properties': unique_locales.add(/(?:_(\w+))?$/.exec(name)?.[1]||''); break
101
+ case '.json': for (let locale in _load_json(path.join(folder,file))) unique_locales.add(locale); break
102
+ case '.csv': return _load_csv (path.join(folder,file))[0].slice(1)
103
+ }
104
+ }
105
+ }
106
+ return super.locales = [...unique_locales]
107
+ }
108
+
109
+
110
+ /**
111
+ * Loads content from all files for the given locale.
112
+ * @returns {entries[]} An array of entries, one for each file found.
113
+ */
114
+ content4 (locale, suffix = locale?.replace(/-/g,'_')) {
115
+ const content = [], { file, files } = this
116
+ for (let folder in files) {
117
+ const all = this.content4[folder] ??= _load('json',folder) || _load('csv',folder)
118
+ if (all) { if (locale in all) content.push (all[locale]); continue }
119
+ const props = _load ('properties', folder, file + (suffix ? '_'+suffix : ''))
120
+ if (props) content.push (props)
121
+ }
122
+ function _load (kind, folder, basename = file) {
123
+ const entry = `${basename}.${kind}`; if (!files[folder].includes(entry)) return
124
+ const file = path.join (folder, entry)
125
+ try { switch (kind) {
126
+ case 'properties': return _load_properties (file)
127
+ case 'json': return _load_json (file)
128
+ case 'csv': return _load_csv (file)
129
+ }} finally {
130
+ LOG.debug ('read:', file)
131
+ }
132
+ }
133
+ return content
134
+ }
135
+ }
136
+
137
+
138
+ const _load_properties = cds.load.properties
139
+ const _load_json = require
140
+ const _load_csv = file => {
141
+ const csv = cds.load.csv(file); if (!csv) return
142
+ const [ header, ...rows ] = csv, all = {}
143
+ header.slice(1).forEach ((lang,i) => {
144
+ const entries = all[lang] = {}
145
+ for (let row of rows) if (row[i]) entries[row[0]] = row[i]
146
+ })
147
+ return all
148
+ }
149
+
150
+ module.exports = I18nResources
package/lib/index.js CHANGED
@@ -34,6 +34,7 @@ const cds = module.exports = global.cds = new class cds extends EventEmitter {
34
34
  get extend() { return super.extend = require('./compile/extend') }
35
35
  get deploy() { return super.deploy = require('./dbs/cds-deploy') }
36
36
  get localize() { return super.localize = require('./i18n/localize') }
37
+ get i18n() { return super.i18n = require('./i18n') }
37
38
 
38
39
  // Model Reflection, Builtin types and classes
39
40
  get entities() { return this.db?.entities || this.model?.entities }
@@ -6,4 +6,9 @@ function cls_aspect(/* module, level, args, toLog */) {
6
6
 
7
7
  cls_aspect.cf = () => [...cds.env.log.cls_custom_fields]
8
8
 
9
- module.exports = process.env.VCAP_SERVICES?.match(/"label":\s*"cloud-logging"/) ? cls_aspect : () => {}
9
+ const VCAP_SERVICES = process.env.VCAP_SERVICES ? JSON.parse(process.env.VCAP_SERVICES) : {}
10
+
11
+ module.exports =
12
+ VCAP_SERVICES['cloud-logging'] || VCAP_SERVICES['user-provided']?.find(e => e.tags.includes('cloud-logging'))
13
+ ? cls_aspect
14
+ : () => {}
@@ -22,7 +22,7 @@ const _is_custom_fields = (arg, custom_fields) => {
22
22
  return true
23
23
  }
24
24
 
25
- const _is_categories = arg => arg.categories && Array.isArray(arg.categories) && Object.keys(arg).length === 1
25
+ const _is_categories = arg => arg?.categories && Array.isArray(arg.categories) && Object.keys(arg).length === 1
26
26
 
27
27
  const _extract_custom_fields_and_categories = (args, toLog, custom_fields) => {
28
28
  if (args.length) {
package/lib/ql/CREATE.js CHANGED
@@ -1,5 +1,6 @@
1
1
  module.exports = class Query extends require('./Query') {
2
2
 
3
+ get kind() { return 'CREATE' }
3
4
  static _api() {
4
5
  return Object.assign((..._) => (new this).entity(..._), {
5
6
  entity: (..._) => (new this).entity(..._),
package/lib/ql/DELETE.js CHANGED
@@ -2,6 +2,7 @@ const Whereable = require('./Whereable')
2
2
 
3
3
  module.exports = class Query extends Whereable {
4
4
 
5
+ get kind() { return 'DELETE' }
5
6
  static _api() {
6
7
  return Object.assign ((..._) => (new this).from(..._), {
7
8
  from: (..._) => (new this).from(..._),
package/lib/ql/DROP.js CHANGED
@@ -1,4 +1,5 @@
1
1
  module.exports = class Query extends require('./Query') {
2
+ get kind() { return 'DROP' }
2
3
  static _api() {
3
4
  return Object.assign ((e) => (new this).entity(e), {
4
5
  entity: (e) => (new this).entity(e),
package/lib/ql/INSERT.js CHANGED
@@ -1,5 +1,6 @@
1
1
  module.exports = class Query extends require('./Query') {
2
2
 
3
+ get kind() { return 'INSERT' }
3
4
  static _api() {
4
5
  return Object.assign ((..._) => (new this).entries(..._), {
5
6
  into: (..._) => (new this).into(..._),
@@ -7,7 +8,7 @@ module.exports = class Query extends require('./Query') {
7
8
  }
8
9
 
9
10
  into (entity, ...data) {
10
- this[this.cmd].into = this._target4 (...arguments) // supporting tts
11
+ this._.into = this._target4 (...arguments) // supporting tts
11
12
  if (data.length) this.entries(...data)
12
13
  return this
13
14
  }
@@ -15,21 +16,21 @@ module.exports = class Query extends require('./Query') {
15
16
  entries (...x) {
16
17
  if (!x.length) return this
17
18
  if (x[0].SELECT) return this.from(x[0])
18
- this[this.cmd].entries = is_array(x[0]) ? x[0] : x
19
+ this._.entries = is_array(x[0]) ? x[0] : x
19
20
  return this
20
21
  }
21
22
  columns (...x) {
22
- this[this.cmd].columns = is_array(x[0]) ? x[0] : x
23
+ this._.columns = is_array(x[0]) ? x[0] : x
23
24
  return this
24
25
  }
25
26
  values (...x) {
26
- this[this.cmd].values = is_array(x[0]) ? x[0] : x
27
+ this._.values = is_array(x[0]) ? x[0] : x
27
28
  return this
28
29
  }
29
30
  rows (...rows) {
30
31
  if (is_array(rows[0]) && is_array(rows[0][0])) rows = rows[0]
31
32
  if (!is_array(rows[0])) this._expected `Arguments ${{rows}} to be an array of arrays`
32
- this[this.cmd].rows = rows
33
+ this._.rows = rows
33
34
  return this
34
35
  }
35
36
 
@@ -39,7 +40,7 @@ module.exports = class Query extends require('./Query') {
39
40
  if (query.name || typeof query === 'string') query = SELECT.from(query)
40
41
  else this._expected `${{query}} to be a CQN {SELECT} query object`
41
42
  }
42
- this[this.cmd].as = query // REVISIT: should we also change CSN spec, and adopt db service impls?
43
+ this._.as = query // REVISIT: should we also change CSN spec, and adopt db service impls?
43
44
  return this
44
45
  }
45
46
 
@@ -53,8 +54,8 @@ module.exports = class Query extends require('./Query') {
53
54
  return super.valueOf('INSERT INTO')
54
55
  }
55
56
 
56
- get _target_ref(){ return this.INSERT.into }
57
+ get _target_ref(){ return this._.into }
57
58
  }
58
59
 
59
60
  const is_array = Array.isArray
60
- let INSERT_as_SELECT
61
+ let INSERT_as_SELECT