@sap/cds-dk 4.4.2 → 4.5.4
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 +60 -0
- package/bin/activate.js +1 -2
- package/bin/deploy/index.js +3 -2
- package/bin/deploy/to-sqlite.js +2 -2
- package/bin/extend.js +2 -4
- package/bin/help.js +1 -1
- package/bin/import.js +105 -23
- package/bin/login.js +7 -8
- package/bin/logout.js +1 -2
- package/bin/repl.js +1 -1
- package/bin/watch.js +30 -11
- 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/common.js +24 -1
- package/lib/import/importapi.js +1 -1
- package/lib/import/index.js +25 -41
- package/lib/import/message.js +10 -6
- package/lib/import/v2parser.js +301 -47
- package/lib/import/v4parser.js +391 -71
- package/lib/init/constants.js +1 -1
- package/lib/init/index.js +15 -10
- package/lib/init/template/data/index.js +38 -46
- package/lib/init/template/hana/index.js +2 -2
- 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/lint/Linter.js +62 -60
- package/lib/lint/generators.js +1 -1
- package/lib/lint/io.js +9 -33
- package/npm-shrinkwrap.json +5179 -10848
- package/package.json +5 -5
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const axios = require('axios');
|
|
3
|
+
const { fse } = require('@sap/cds-foss');
|
|
4
|
+
|
|
5
|
+
const AuthManager = require('./auth_manager');
|
|
6
|
+
const BaseCommand = require('./base_command');
|
|
7
|
+
const { collectFileContent, collectFiles, toUnixPath } = require('./helper/fs_helper');
|
|
8
|
+
const Logger = require('./helper/logging');
|
|
9
|
+
|
|
10
|
+
const COMMAND_URL_SUFFIX = 'asyncActivate';
|
|
11
|
+
const UPDATE_CUSTOM_FILE_URL_SUFFIX = 'updateCustomTenantContent';
|
|
12
|
+
|
|
13
|
+
const I18N_WHITELIST = ['.properties', '.csv'];
|
|
14
|
+
|
|
15
|
+
const JOB_STATUS = {
|
|
16
|
+
QUEUED: 'QUEUED',
|
|
17
|
+
RUNNING: 'RUNNING',
|
|
18
|
+
FINISHED: 'FINISHED',
|
|
19
|
+
FAILED: 'FAILED'
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
async function checkFinished(jobId, statusUrl, params, finisher) {
|
|
23
|
+
const requestOptions = Object.assign({
|
|
24
|
+
method: 'GET'
|
|
25
|
+
}, params.get('reqAuth'));
|
|
26
|
+
|
|
27
|
+
const { status, error } = (await axios(statusUrl, requestOptions)).data;
|
|
28
|
+
|
|
29
|
+
if ((status === JOB_STATUS.FINISHED || status === JOB_STATUS.FAILED) && !finisher.finished) {
|
|
30
|
+
finisher.finished = true;
|
|
31
|
+
clearInterval(finisher.timer);
|
|
32
|
+
if (status === JOB_STATUS.FAILED) {
|
|
33
|
+
return finisher.reject(new Error(`Job ${jobId} failed: ${error.message || JSON.stringify(error)}`));
|
|
34
|
+
}
|
|
35
|
+
Logger.log('Extension active');
|
|
36
|
+
finisher.resolve();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
module.exports = class Activate extends BaseCommand {
|
|
41
|
+
|
|
42
|
+
static get COMMAND_URL_SUFFIX() { return COMMAND_URL_SUFFIX; }
|
|
43
|
+
static get UPDATE_CUSTOM_FILE_URL_SUFFIX() { return UPDATE_CUSTOM_FILE_URL_SUFFIX; }
|
|
44
|
+
|
|
45
|
+
static async run(injection, paramValues) {
|
|
46
|
+
try {
|
|
47
|
+
const params = await AuthManager.loginAndUpdateSettings(paramValues);
|
|
48
|
+
|
|
49
|
+
if (process.env.DEBUG) {
|
|
50
|
+
Logger.log('Activating extension with parameters ' + JSON.stringify({
|
|
51
|
+
appUrl: params.get('appUrl'),
|
|
52
|
+
projectFolder: params.get('projectFolder'),
|
|
53
|
+
undeploy: params.get('undeploy')
|
|
54
|
+
}));
|
|
55
|
+
} else {
|
|
56
|
+
Logger.log(`Activating extension located at ${params.get('projectFolder')} to URL ${params.get('appUrl')}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!await Activate.checkProject(params.get('projectFolder'))) {
|
|
60
|
+
throw new Error(params.get('projectFolder') + ' is not a valid extension project');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
await Activate.runActivate(injection.cds, params);
|
|
65
|
+
if (params.get('wsdl')) {
|
|
66
|
+
await Activate.runCustomFileUpload(params);
|
|
67
|
+
}
|
|
68
|
+
} catch (error) {
|
|
69
|
+
Activate.handleHttpError(error, params);
|
|
70
|
+
}
|
|
71
|
+
} catch (error) {
|
|
72
|
+
Logger.error(undefined, error);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
static async collectExtensionFiles(cds, projectFolder) {
|
|
77
|
+
|
|
78
|
+
const absoluteFolder = projectFolder;
|
|
79
|
+
const dbFolder = path.join(absoluteFolder, 'db');
|
|
80
|
+
const srvFolder = path.join(absoluteFolder, 'srv');
|
|
81
|
+
|
|
82
|
+
const resolvedPathDb = cds.resolve(dbFolder, { root: dbFolder });
|
|
83
|
+
const resolvedPathSrv = cds.resolve(srvFolder, { root: srvFolder });
|
|
84
|
+
|
|
85
|
+
let resolvedPaths = [];
|
|
86
|
+
if (resolvedPathSrv) {
|
|
87
|
+
resolvedPaths = resolvedPaths.concat(resolvedPathSrv);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (resolvedPathDb) {
|
|
91
|
+
resolvedPaths = resolvedPaths.concat(resolvedPathDb);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let allFiles = new Map();
|
|
95
|
+
|
|
96
|
+
if (resolvedPaths.length < 1) {
|
|
97
|
+
Logger.log('No extension files found');
|
|
98
|
+
return allFiles;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const csn = await cds.load(resolvedPaths);
|
|
102
|
+
const sources = csn.$sources || csn._sources;
|
|
103
|
+
|
|
104
|
+
const filteredSources = sources && sources.filter((entry) => {
|
|
105
|
+
if (! entry.startsWith(projectFolder)) {
|
|
106
|
+
/* Current file might be below the base model's node_modules, in which case compilation on server should succeed.
|
|
107
|
+
Otherwise, compilation will probably fail, with this message giving a hint. */
|
|
108
|
+
Logger.debug(`Source file outside project folder will not be uploaded: ${entry}`);
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
const unixFilename = toUnixPath(entry);
|
|
112
|
+
return !/.*node_modules\/_base\/.*/.test(unixFilename);
|
|
113
|
+
}) || [];
|
|
114
|
+
|
|
115
|
+
const sourceFiles = await collectFileContent(filteredSources, projectFolder);
|
|
116
|
+
allFiles = new Map([...allFiles, ...sourceFiles]);
|
|
117
|
+
|
|
118
|
+
const modelFiles4I18n = { $sources: filteredSources };
|
|
119
|
+
|
|
120
|
+
let i18nFolders = cds.localize.folders4(modelFiles4I18n);
|
|
121
|
+
i18nFolders = i18nFolders.filter(i18nFolder => {
|
|
122
|
+
const unixFilename = toUnixPath(i18nFolder);
|
|
123
|
+
return !(/.*node_modules\/_base\/.*/.test(unixFilename));
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
i18nFolders.forEach(async(folder) => {
|
|
127
|
+
const i18nFiles = collectFiles(projectFolder, folder, entry => {
|
|
128
|
+
const ext = path.extname(entry);
|
|
129
|
+
return I18N_WHITELIST.includes(ext);
|
|
130
|
+
});
|
|
131
|
+
allFiles = new Map([...allFiles, ...i18nFiles]);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
const pkgJson = await fse.readFile(path.join(absoluteFolder, 'package.json'), 'utf-8');
|
|
136
|
+
allFiles.set('package.json', pkgJson);
|
|
137
|
+
} catch(e) {
|
|
138
|
+
Logger.log('No package.json file found');
|
|
139
|
+
Logger.error('No package.json file found', e);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return allFiles;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
static collectCustomFiles(projectFolder) {
|
|
146
|
+
const wsdlFolder = path.join(projectFolder, 'wsdl');
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
const wsdlFiles = collectFiles(projectFolder, wsdlFolder);
|
|
150
|
+
if (wsdlFiles.size === 0) {
|
|
151
|
+
Logger.log('No custom files found');
|
|
152
|
+
}
|
|
153
|
+
return wsdlFiles;
|
|
154
|
+
} catch (error) {
|
|
155
|
+
Logger.error(undefined, error);
|
|
156
|
+
throw new Error('No custom files found');
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
static async runActivate(cds, params) {
|
|
161
|
+
let activationUrl;
|
|
162
|
+
|
|
163
|
+
return Activate.collectExtensionFiles(cds, params.get('projectFolder'))
|
|
164
|
+
.then(allFiles => {
|
|
165
|
+
if (allFiles.size === 0) {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
Logger.log('Activating extension...');
|
|
170
|
+
|
|
171
|
+
activationUrl = Activate.getMtxApiUrl(params.get('appUrl'), COMMAND_URL_SUFFIX);
|
|
172
|
+
const requestOptions = Object.assign({
|
|
173
|
+
method: 'POST',
|
|
174
|
+
headers: {},
|
|
175
|
+
data: {
|
|
176
|
+
extension: [...allFiles],
|
|
177
|
+
undeployExtension: params.get('undeploy')
|
|
178
|
+
}
|
|
179
|
+
}, params.get('reqAuth'));
|
|
180
|
+
requestOptions.headers['Content-Type'] = 'application/json';
|
|
181
|
+
|
|
182
|
+
return axios(activationUrl, requestOptions);
|
|
183
|
+
})
|
|
184
|
+
.then(response => {
|
|
185
|
+
if (response === null) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const { jobID: jobId, statusPath } = response.data;
|
|
190
|
+
|
|
191
|
+
if (jobId === undefined) {
|
|
192
|
+
throw new Error('Activation failed: did not receive job ID');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (!statusPath) {
|
|
196
|
+
throw new Error(`Job ${jobId} failed: did not receive job-status path`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
Logger.debug(`Job ID: ${jobId}`);
|
|
200
|
+
|
|
201
|
+
return new Promise((resolve, reject) => {
|
|
202
|
+
const statusUrl = Activate.concatUrls(params.get('appUrl'), statusPath);
|
|
203
|
+
const finisher = {
|
|
204
|
+
resolve,
|
|
205
|
+
reject,
|
|
206
|
+
finished: false,
|
|
207
|
+
timer: null
|
|
208
|
+
};
|
|
209
|
+
finisher.timer = setInterval(checkFinished.bind(undefined, jobId, statusUrl, params, finisher), 250);
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
static async runCustomFileUpload(params) {
|
|
215
|
+
Logger.log(`Uploading custom files to ${params.get('appUrl')}...`);
|
|
216
|
+
|
|
217
|
+
const customFiles = Activate.collectCustomFiles(params.get('projectFolder'));
|
|
218
|
+
|
|
219
|
+
const customFilesSerialized = [];
|
|
220
|
+
customFiles.forEach((value, key) => {
|
|
221
|
+
customFilesSerialized.push({
|
|
222
|
+
category: "wsdl",
|
|
223
|
+
name: key,
|
|
224
|
+
content: value
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
const customFileUrl = Activate.getMtxApiUrl(params.get('appUrl'), Activate.UPDATE_CUSTOM_FILE_URL_SUFFIX);
|
|
229
|
+
const requestOptions = Object.assign({
|
|
230
|
+
method: 'POST',
|
|
231
|
+
headers: {},
|
|
232
|
+
data: {
|
|
233
|
+
content: customFilesSerialized
|
|
234
|
+
}
|
|
235
|
+
}, params.get('reqAuth'));
|
|
236
|
+
requestOptions.headers['Content-Type'] = 'application/json';
|
|
237
|
+
|
|
238
|
+
await axios(customFileUrl, requestOptions);
|
|
239
|
+
|
|
240
|
+
Logger.log('Custom file upload finished');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
const util = require('util');
|
|
2
|
+
const axios = require('axios');
|
|
3
|
+
|
|
4
|
+
const Logger = require('./helper/logging');
|
|
5
|
+
const { SettingsManager } = require('./settings_manager');
|
|
6
|
+
const { ParamCollection } = require('./params');
|
|
7
|
+
|
|
8
|
+
function addTokenInfo(params, authData) {
|
|
9
|
+
params.set('token', authData.access_token);
|
|
10
|
+
if (authData.expires_in) {
|
|
11
|
+
params.set('tokenExpirationDate', Date.now() + authData.expires_in * 1000);
|
|
12
|
+
} else {
|
|
13
|
+
params.delete('tokenExpirationDate');
|
|
14
|
+
}
|
|
15
|
+
if (authData.refresh_token) {
|
|
16
|
+
params.set('refreshToken', authData.refresh_token);
|
|
17
|
+
} else {
|
|
18
|
+
params.delete('refreshToken');
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function addUrlInfo(params, authData) {
|
|
22
|
+
params.set('passcodeUrl', authData.passcode_url);
|
|
23
|
+
}
|
|
24
|
+
function getTokenUrl(params) {
|
|
25
|
+
const tokenUrl = new URL(params.get('tokenUrl'));
|
|
26
|
+
tokenUrl.search = `subdomain=${params.get('subdomain')}`;
|
|
27
|
+
|
|
28
|
+
if (params.has('refreshToken')) {
|
|
29
|
+
tokenUrl.search += `&refresh_token=${params.get('refreshToken')}`;
|
|
30
|
+
} else if (params.has('passcode')) {
|
|
31
|
+
tokenUrl.search += `&passcode=${params.get('passcode')}`;
|
|
32
|
+
}
|
|
33
|
+
if (params.has('clientid')) {
|
|
34
|
+
tokenUrl.search += `&clientid=${params.get('clientid')}&clientsecret=${params.get('clientsecret')}`;
|
|
35
|
+
}
|
|
36
|
+
return tokenUrl.toString();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function retrieveTokenOrPasscodeUrl(params) {
|
|
40
|
+
if (params.has('token')) {
|
|
41
|
+
if (params.get('renewLogin') || params.get('tokenExpirationDate') <= Date.now()) {
|
|
42
|
+
Logger.debug((params.get('renewLogin') ? 'Renewing' : 'Refreshing expired') + ' authentication token');
|
|
43
|
+
params.delete('token');
|
|
44
|
+
params.delete('tokenExpirationDate');
|
|
45
|
+
} else {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (!params.has('passcode') && !params.has('refreshToken') && params.has('passcodeUrl')) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let retry;
|
|
54
|
+
let authData;
|
|
55
|
+
do {
|
|
56
|
+
const tokenUrl = getTokenUrl(params);
|
|
57
|
+
Logger.debug(`Getting authentication token or passcode URL from ${(params.obfuscateQueryParams(tokenUrl))}`);
|
|
58
|
+
|
|
59
|
+
let response;
|
|
60
|
+
try {
|
|
61
|
+
response = await axios(tokenUrl.toString(), { method: 'GET' });
|
|
62
|
+
} catch (error) {
|
|
63
|
+
throw new Error(`Failed to get authentication token: ${error}.\nResponse body: ${util.inspect(response && response.data)}`);
|
|
64
|
+
}
|
|
65
|
+
authData = response.data;
|
|
66
|
+
|
|
67
|
+
retry = !authData.access_token && params.has('refreshToken') && (params.delete('refreshToken'), true);
|
|
68
|
+
} while (retry);
|
|
69
|
+
|
|
70
|
+
if (authData.access_token) {
|
|
71
|
+
addTokenInfo(params, authData);
|
|
72
|
+
} else {
|
|
73
|
+
addUrlInfo(params, authData);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
module.exports = class AuthManager {
|
|
79
|
+
|
|
80
|
+
static async loginAndUpdateSettings(paramValues) {
|
|
81
|
+
SettingsManager.init();
|
|
82
|
+
const params = new ParamCollection(paramValues);
|
|
83
|
+
await SettingsManager.loadAndMergeSettings(params);
|
|
84
|
+
if (params.has('username')) {
|
|
85
|
+
params.set('reqAuth', { auth: { username: params.get('username'), password: params.get('password') } });
|
|
86
|
+
} else {
|
|
87
|
+
await retrieveTokenOrPasscodeUrl(params);
|
|
88
|
+
if (!params.has('token')) {
|
|
89
|
+
await SettingsManager.saveSettings(params);
|
|
90
|
+
throw new Error('Failed to login: no valid token or passcode provided. ' +
|
|
91
|
+
'Get a passcode' + (params.get('passcodeUrl') ? ' from ' + params.get('passcodeUrl') : '') +
|
|
92
|
+
' and supply the code when re-running the command');
|
|
93
|
+
}
|
|
94
|
+
params.set('reqAuth', { headers: { Authorization: 'Bearer ' + params.get('token') } });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
await SettingsManager.saveSettings(params); // saves token conditionally
|
|
98
|
+
|
|
99
|
+
return params;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
static async logout(paramValues) {
|
|
103
|
+
SettingsManager.init();
|
|
104
|
+
const params = new ParamCollection(paramValues);
|
|
105
|
+
if (params.get('clearInvalid')) {
|
|
106
|
+
await SettingsManager.deleteInvalid();
|
|
107
|
+
} else {
|
|
108
|
+
await SettingsManager.deleteToken(params);
|
|
109
|
+
if (params.get('deleteSettings')) {
|
|
110
|
+
await SettingsManager.deleteSettingsWithoutToken(params);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const { fse } = require('@sap/cds-foss');
|
|
3
|
+
|
|
4
|
+
const Logger = require('./helper/logging');
|
|
5
|
+
|
|
6
|
+
const MTX_URL_PATH_PREFIX = 'mtx/v1/model/';
|
|
7
|
+
const CUSTOM_FILE_URL_SUFFIX = 'customTenantContent';
|
|
8
|
+
|
|
9
|
+
module.exports = class BaseCommand {
|
|
10
|
+
|
|
11
|
+
static get CUSTOM_FILE_URL_SUFFIX() { return CUSTOM_FILE_URL_SUFFIX; }
|
|
12
|
+
|
|
13
|
+
static async checkProject(folder) {
|
|
14
|
+
return fse.pathExists(path.join(folder, 'package.json'));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
static async handleHttpError(error, params) {
|
|
18
|
+
if (!('isAxiosError' in error)) {
|
|
19
|
+
throw error;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
switch (error.response.status) {
|
|
23
|
+
case 401:
|
|
24
|
+
Logger.error('Request failed: authorization might have expired. Please retry with valid passcode' +
|
|
25
|
+
(params.has('passcodeUrl') ? ` from ${params.get('passcodeUrl')}` : ''), error);
|
|
26
|
+
break;
|
|
27
|
+
case 404:
|
|
28
|
+
Logger.error(`Request failed. Please check application URL ${params.get('appUrl')}`, error);
|
|
29
|
+
break;
|
|
30
|
+
default:
|
|
31
|
+
// Including 422 (compilation or native-extension error).
|
|
32
|
+
Logger.error(undefined, error);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
static concatUrls(base, ...paths) {
|
|
37
|
+
function trimBeginSlash(str) {
|
|
38
|
+
return str.replace(/^\//, '');
|
|
39
|
+
}
|
|
40
|
+
function trimEndSlash(str) {
|
|
41
|
+
return str.replace(/\/$/, '');
|
|
42
|
+
}
|
|
43
|
+
function trimSlashes(str) {
|
|
44
|
+
return trimBeginSlash(trimEndSlash(str));
|
|
45
|
+
}
|
|
46
|
+
const concatenated = new URL(base);
|
|
47
|
+
concatenated.pathname = trimEndSlash(concatenated.pathname) + '/' +
|
|
48
|
+
paths.map((p, i) => i < paths.length - 1 ? trimSlashes(p) : trimBeginSlash(p)).join('/');
|
|
49
|
+
return concatenated.toString();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
static getMtxApiUrl(url, action) {
|
|
53
|
+
return BaseCommand.concatUrls(url, MTX_URL_PATH_PREFIX, action);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
const axios = require('axios');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { fse } = require('@sap/cds-foss');
|
|
4
|
+
|
|
5
|
+
const AuthManager = require('./auth_manager');
|
|
6
|
+
const BaseCommand = require('./base_command');
|
|
7
|
+
const { normalizePath, isParent } = require('./helper/fs_helper');
|
|
8
|
+
const Logger = require('./helper/logging');
|
|
9
|
+
const Question = require('./helper/question');
|
|
10
|
+
|
|
11
|
+
const COMMAND_URL_SUFFIX = 'content';
|
|
12
|
+
const CUSTOM_FILE_FOLDERS = ['wsdl'];
|
|
13
|
+
const DB_FOLDER = 'db';
|
|
14
|
+
const SRV_FOLDER = 'srv';
|
|
15
|
+
const TEMPLATE_FOLDER = 'tpl';
|
|
16
|
+
|
|
17
|
+
function folderExists(folder) {
|
|
18
|
+
return `${folder} already contains an extension project, use another directory or use --force.` +
|
|
19
|
+
'\nWhen using --force, make sure you are using a version control system (like git) to avoid data loss';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
module.exports = class Extend extends BaseCommand {
|
|
23
|
+
|
|
24
|
+
static get COMMAND_URL_SUFFIX() { return COMMAND_URL_SUFFIX; }
|
|
25
|
+
|
|
26
|
+
static async storeFiles(content, targetDirectory, filter) {
|
|
27
|
+
if (!fse.existsSync(targetDirectory)) {
|
|
28
|
+
fse.mkdirpSync(targetDirectory);
|
|
29
|
+
}
|
|
30
|
+
targetDirectory = fse.realpathSync(targetDirectory)
|
|
31
|
+
const writtenFiles = new Set();
|
|
32
|
+
const filesToWrite = new Map();
|
|
33
|
+
for (const [filename, filecontent] of content) {
|
|
34
|
+
const filepath = path.join(targetDirectory, filename);
|
|
35
|
+
if (filepath.startsWith(targetDirectory) && (!filter || await filter(filepath))) {
|
|
36
|
+
filesToWrite.set(filepath, filecontent);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
for (const [filepath, content] of [...filesToWrite.entries()]) {
|
|
40
|
+
const normalizedFilepath = normalizePath(filepath);
|
|
41
|
+
fse.mkdirpSync(path.dirname(normalizedFilepath));
|
|
42
|
+
fse.writeFileSync(normalizedFilepath, content);
|
|
43
|
+
writtenFiles.add(path.relative(process.cwd(), normalizedFilepath));
|
|
44
|
+
Extend._logWrittenFile(normalizedFilepath);
|
|
45
|
+
}
|
|
46
|
+
return writtenFiles;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
static async storeCustomFiles(directory, customFiles) {
|
|
50
|
+
const customFileMap = new Map();
|
|
51
|
+
customFiles.forEach(entry => {
|
|
52
|
+
if (entry.category === 'wsdl') {
|
|
53
|
+
const normalizedFilename = normalizePath(entry.name);
|
|
54
|
+
const folder = normalizedFilename.split(path.sep)[0];
|
|
55
|
+
if (CUSTOM_FILE_FOLDERS.includes(folder)) {
|
|
56
|
+
customFileMap.set(entry.name, entry.content);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
await Extend.storeFiles([...customFileMap], directory);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
static async storeTemplateFiles(templates, directory) {
|
|
64
|
+
await Extend.storeFiles(templates, directory, async filepath => {
|
|
65
|
+
const normalizedPath = normalizePath(filepath);
|
|
66
|
+
if (!isParent(path.join(directory, DB_FOLDER), normalizedPath) && !isParent(path.join(directory, SRV_FOLDER), normalizedPath)) {
|
|
67
|
+
Logger.log(`Ignoring template ${normalizedPath}: not in ${DB_FOLDER} or ${SRV_FOLDER}`);
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
return true;
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
static async run(paramValues) {
|
|
75
|
+
try {
|
|
76
|
+
const params = await AuthManager.loginAndUpdateSettings(paramValues);
|
|
77
|
+
|
|
78
|
+
if (process.env.DEBUG) {
|
|
79
|
+
Logger.log(`\n\nCreating extension project with parameters ${JSON.stringify({
|
|
80
|
+
appUrl: params.get('appUrl'),
|
|
81
|
+
projectFolder: params.get('projectFolder'),
|
|
82
|
+
subdomain: params.get('subdomain'),
|
|
83
|
+
passcode: params.get('passcode'),
|
|
84
|
+
username: params.get('username'),
|
|
85
|
+
password: params.get('password') ? '***' : 'undefined'
|
|
86
|
+
})}`);
|
|
87
|
+
} else {
|
|
88
|
+
Logger.log(`\n\nCreating extension project for ${params.get('appUrl')}, subdomain '${params.get('subdomain')}', in folder '${params.get('projectFolder')}'`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const runOnExistingProject = await Extend.checkProject(params.get('projectFolder'));
|
|
92
|
+
if (runOnExistingProject && !(params.get('force') || params.get('templates'))) {
|
|
93
|
+
throw new Error(folderExists(params.get('projectFolder')));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
await Extend.runExtend(params);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
await Extend.handleHttpError(error, params);
|
|
100
|
+
}
|
|
101
|
+
} catch (mainError) {
|
|
102
|
+
Logger.error(undefined, mainError);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
static async runExtend(params) {
|
|
107
|
+
const extendUrl = Extend.getMtxApiUrl(params.get('appUrl'), COMMAND_URL_SUFFIX);
|
|
108
|
+
const requestOptions = Object.assign({
|
|
109
|
+
method: 'GET',
|
|
110
|
+
retry: 0
|
|
111
|
+
}, params.get('reqAuth'));
|
|
112
|
+
|
|
113
|
+
Logger.log('Retrieving files...');
|
|
114
|
+
|
|
115
|
+
const [ response ] = (await axios(extendUrl, requestOptions)).data;
|
|
116
|
+
|
|
117
|
+
Logger.log('\nWriting files to: ' + params.get('projectFolder'));
|
|
118
|
+
|
|
119
|
+
if (params.get('templates')) {
|
|
120
|
+
await Extend.storeTemplateFilesToTpl(response.templates, params.get('projectFolder'));
|
|
121
|
+
return; // only write templates
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const content = response.base.concat(response.extension);
|
|
125
|
+
|
|
126
|
+
await Extend.storeFiles(content, params.get('projectFolder'));
|
|
127
|
+
|
|
128
|
+
await fse.mkdirp(path.join(params.get('projectFolder'), DB_FOLDER));
|
|
129
|
+
await fse.mkdirp(path.join(params.get('projectFolder'), SRV_FOLDER));
|
|
130
|
+
|
|
131
|
+
Logger.log(`\nPut database extension models into ${DB_FOLDER} folder.`);
|
|
132
|
+
Logger.log(`Put service extension models into ${SRV_FOLDER} folder.`);
|
|
133
|
+
|
|
134
|
+
if (response.templates && response.templates.length > 0) {
|
|
135
|
+
// only write templates if there are no extensions so far
|
|
136
|
+
if (response.extension && response.extension.length === 0) {
|
|
137
|
+
Logger.log(`Writing template files to ${DB_FOLDER} and ${SRV_FOLDER} folder`);
|
|
138
|
+
await Extend.storeTemplateFiles(response.templates, params.get('projectFolder'));
|
|
139
|
+
} else {
|
|
140
|
+
Logger.log('Ignoring templates because extension is already active.');
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (params.get('wsdl')) {
|
|
145
|
+
|
|
146
|
+
Logger.log('Writing custom files');
|
|
147
|
+
|
|
148
|
+
const customFileUrl = Extend.getMtxApiUrl(params.get('appUrl'), Extend.CUSTOM_FILE_URL_SUFFIX);
|
|
149
|
+
const customFiles = (await axios(customFileUrl, params)).data;
|
|
150
|
+
await Extend.storeCustomFiles(params.get('projectFolder'), customFiles);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
static async storeTemplateFilesToTpl(templates, projectFolder) {
|
|
155
|
+
if (templates && templates.length > 0) {
|
|
156
|
+
const targetDirectory = path.join(projectFolder, TEMPLATE_FOLDER);
|
|
157
|
+
const targetExists = await fse.pathExists(targetDirectory);
|
|
158
|
+
if (!targetExists || await this.promptTemplateOverwrite()) {
|
|
159
|
+
if (targetExists) {
|
|
160
|
+
await fse.remove(targetDirectory);
|
|
161
|
+
}
|
|
162
|
+
Logger.log(`Writing template files to ${TEMPLATE_FOLDER} folder`);
|
|
163
|
+
await Extend.storeTemplateFiles(templates, targetDirectory);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
} else {
|
|
167
|
+
Logger.log('No templates available');
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
static promptTemplateOverwrite() {
|
|
172
|
+
return Question.askBooleanQuestion(`This will overwrite extisting templates in folder ${TEMPLATE_FOLDER}. Continue (yN)? `);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
static _logWrittenFile(file) {
|
|
176
|
+
Logger.log('CREATE: ' + file);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
}
|