@sap/cds-dk 4.3.3 → 4.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 +62 -1
- package/bin/activate.js +1 -2
- package/bin/deploy/index.js +1 -1
- package/bin/deploy/to-sqlite.js +2 -2
- package/bin/extend.js +2 -4
- package/bin/help.js +1 -1
- package/bin/login.js +7 -8
- package/bin/logout.js +1 -2
- package/bin/repl.js +1 -1
- package/bin/watch.js +12 -8
- package/lib/client/activate.js +243 -0
- package/lib/client/auth_manager.js +115 -0
- package/lib/client/base_command.js +55 -0
- package/lib/client/extend.js +179 -0
- package/lib/client/helper/cf_helper.js +186 -0
- package/lib/client/helper/dependency_helper.js +26 -0
- package/lib/client/helper/env_helper.js +4 -0
- package/lib/client/helper/fs_helper.js +83 -0
- package/lib/client/helper/logging.js +37 -0
- package/lib/client/helper/question.js +50 -0
- package/lib/client/helper/string_helper.js +53 -0
- package/lib/client/helper/url_helper.js +17 -0
- package/lib/client/index.js +14 -0
- package/lib/client/params.js +435 -0
- package/lib/client/settings_manager.js +526 -0
- package/lib/import/index.js +17 -38
- package/lib/import/message.js +6 -7
- package/lib/import/v2parser.js +340 -64
- package/lib/import/v4parser.js +346 -59
- package/lib/init/constants.js +1 -1
- package/lib/init/index.js +13 -10
- package/lib/init/template/data/index.js +38 -46
- package/lib/init/template/hana/index.js +2 -2
- package/lib/init/template/lint/files/.eslint/tests/no-entity-moo/fixed/schema.cds +4 -0
- package/lib/init/template/lint/files/.eslint/tests/no-entity-moo/invalid/schema.cds +4 -0
- package/lib/init/template/lint/files/.eslint/tests/no-entity-moo/test.js +10 -0
- package/lib/init/template/lint/files/.eslint/tests/no-entity-moo/valid/schema.cds +6 -0
- package/lib/init/template/lint/index.js +8 -6
- package/lib/init/template/mta/projectReader.js +8 -5
- package/lib/init/template/nodejs/files/.vscode/extensions.json +1 -1
- package/lib/init/template/nodejs/index.js +1 -8
- package/lib/init/util/logger.js +1 -1
- package/lib/lint/Linter.js +82 -65
- package/lib/lint/io.js +9 -33
- package/npm-shrinkwrap.json +6327 -11301
- package/package.json +6 -7
- package/lib/init/template/lint/files/.eslint/tests/no-entity-moo.test.js +0 -34
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
const os = require('os');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { fse } = require('@sap/cds-foss');
|
|
4
|
+
|
|
5
|
+
const requireGlobal = require('./helper/dependency_helper');
|
|
6
|
+
const keytar = requireGlobal('keytar');
|
|
7
|
+
|
|
8
|
+
const { getAppFromSuggestions, getAppName, getSubdomain } = require('./helper/cf_helper');
|
|
9
|
+
const isBas = require('./helper/env_helper');
|
|
10
|
+
const Logger = require('./helper/logging');
|
|
11
|
+
const Question = require('./helper/question');
|
|
12
|
+
const { defaultSchema, extractSubdomain, schemaRegex } = require('./helper/url_helper');
|
|
13
|
+
const { ParamCollection, Persistence } = require('./params');
|
|
14
|
+
|
|
15
|
+
const CONFIG_SUBDIRS = {
|
|
16
|
+
linux: '.config',
|
|
17
|
+
darwin: path.join('Library', 'Preferences'),
|
|
18
|
+
win32: 'AppData'
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const TOKEN_STORAGE_DESC = {
|
|
22
|
+
plain: 'plain-text storage',
|
|
23
|
+
keyring: 'keyring'
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const KEYRING_DESC = {
|
|
27
|
+
linux: 'libsecret',
|
|
28
|
+
darwin: 'Keychain',
|
|
29
|
+
win32: 'Credential Vault'
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const MTX_FULLY_QUALIFIED = 'com.sap.cds.mtx';
|
|
33
|
+
const OAUTH_PATH = '/mtx/v1/oauth/token';
|
|
34
|
+
|
|
35
|
+
const SETTINGS_DIR = path.join(os.homedir(), CONFIG_SUBDIRS[os.platform()] || '', MTX_FULLY_QUALIFIED);
|
|
36
|
+
const SETTINGS_FILE = 'projects.json';
|
|
37
|
+
const AUTH_FILE = 'auth.json';
|
|
38
|
+
|
|
39
|
+
const CONFIG = {
|
|
40
|
+
paths: {
|
|
41
|
+
settings: path.join(SETTINGS_DIR, SETTINGS_FILE),
|
|
42
|
+
auth: path.join(SETTINGS_DIR, AUTH_FILE)
|
|
43
|
+
},
|
|
44
|
+
keyringDesignation: KEYRING_DESC[os.platform()] || 'not supported'
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
function other(tokenStorage) {
|
|
48
|
+
if (!tokenStorage) {
|
|
49
|
+
return tokenStorage;
|
|
50
|
+
}
|
|
51
|
+
return tokenStorage === 'plain'
|
|
52
|
+
? 'keyring'
|
|
53
|
+
: 'plain';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function getProjectFolder(params) {
|
|
57
|
+
const relativePath = params.get('projectFolder') || params.get('directory') || (
|
|
58
|
+
params.has('subdomain') && fse.existsSync(path.resolve(params.get('subdomain')))
|
|
59
|
+
? params.get('subdomain')
|
|
60
|
+
: '.'
|
|
61
|
+
);
|
|
62
|
+
return fse.realpathSync(relativePath);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function getAppUrlAndSubdomainFromSuggestions(params) {
|
|
66
|
+
const app = await getAppFromSuggestions();
|
|
67
|
+
if (!app) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
params.set('appUrl', app.url);
|
|
72
|
+
|
|
73
|
+
if (!params.has('subdomain')) {
|
|
74
|
+
params.set('subdomain', await getSubdomain(app.name));
|
|
75
|
+
if (params.has('subdomain')) {
|
|
76
|
+
Logger.log(`Subdomain determined from CF app environment: ${params.get('subdomain')}`);
|
|
77
|
+
} else {
|
|
78
|
+
Logger.log('Failed to determine subdomain from CF app environment');
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function getTokenBaseUrl(params, renewUrl) {
|
|
84
|
+
if (params.has('tokenUrl') && !renewUrl) {
|
|
85
|
+
return params.get('tokenUrl');
|
|
86
|
+
}
|
|
87
|
+
if (!params.has('appUrl')) {
|
|
88
|
+
throw new Error('Failed to determine token URL: app URL not given');
|
|
89
|
+
}
|
|
90
|
+
const baseUrl = new URL(params.get('appUrl'));
|
|
91
|
+
baseUrl.pathname = baseUrl.pathname.replace(/\/?$/, OAUTH_PATH);
|
|
92
|
+
return baseUrl.toString();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function getKeyringAccountName(params) {
|
|
96
|
+
return `${params.get('appUrl')}|${params.get('subdomain')}`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
function logConfigPaths() {
|
|
101
|
+
Logger.debug(`Settings are stored in ${CONFIG.paths.settings}`);
|
|
102
|
+
Logger.debug(`Authentication data is stored in ${CONFIG.paths.auth} or keyring ('${CONFIG.keyringDesignation}' on this platform)`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function notifyLoggedIn(params) {
|
|
106
|
+
return params.get('renewLogin')
|
|
107
|
+
? Logger.log.bind(Logger)
|
|
108
|
+
: Logger.debug.bind(Logger);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function emptyData(all) {
|
|
112
|
+
return all ? {} : new ParamCollection();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
class SettingsManager {
|
|
116
|
+
|
|
117
|
+
static init() {
|
|
118
|
+
logConfigPaths();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
static get config() {
|
|
122
|
+
return CONFIG;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Call with 'projectFolder' param as a resolved path.
|
|
126
|
+
static async saveSettings(params) {
|
|
127
|
+
Logger.debug(`Saving settings for project ${params.get('projectFolder')}`);
|
|
128
|
+
if (params.has('token') && ! params.has('tokenStorage')) {
|
|
129
|
+
Logger.log('Note: the authentication token is not saved by default. To save the token for later commands, please run `cds login`.');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
await this._saveToFile(params.get('projectFolder'), params.toValueMap(Persistence.setting));
|
|
133
|
+
|
|
134
|
+
if (!params.has('token')) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (params.get('clearOtherTokenStorage')) {
|
|
139
|
+
// Delete token from the other storage.
|
|
140
|
+
params.delete('clearOtherTokenStorage');
|
|
141
|
+
await this.deleteToken(params, other(params.get('tokenStorage')));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (params.get('tokenStorage') === 'plain') {
|
|
145
|
+
await this._saveAuthToFile(params, params.toValueMap(Persistence.auth));
|
|
146
|
+
} else if (params.get('tokenStorage') === 'keyring') {
|
|
147
|
+
await this._saveAuthToKeyring(params, params.toValueMap(Persistence.auth));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
static async loadAndMergeSettings(params, logout = false) {
|
|
152
|
+
const projectFolder = getProjectFolder(params);
|
|
153
|
+
params.set('projectFolder', projectFolder);
|
|
154
|
+
|
|
155
|
+
Logger.debug(`Loading settings for project ${projectFolder}`);
|
|
156
|
+
const loadedSettings = await this._loadFromFile(projectFolder);
|
|
157
|
+
Logger.debug(`Loaded project settings: ${loadedSettings.format()}`);
|
|
158
|
+
|
|
159
|
+
params.merge(loadedSettings);
|
|
160
|
+
|
|
161
|
+
await this.updateUrls(params, logout);
|
|
162
|
+
await this.checkKeytar(params, logout);
|
|
163
|
+
await this.addAuth(params, logout);
|
|
164
|
+
Logger.debug(`Effective project settings: ${params.format()}`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
static async updateUrls(params, logout) {
|
|
168
|
+
async function addAppUrlAndSubdomain() {
|
|
169
|
+
if (params.has('appUrl') && !schemaRegex.test(params.get('appUrl'))) {
|
|
170
|
+
params.set('appUrl', defaultSchema + params.get('appUrl'));
|
|
171
|
+
Logger.debug(`Added schema to app URL: ${params.get('appUrl')}`);
|
|
172
|
+
}
|
|
173
|
+
if (!logout) {
|
|
174
|
+
if (!params.has('appUrl')) {
|
|
175
|
+
await getAppUrlAndSubdomainFromSuggestions(params);
|
|
176
|
+
} else if (!params.has('subdomain')) {
|
|
177
|
+
Logger.debug('Subdomain not given');
|
|
178
|
+
const appName = await getAppName(params.get('appUrl'));
|
|
179
|
+
if (appName) {
|
|
180
|
+
params.set('subdomain', await getSubdomain(appName));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (!params.has('appUrl')) {
|
|
185
|
+
throw new Error('App URL not given. Please specify it or log in to Cloud Foundry and repeat this command.');
|
|
186
|
+
}
|
|
187
|
+
if (!params.has('subdomain')) {
|
|
188
|
+
params.set('subdomain', extractSubdomain(params.get('appUrl')));
|
|
189
|
+
if (!params.has('subdomain')) {
|
|
190
|
+
throw new Error('Subdomain not given');
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
function renewTokenUrl() {
|
|
195
|
+
const renewUrl = params.get('appUrl') !== params.get('appUrl');
|
|
196
|
+
if (!params.get('tokenUrl') || renewUrl) {
|
|
197
|
+
params.set('tokenUrl', getTokenBaseUrl(params, renewUrl));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
await addAppUrlAndSubdomain();
|
|
202
|
+
|
|
203
|
+
if (!logout) {
|
|
204
|
+
renewTokenUrl();
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
static async checkKeytar(params, logout) {
|
|
209
|
+
if (isBas()) {
|
|
210
|
+
if (params.get('tokenStorage') === 'keyring') {
|
|
211
|
+
Logger.log(`NOTE: Keyring storage not supported on SAP Business Application Studio. Switching to plain-text storage.`);
|
|
212
|
+
params.set('tokenStorage', 'plain');
|
|
213
|
+
}
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
if (params.get('username') || keytar) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
if (params.get('tokenStorage') === 'keyring') {
|
|
220
|
+
throw new Error('Keyring storage requested but keytar not installed. Please run `npm install -g keytar` or ' +
|
|
221
|
+
'switch to plain-text storage by adding `--plain` (discouraged).');
|
|
222
|
+
}
|
|
223
|
+
if (params.get('renewLogin') || logout) {
|
|
224
|
+
Logger.log('Disabling keyring functionality: \'keytar\' module not found. Run `npm install -g keytar` to install it.');
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
static async addAuth(params, logout) {
|
|
229
|
+
async function addPassword() {
|
|
230
|
+
params.set('password', await Question.askQuestion('Password: ', undefined, true));
|
|
231
|
+
if (params.get('password') === '') {
|
|
232
|
+
throw new Error('Password cannot be empty');
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
async function addClientSecret() {
|
|
236
|
+
params.set('clientsecret', await Question.askQuestion('clientsecret: ', undefined, true));
|
|
237
|
+
if (params.get('clientsecret') === '') {
|
|
238
|
+
throw new Error('clientsecret cannot be empty');
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
async function addPasscode() {
|
|
242
|
+
const prompt = `Passcode${params.get('passcodeUrl') ? ' (visit ' + params.get('passcodeUrl') + ' to generate)' : ''}: `;
|
|
243
|
+
params.set('passcode', await Question.askQuestion(prompt, undefined, true));
|
|
244
|
+
console.log(); // DON'T: process.stdout.write('\n');
|
|
245
|
+
if (params.get('passcode') === '') {
|
|
246
|
+
throw new Error('Passcode cannot be empty');
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
function setRelevantAuth(auth) {
|
|
250
|
+
if (!(auth.keyring.has('tokenStorage') || auth.plain.has('tokenStorage'))) {
|
|
251
|
+
// Saved auth data not present.
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
let storage = params.get('tokenStorage');
|
|
255
|
+
if (auth.keyring.has('tokenStorage') && auth.plain.has('tokenStorage')) {
|
|
256
|
+
// Both storage places contain data: retrieve from selected or keyring.
|
|
257
|
+
storage = storage || 'keyring';
|
|
258
|
+
Logger.debug('WARNING: authentication data found in both kinds of storage. ' +
|
|
259
|
+
`Using data from ${TOKEN_STORAGE_DESC[storage]}; other storage will be cleared when next saving.`);
|
|
260
|
+
|
|
261
|
+
params.merge(auth[storage]);
|
|
262
|
+
params.set('clearOtherTokenStorage', true);
|
|
263
|
+
|
|
264
|
+
} else if (storage && auth[other(storage)].has('tokenStorage')) {
|
|
265
|
+
// Selected storage contains no data, but other one does: retrieve from other storage, but earmark migration to selected one.
|
|
266
|
+
Logger.debug(`Using authentication data from ${TOKEN_STORAGE_DESC[other(storage)]}; will be migrated to other storage on save.`);
|
|
267
|
+
|
|
268
|
+
params.merge(auth[other(storage)]);
|
|
269
|
+
params.set('tokenStorage', storage);
|
|
270
|
+
params.set('clearOtherTokenStorage', true);
|
|
271
|
+
|
|
272
|
+
} else {
|
|
273
|
+
// One storage contains data, and there's no conflict with selection.
|
|
274
|
+
const storage = auth.keyring.get('tokenStorage') || auth.plain.get('tokenStorage');
|
|
275
|
+
Logger.debug(`Using authentication data from ${TOKEN_STORAGE_DESC[storage]}.`);
|
|
276
|
+
|
|
277
|
+
params.merge(auth[storage]);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
Logger.debug(`Consolidating authentication data for project ${params.get('projectFolder')}`);
|
|
282
|
+
|
|
283
|
+
if (params.has('username')) {
|
|
284
|
+
if (!params.get('password') && !logout) {
|
|
285
|
+
await addPassword();
|
|
286
|
+
}
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (params.get('clientid') && !params.get('clientsecret') && !logout) {
|
|
291
|
+
await addClientSecret();
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const auth = {
|
|
295
|
+
plain: await this._loadAuthFromFile(params.get('appUrl'), params.get('subdomain')),
|
|
296
|
+
keyring: await this._loadAuthFromKeyring(params.get('appUrl'), params.get('subdomain'))
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
setRelevantAuth(auth);
|
|
300
|
+
|
|
301
|
+
if (!params.has('token') && !params.has('passcode') && !logout) {
|
|
302
|
+
await addPasscode();
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
static async deleteToken(params, fromStorage) {
|
|
307
|
+
const allParams = params.clone();
|
|
308
|
+
await this.loadAndMergeSettings(allParams, true);
|
|
309
|
+
|
|
310
|
+
const target = `URL ${allParams.get('appUrl')}, subdomain '${allParams.get('subdomain')}'`;
|
|
311
|
+
Logger.debug(`Deleting authentication data for ${target}`);
|
|
312
|
+
|
|
313
|
+
const deleteBoth = !fromStorage && allParams.get('tokenStorage') && allParams.get('clearOtherTokenStorage');
|
|
314
|
+
|
|
315
|
+
fromStorage = fromStorage || allParams.get('tokenStorage');
|
|
316
|
+
if (!fromStorage) {
|
|
317
|
+
Logger.log(`No authentication data found for ${target}`);
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (fromStorage === 'plain' || deleteBoth) {
|
|
322
|
+
await this._saveAuthToFile(allParams, null);
|
|
323
|
+
}
|
|
324
|
+
if (fromStorage === 'keyring' || deleteBoth) {
|
|
325
|
+
await this._saveAuthToKeyring(allParams, null);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
static async deleteSettingsWithoutToken(params) {
|
|
330
|
+
const projectFolder = getProjectFolder(params);
|
|
331
|
+
Logger.debug(`Deleting settings for project ${projectFolder}`);
|
|
332
|
+
await this._saveToFile(projectFolder, null);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
static async deleteInvalid() {
|
|
336
|
+
Logger.log('Deleting invalid project settings');
|
|
337
|
+
|
|
338
|
+
const settingsByFolder = await this._loadFromFile(undefined);
|
|
339
|
+
const deletionFolders = [];
|
|
340
|
+
const deletionUrlsAndSubdomains = new Set();
|
|
341
|
+
|
|
342
|
+
Object.entries(settingsByFolder)
|
|
343
|
+
.filter(entry => ! fse.pathExistsSync(entry[0]))
|
|
344
|
+
.forEach(entry => {
|
|
345
|
+
delete settingsByFolder[entry[0]];
|
|
346
|
+
deletionFolders.push(entry[0]);
|
|
347
|
+
deletionUrlsAndSubdomains.add(entry[1].appUrl + '|' + entry[1].subdomain);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
await this._saveAllSettingsToFile(settingsByFolder);
|
|
351
|
+
|
|
352
|
+
if (deletionFolders.length) {
|
|
353
|
+
for (const urlAndSubdomain of deletionUrlsAndSubdomains.values()) {
|
|
354
|
+
const appUrl = urlAndSubdomain.replace(/\|.*/, '');
|
|
355
|
+
const subdomain = urlAndSubdomain.replace(/.*\|/, '');
|
|
356
|
+
const urlReference = Object.values(settingsByFolder).find(settings => settings.appUrl === appUrl && settings.subdomain === subdomain);
|
|
357
|
+
if (!urlReference) {
|
|
358
|
+
await this.deleteToken(new ParamCollection({ appUrl, subdomain }));
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
Logger.log(`Deleted settings for nonexistent project folders:${deletionFolders.map(folder => ' ' + folder)}`);
|
|
362
|
+
} else {
|
|
363
|
+
Logger.log('All settings seem valid');
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
static async _saveToFile(projectFolder, paramValues) {
|
|
368
|
+
const paramValuesByFolder = await this._loadFromFile(undefined);
|
|
369
|
+
if (paramValues !== null) {
|
|
370
|
+
paramValuesByFolder[projectFolder] = paramValues;
|
|
371
|
+
await this._saveAllSettingsToFile(paramValuesByFolder);
|
|
372
|
+
Logger.debug(`Saved project settings: ${JSON.stringify(paramValues)}`);
|
|
373
|
+
} else {
|
|
374
|
+
delete paramValuesByFolder[projectFolder];
|
|
375
|
+
await this._saveAllSettingsToFile(paramValuesByFolder);
|
|
376
|
+
Logger.log('Deleted project settings');
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
static async _saveAllSettingsToFile(paramValuesByFolder) {
|
|
381
|
+
Logger.debug(`Saving all settings to ${CONFIG.paths.settings}`);
|
|
382
|
+
await fse.outputJson(CONFIG.paths.settings, paramValuesByFolder, { spaces: 2 });
|
|
383
|
+
Logger.debug('Saved settings');
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
static async _saveAuthToFile(params, authValues) {
|
|
387
|
+
Logger.debug(`Updating authentication data to ${CONFIG.paths.auth}`);
|
|
388
|
+
const appUrl = params.get('appUrl');
|
|
389
|
+
const location = `plain-text storage for app URL ${appUrl}, subdomain ${params.get('subdomain')}`;
|
|
390
|
+
|
|
391
|
+
const allAuthValues = await this._loadAuthFromFile(undefined, undefined);
|
|
392
|
+
if (authValues !== null) {
|
|
393
|
+
(allAuthValues[appUrl] || (allAuthValues[appUrl] = {}))[params.get('subdomain')] = authValues;
|
|
394
|
+
await fse.outputJson(CONFIG.paths.auth, allAuthValues, { spaces: 2 });
|
|
395
|
+
notifyLoggedIn(params)(`Saved authentication data to ${location}`);
|
|
396
|
+
} else if (allAuthValues[appUrl]) {
|
|
397
|
+
delete allAuthValues[appUrl][params.get('subdomain')];
|
|
398
|
+
if (Object.keys(allAuthValues[appUrl]).length === 0) {
|
|
399
|
+
delete allAuthValues[appUrl];
|
|
400
|
+
}
|
|
401
|
+
await fse.outputJson(CONFIG.paths.auth, allAuthValues, { spaces: 2 });
|
|
402
|
+
Logger.log(`Deleted authentication data from ${location}`);
|
|
403
|
+
} else {
|
|
404
|
+
Logger.log(`No authentication data to delete from ${location}`);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
static async _saveAuthToKeyring(params, auth) {
|
|
409
|
+
if (!keytar) {
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
Logger.debug('Updating authentication data to keyring');
|
|
414
|
+
const location = `keyring for app URL ${params.get('appUrl')}, subdomain ${params.get('subdomain')}`;
|
|
415
|
+
|
|
416
|
+
if (auth !== null) {
|
|
417
|
+
await keytar.setPassword(MTX_FULLY_QUALIFIED, getKeyringAccountName(params), JSON.stringify(auth));
|
|
418
|
+
notifyLoggedIn(params)(`Saved authentication data to ${location}`);
|
|
419
|
+
} else {
|
|
420
|
+
await keytar.deletePassword(MTX_FULLY_QUALIFIED, getKeyringAccountName(params));
|
|
421
|
+
Logger.log(`Deleted authentication data from ${location}`);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
static async _loadFromFile(projectFolder) {
|
|
426
|
+
const all = !projectFolder;
|
|
427
|
+
|
|
428
|
+
if (!fse.existsSync(CONFIG.paths.settings)) {
|
|
429
|
+
Logger.debug('Settings file absent');
|
|
430
|
+
return emptyData(all);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
Logger.debug('Reading settings file');
|
|
434
|
+
const settingsByFolder = await fse.readJson(CONFIG.paths.settings);
|
|
435
|
+
|
|
436
|
+
if (!settingsByFolder) {
|
|
437
|
+
Logger.debug('Empty settings file');
|
|
438
|
+
return emptyData(all);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if (all) { // return settings for all projects
|
|
442
|
+
return settingsByFolder;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (!settingsByFolder[projectFolder]) {
|
|
446
|
+
Logger.debug(`No settings found for project ${projectFolder}`);
|
|
447
|
+
return new ParamCollection();
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return new ParamCollection(settingsByFolder[projectFolder]);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
static async _loadAuthFromFile(appUrl, subdomain) {
|
|
454
|
+
const all = !appUrl;
|
|
455
|
+
|
|
456
|
+
if (!fse.existsSync(CONFIG.paths.auth)) {
|
|
457
|
+
Logger.debug('Authentication-data file absent');
|
|
458
|
+
return emptyData(all);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
Logger.debug(`Reading authentication data from ${CONFIG.paths.auth}`);
|
|
462
|
+
const allAuth = await fse.readJson(CONFIG.paths.auth);
|
|
463
|
+
|
|
464
|
+
if (!allAuth) {
|
|
465
|
+
Logger.debug('Empty authentication data');
|
|
466
|
+
return emptyData(all);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (all) { // return auth data for all projects
|
|
470
|
+
return allAuth;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
if (!allAuth[appUrl]) {
|
|
474
|
+
Logger.debug(`No authentication data found for app URL ${appUrl}`);
|
|
475
|
+
return new ParamCollection();
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const authForSubdomain = allAuth[appUrl][subdomain];
|
|
479
|
+
|
|
480
|
+
if (!authForSubdomain) {
|
|
481
|
+
Logger.debug(`No authentication data found for subdomain ${subdomain}`);
|
|
482
|
+
return new ParamCollection();
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (authForSubdomain.token) {
|
|
486
|
+
authForSubdomain.tokenStorage = 'plain';
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
return new ParamCollection(authForSubdomain);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
static async _loadAuthFromKeyring(appUrl, subdomain) {
|
|
493
|
+
if (!keytar) {
|
|
494
|
+
return new ParamCollection();
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
Logger.debug('Reading authentication data from keyring');
|
|
498
|
+
|
|
499
|
+
const authString = await keytar.getPassword(MTX_FULLY_QUALIFIED, getKeyringAccountName(new ParamCollection({ appUrl, subdomain })));
|
|
500
|
+
|
|
501
|
+
if (!authString) {
|
|
502
|
+
Logger.debug(`No authentication data found for URL ${appUrl} and subdomain '${subdomain}'`);
|
|
503
|
+
return new ParamCollection();
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
let auth;
|
|
507
|
+
|
|
508
|
+
try {
|
|
509
|
+
auth = JSON.parse(authString);
|
|
510
|
+
} catch (error) {
|
|
511
|
+
auth = {};
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
if (auth.token) {
|
|
515
|
+
auth.tokenStorage = 'keyring';
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
return new ParamCollection(auth);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
module.exports = {
|
|
524
|
+
SettingsManager,
|
|
525
|
+
other
|
|
526
|
+
};
|
package/lib/import/index.js
CHANGED
|
@@ -1,22 +1,15 @@
|
|
|
1
|
-
// Command to invoke from: C:\D\edmx2csn\core\bin>
|
|
2
|
-
// a) edmx2csn -i ../../input/metadata.xml -o ../../output/
|
|
3
|
-
// b) node edmx2csn.js -i ../../input/metadata.xml -o ../../output/
|
|
4
1
|
|
|
5
2
|
"use strict";
|
|
6
3
|
let convert = require('xml-js');
|
|
7
4
|
let axios = require('axios');
|
|
8
5
|
let edmxv2CSN = require("./v2parser");
|
|
9
6
|
let edmxv4CSN = require("./v4parser");
|
|
10
|
-
|
|
7
|
+
const messages = require("./message").getMessages();
|
|
11
8
|
let common = require("./common");
|
|
12
9
|
const cds = require('@sap/cds');
|
|
13
10
|
const fs = cds.utils, { path } = fs;
|
|
14
11
|
let defaultFileName;
|
|
15
12
|
|
|
16
|
-
function getV2Messages() {
|
|
17
|
-
return getMessages();
|
|
18
|
-
}
|
|
19
|
-
|
|
20
13
|
async function _isValidXML(text) {
|
|
21
14
|
let isValid = false;
|
|
22
15
|
let cjson;
|
|
@@ -38,12 +31,13 @@ async function isValidInputFile(inputFileLoc) {
|
|
|
38
31
|
const fileResolved = await fs.isfile(inputFileLoc);
|
|
39
32
|
|
|
40
33
|
if (!fileResolved) {
|
|
41
|
-
throw new Error(
|
|
34
|
+
throw new Error(messages.SPECIFY_INPUT_FILE);
|
|
42
35
|
}
|
|
43
36
|
else if (inputFileLoc.endsWith(".edmx") || inputFileLoc.endsWith(".xml")) {
|
|
44
37
|
file = fileResolved;
|
|
38
|
+
defaultFileName = file;
|
|
45
39
|
} else {
|
|
46
|
-
throw new Error(
|
|
40
|
+
throw new Error(messages.INVALID_INPUT_FILE);
|
|
47
41
|
}
|
|
48
42
|
return file;
|
|
49
43
|
}
|
|
@@ -56,7 +50,7 @@ function validateAndComputeOutputFilePath(outputFileLoc) {
|
|
|
56
50
|
}
|
|
57
51
|
if (outputFileLoc === true || // if output file is not mentioned
|
|
58
52
|
outputFileLoc.trim() === "") {
|
|
59
|
-
throw new Error(
|
|
53
|
+
throw new Error(messages.SPECIFY_OUTPUT_FILE);
|
|
60
54
|
} else if (outputFileLoc.endsWith(".json") || outputFileLoc.endsWith(".csn")) {
|
|
61
55
|
filePath = outputFileLoc;
|
|
62
56
|
} else if (index > 0 &&
|
|
@@ -65,19 +59,16 @@ function validateAndComputeOutputFilePath(outputFileLoc) {
|
|
|
65
59
|
extension !== 'json' &&
|
|
66
60
|
extension !== 'csn') {
|
|
67
61
|
// Output file extension is not either .json or .csn ("test.abcde", "./srv/external/test.abcde")
|
|
68
|
-
throw new Error(
|
|
62
|
+
throw new Error(messages.INVALID_OUTPUT_FILE);
|
|
69
63
|
} else if (!outputFileLoc.endsWith(".json") &&
|
|
70
64
|
!outputFileLoc.endsWith(".csn") &&
|
|
71
65
|
!outputFileLoc.endsWith("\\") &&
|
|
72
66
|
!outputFileLoc.endsWith("/")) {
|
|
73
67
|
// Output file extension is not mentioned ("./srv/external/test", "test", "c:\windows\output\test")
|
|
74
68
|
filePath = outputFileLoc + ".csn";
|
|
75
|
-
} else
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
} else {
|
|
79
|
-
filePath = outputFileLoc + "/" + _getDefaultFileName();
|
|
80
|
-
}
|
|
69
|
+
} else if (outputFileLoc.endsWith("/") || outputFileLoc.endsWith("\\")){
|
|
70
|
+
// File name not specified ("./srv/external/", "c:\windows\output\test\")
|
|
71
|
+
filePath = outputFileLoc + path.parse(defaultFileName).name + ".csn";
|
|
81
72
|
}
|
|
82
73
|
|
|
83
74
|
return filePath;
|
|
@@ -96,7 +87,7 @@ async function getMetadataFromURL(url) {
|
|
|
96
87
|
if (edmx && (await _isValidXML(edmx))) {
|
|
97
88
|
return edmx;
|
|
98
89
|
} else {
|
|
99
|
-
throw new Error(
|
|
90
|
+
throw new Error(messages.INVALID_EDMX_METADATA);
|
|
100
91
|
}
|
|
101
92
|
}
|
|
102
93
|
|
|
@@ -135,21 +126,21 @@ async function edmx2csn(edmx, filename, context) {
|
|
|
135
126
|
const result = edmx.match(/EntityType="([^.]+)/);
|
|
136
127
|
const multipleSchemas = /(\s*<\/Schema>\s*<Schema [^>]*>)\s*/m;
|
|
137
128
|
if (!result) {
|
|
138
|
-
throw new Error(
|
|
129
|
+
throw new Error(messages.MISSING_ENTITIES);
|
|
139
130
|
}
|
|
140
131
|
const namespace = result[1];
|
|
141
132
|
|
|
142
133
|
if (multipleSchemas.test(edmx)) {
|
|
143
134
|
edmx = _convertEDMXWithMultipleSchemas(edmx, multipleSchemas, namespace);
|
|
144
135
|
}
|
|
145
|
-
|
|
146
|
-
|
|
136
|
+
let csn = await generateCSN(edmx, false, true, context);
|
|
137
|
+
if (filename) {
|
|
138
|
+
// Replace namespace by given filename in the csn file instead of edmx file
|
|
147
139
|
const re = RegExp(`([("/])${namespace}([/").])`, 'g');
|
|
148
140
|
const fn = `$1${path.parse(filename).name}$2`;
|
|
149
|
-
|
|
141
|
+
csn = csn.replace(re, fn);
|
|
150
142
|
}
|
|
151
|
-
|
|
152
|
-
return csn;
|
|
143
|
+
return JSON.parse(csn);
|
|
153
144
|
}
|
|
154
145
|
|
|
155
146
|
async function _generateEDMX2JSON(edmx) {
|
|
@@ -191,27 +182,15 @@ async function generateCSN(edmx, ignorePersistenceSkip, mockServerUc, context) {
|
|
|
191
182
|
return edmxv4CSN.getEdmxv4CSN(edmx, ignorePersistenceSkip, mockServerUc);
|
|
192
183
|
}
|
|
193
184
|
return new Promise(function invalidFile(resolve, reject) {
|
|
194
|
-
reject(new Error(
|
|
185
|
+
reject(new Error(messages.INVALID_EDMX_METADATA));
|
|
195
186
|
});
|
|
196
187
|
}
|
|
197
188
|
|
|
198
|
-
function _getDefaultFileName() {
|
|
199
|
-
let nameSpace;
|
|
200
|
-
if (defaultFileName === undefined) {
|
|
201
|
-
nameSpace = edmxv2CSN.getNamespace();
|
|
202
|
-
if (nameSpace) {
|
|
203
|
-
defaultFileName = nameSpace + ".csn"; //".json";
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
return defaultFileName;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
189
|
async function saveCSNModel(csnDataModel, fileLoc) {
|
|
210
190
|
await fs.write(fileLoc, csnDataModel, "utf-8");
|
|
211
191
|
}
|
|
212
192
|
|
|
213
193
|
module.exports = {
|
|
214
|
-
getV2Messages,
|
|
215
194
|
isValidInputFile,
|
|
216
195
|
validateAndComputeOutputFilePath,
|
|
217
196
|
edmx2csn,
|
package/lib/import/message.js
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
const messages = {
|
|
4
|
-
INVALID_EDMX_METADATA:
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
"Please provide an output file with either .json or .csn extension.",
|
|
8
|
-
MISSING_ENTITIES:
|
|
9
|
-
"There are no entities in the OData model. Specify the correct OData model in the provided file.",
|
|
4
|
+
INVALID_EDMX_METADATA: "The OData model is not valid. Specify the correct OData model.",
|
|
5
|
+
INVALID_OUTPUT_FILE: "Please provide an output file with either .json or .csn extension.",
|
|
6
|
+
MISSING_ENTITIES: "There are no entities in the OData model. Specify the correct OData model in the provided file.",
|
|
10
7
|
SPECIFY_INPUT_FILE: "Specify an input file that exists.",
|
|
11
8
|
SPECIFY_OUTPUT_FILE: "Specify an output file.",
|
|
12
9
|
MISSING_ENTITY_CONTAINER: 'There is no entity container in the OData model. Specify the correct OData model.',
|
|
@@ -18,4 +15,6 @@ function getMessages() {
|
|
|
18
15
|
return messages;
|
|
19
16
|
}
|
|
20
17
|
|
|
21
|
-
module.exports =
|
|
18
|
+
module.exports = {
|
|
19
|
+
getMessages
|
|
20
|
+
};
|