@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.
- package/CHANGELOG.md +42 -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 +5 -2
- 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/ODataAdapter.js +2 -1
- package/libx/odata/index.js +3 -0
- package/libx/odata/middleware/batch.js +4 -0
- package/libx/odata/middleware/create.js +8 -6
- package/libx/odata/middleware/update.js +24 -21
- package/libx/odata/parse/afterburner.js +16 -3
- package/libx/odata/parse/grammar.peggy +24 -7
- package/libx/odata/parse/parser.js +1 -1
- package/libx/odata/utils/index.js +1 -0
- 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
package/lib/i18n/localize.js
CHANGED
|
@@ -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
|
-
*
|
|
23
|
-
*
|
|
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
|
-
|
|
15
|
+
class Localize {
|
|
26
16
|
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
21
|
+
from (model) {
|
|
22
|
+
this.model = model
|
|
23
|
+
return this
|
|
24
|
+
}
|
|
43
25
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
67
|
-
|
|
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
|
-
|
|
57
|
+
module.exports = exports = localize
|
|
73
58
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
+
'"' : '"',
|
|
73
|
+
'<' : '<',
|
|
74
|
+
'>' : '>',
|
|
75
|
+
'&' : '&', // if not followed by amp; quot; lt; gt; apos; or #
|
|
76
|
+
'\n' : '
',
|
|
77
|
+
'\r' : '',
|
|
94
78
|
}
|
|
95
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
137
|
-
|
|
92
|
+
// -----------------------------------------------------------------------------------------------
|
|
93
|
+
// Legacy API, not used anymore in @sap/cds...
|
|
138
94
|
|
|
139
|
-
|
|
140
|
-
if (!folders.length) return bundles[locale] = {}
|
|
95
|
+
/** @deprecated */ exports.legacy = function (input, locales, model, ext) {
|
|
141
96
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
[
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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, '&')
|
|
253
|
-
.replace(/</g, '<')
|
|
254
|
-
.replace(/"/g, '"')
|
|
255
|
-
.replace(/\r\n|\n/g, '
');
|
|
256
|
-
if (!result.startsWith('{i18n>')) result = result.replace(/>/g, '>')
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
-
|
|
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
|
+
: () => {}
|
package/lib/log/format/json.js
CHANGED
|
@@ -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
|
|
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
package/lib/ql/DELETE.js
CHANGED
package/lib/ql/DROP.js
CHANGED
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
|
|
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
|
|
19
|
+
this._.entries = is_array(x[0]) ? x[0] : x
|
|
19
20
|
return this
|
|
20
21
|
}
|
|
21
22
|
columns (...x) {
|
|
22
|
-
this
|
|
23
|
+
this._.columns = is_array(x[0]) ? x[0] : x
|
|
23
24
|
return this
|
|
24
25
|
}
|
|
25
26
|
values (...x) {
|
|
26
|
-
this
|
|
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
|
|
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
|
|
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.
|
|
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
|