@corva/create-app 0.39.0-2 → 0.40.0-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/README.md CHANGED
@@ -26,6 +26,7 @@ Commands:
26
26
  create [options] [project-directory] Create a new app
27
27
  zip [options] <project-directory> [patterns...] Bundle app
28
28
  release [options] <project-directory> [patterns...] Release app
29
+ rerun [options] <project-directory> Rerun app
29
30
  help [command] display help for command
30
31
  ```
31
32
 
@@ -199,4 +200,4 @@ create-corva-app release test-app --bump-version=patch
199
200
 
200
201
  ```sh
201
202
  create-corva-app release test-app --bump-version=4.2.0
202
- ```
203
+ ```
@@ -0,0 +1,225 @@
1
+ const axios = require('axios');
2
+
3
+ /**
4
+ * Connect to Corva API
5
+ *
6
+ */
7
+ class Api {
8
+ constructor(envName, apiKey, authToken = null) {
9
+ this.baseURL = `https://api${
10
+ envName === 'production' ? '' : `.${envName}`
11
+ }.corva.ai`;
12
+
13
+ this.config = {
14
+ headers: {
15
+ 'Authorization': authToken ? `Bearer ${authToken}` :`API ${apiKey}`,
16
+ 'Content-Type': 'application/json',
17
+ 'Accept': 'application/json',
18
+ },
19
+ };
20
+ }
21
+
22
+ /**
23
+ * Get api url
24
+ *
25
+ * @private
26
+ *
27
+ * @param {string} path
28
+ *
29
+ * @returns {string}
30
+ */
31
+ getUrl(path) {
32
+ return `${this.baseURL}${path}`;
33
+ }
34
+
35
+ /**
36
+ * Send api request
37
+ *
38
+ * @private
39
+ *
40
+ * @param {string} path
41
+ * @param {string} method
42
+ * @param {object} data
43
+ *
44
+ * @returns {object}
45
+ */
46
+ sendRequest(method, url, data = null) {
47
+ return data ? axios[method](url, data, this.config) : axios[method](url, this.config);
48
+ }
49
+
50
+ /**
51
+ * Get app by appKey
52
+ *
53
+ * @public
54
+ *
55
+ * @param {string} appKey
56
+ *
57
+ * @returns {object}
58
+ */
59
+ async getAppByKey(appKey) {
60
+ const url = this.getUrl(`/v2/apps?per_page=2&search=${appKey}`);
61
+ const response = await this.sendRequest('get', url);
62
+
63
+ const app = response.data.data[0];
64
+ if (!app || app.attributes.app_key != appKey) {
65
+ throw new Error(`App with key - ${appKey}, not exist`);
66
+ }
67
+
68
+ return app;
69
+ }
70
+
71
+ /**
72
+ * Get app datasets
73
+ *
74
+ * @public
75
+ *
76
+ * @param {string} appId
77
+ *
78
+ * @returns {object}
79
+ */
80
+ async getAppDatasetsOperationWrite(appId) {
81
+ const url = this.getUrl(
82
+ `/v2/apps/${appId}/app_datasets?dataset_operation=write&fields[]=app_dataset.dataset_name`
83
+ );
84
+ const response = await this.sendRequest('get', url);
85
+
86
+ if (!response.data.data.length) {
87
+ throw new Error(`App with ID - ${appId}, has not any datasets`);
88
+ }
89
+
90
+ return response.data.data;
91
+ }
92
+
93
+ /**
94
+ * Get asset details
95
+ *
96
+ * @public
97
+ *
98
+ * @param {string} assetId
99
+ *
100
+ * @returns {object[]}
101
+ */
102
+ async getAssetById(assetId) {
103
+ const url = this.getUrl(`/v2/assets/${assetId}`);
104
+ const response = await this.sendRequest('get', url);
105
+
106
+ return response.data.data;
107
+ }
108
+
109
+
110
+ /**
111
+ * Get asset streams
112
+ *
113
+ * @public
114
+ *
115
+ * @param {string} assetId
116
+ *
117
+ * @returns {object[]}
118
+ */
119
+ async getStreamByAssetId(assetId) {
120
+ const url = this.getUrl(`/v1/app_streams?asset_id=${assetId}&segment=drilling&status=complete`);
121
+ const response = await this.sendRequest('get', url);
122
+
123
+ if (!response.data[0]) {
124
+ throw new Error(`Could not found streams for asset ID - ${assetId}`);
125
+ }
126
+
127
+ return response.data;
128
+ }
129
+
130
+ /**
131
+ * Get wells data by asset id
132
+ *
133
+ * @public
134
+ *
135
+ * @param {string} assetId
136
+ *
137
+ * @returns {object[]}
138
+ */
139
+ async getWellByAssetId(assetId) {
140
+ const data = {
141
+ assets: [assetId],
142
+ };
143
+
144
+ const url = this.getUrl(`/v2/assets/resolve`);
145
+ const response = await this.sendRequest('post', url, data);
146
+
147
+ if (!response.data.wells.length) {
148
+ throw new Error(`Could not found wells by asset ID - ${assetId}`);
149
+ }
150
+
151
+ return response.data.wells;
152
+ }
153
+
154
+ /**
155
+ * Queue app run for create new task
156
+ *
157
+ * @public
158
+ *
159
+ * @param {string} appId
160
+ * @param {string} version
161
+ * @param {string} wellId
162
+ * @param {string[]} appDatasetsNames
163
+ * @param {string} streamId
164
+ *
165
+ * @returns {object}
166
+ */
167
+ async queueAppRun(appId, version, wellId, appDatasetsNames, streamId) {
168
+ const data = {
169
+ app_run: {
170
+ app_stream_id: streamId,
171
+ well_id: wellId,
172
+ app_version: version,
173
+ datasets: appDatasetsNames,
174
+ settings: {
175
+ end: 0,
176
+ interval: null,
177
+ invokes: null,
178
+ records_per_event: 300,
179
+ start: 0,
180
+ },
181
+ },
182
+ };
183
+
184
+ const url = this.getUrl(`/v2/apps/${appId}/app_runs`);
185
+ const response = await this.sendRequest('post', url, data);
186
+ return response.data.data;
187
+ }
188
+
189
+ /**
190
+ * Get all active(status - pending or in_progress or running) runs by app ID
191
+ *
192
+ * @public
193
+ *
194
+ * @param {string} appId
195
+ *
196
+ * @returns {object[]}
197
+ */
198
+ async getAppRuns(appId) {
199
+ const url = this.getUrl(`/v2/apps/${appId}/app_runs?page=1&per_page=500&status[]=pending&status[]=in_progress&status[]=running`);
200
+ const response = await this.sendRequest('get', url);
201
+
202
+ return response.data.data;
203
+ }
204
+
205
+ /**
206
+ * Upload packages
207
+ *
208
+ * @public
209
+ *
210
+ * @param {string} appKey
211
+ * @param {object} formData
212
+ *
213
+ * @returns
214
+ */
215
+ async uploadPackages(appKey, formData) {
216
+ const url = this.getUrl(`/v2/apps/${appKey}/packages/upload`);
217
+ this.config.headers = {
218
+ ...this.config.headers,
219
+ ...formData.getHeaders(),
220
+ };
221
+ return this.sendRequest('post', url, formData)
222
+ }
223
+ }
224
+
225
+ module.exports = { Api };
@@ -0,0 +1,10 @@
1
+ const { PREPARE_FLOW } = require('./prepare');
2
+ const { RERUN_STEPS } = require('./steps/rerun');
3
+ const { GET_RELEASE_CONFIG_STEP } = require('./steps/release-get-config');
4
+
5
+ const RERUN_FLOW = {
6
+ name: 'rerun',
7
+ steps: [PREPARE_FLOW, GET_RELEASE_CONFIG_STEP, ...RERUN_STEPS],
8
+ };
9
+
10
+ module.exports = { RERUN_FLOW };
@@ -1,31 +1,19 @@
1
1
  const { RELEASE } = require('../../constants/messages');
2
- const axios = require('axios');
3
2
  const FormData = require('form-data');
4
3
  const { resolve } = require('path');
5
4
  const { createReadStream } = require('fs');
6
5
  const { StepError } = require('../lib/step-error');
6
+ const { Api } = require('../lib/api');
7
7
  const { get } = require('lodash/fp');
8
8
 
9
9
  const UPLOAD_ZIP_TO_CORVA_STEP = {
10
10
  message: RELEASE.uploadApp,
11
11
  fn: async ({ zipFileName, appKey, CORVA_API_ENV, AUTH_TOKEN, API_KEY, dirName }) => {
12
12
  const form = new FormData();
13
-
14
13
  form.append('package', createReadStream(resolve(dirName, zipFileName)), 'archive.zip');
15
14
 
16
- const baseURL = `https://api${
17
- CORVA_API_ENV === 'production' ? '' : `.${CORVA_API_ENV}`
18
- }.corva.ai`;
19
- const uploadURL = `${baseURL}/v2/apps/${appKey}/packages/upload`;
20
-
21
- await axios
22
- .post(uploadURL, form, {
23
- headers: {
24
- ...form.getHeaders(),
25
- Authorization: AUTH_TOKEN ? `Bearer ${AUTH_TOKEN}` : `API ${API_KEY}`,
26
- },
27
- })
28
- .catch((e) => {
15
+ const api = new Api(CORVA_API_ENV, API_KEY, AUTH_TOKEN);
16
+ api.uploadPackages(appKey, form).catch((e) => {
29
17
  throw new StepError(
30
18
  `${get('response.data.message', e) || ''} \nPOST: ${uploadURL} failed.`,
31
19
  { cause: e }
@@ -0,0 +1,51 @@
1
+ const _ = require('lodash');
2
+ const chalk = require('chalk');
3
+
4
+ const CREATE_TASK_STEP = {
5
+ message: 'Creating tasks...',
6
+ fn: async ({
7
+ appId,
8
+ assets,
9
+ appDatasetsNames,
10
+ mappedAssetsToStreams,
11
+ mappedAssetsToWells,
12
+ api,
13
+ version,
14
+ CORVA_API_ENV,
15
+ }) => {
16
+ if (!assets.length) {
17
+ process.stdout.write(
18
+ `\n\n${chalk.yellow.bold('There is not a asset ID to create new task')}`
19
+ );
20
+ }
21
+
22
+ for (const assetId of assets) {
23
+
24
+ try {
25
+ const result = await api.queueAppRun(
26
+ appId,
27
+ version,
28
+ mappedAssetsToWells.get(parseInt(assetId)),
29
+ appDatasetsNames,
30
+ mappedAssetsToStreams.get(parseInt(assetId))
31
+ );
32
+
33
+ process.stdout.write(
34
+ `\n Created new task with ID ${chalk.yellow(result.id)} for asset ID - ${chalk.green(
35
+ assetId
36
+ )}`
37
+ );
38
+
39
+ process.stdout.write(
40
+ `\n Task link - https://app${CORVA_API_ENV === 'production' ? '.' : `.${CORVA_API_ENV}.`}corva.ai/dev-center/apps/${appId}/runner`
41
+ );
42
+ } catch (e) {
43
+ process.stdout.write(
44
+ `\n\n${chalk.red.underline.bold(`Could not rerun app for asset ID - ${assetId}, an error occured: ${e.message}`)}\n\n`
45
+ );
46
+ }
47
+ }
48
+ },
49
+ };
50
+
51
+ module.exports = { CREATE_TASK_STEP };
@@ -0,0 +1,243 @@
1
+ const _ = require('lodash');
2
+ const chalk = require('chalk');
3
+ const inquirer = require('inquirer');
4
+ const { Api } = require('../lib/api');
5
+ const { StepError } = require('../lib/step-error');
6
+
7
+ const MAX_ASSET_IDS_COUNT = 10;
8
+
9
+ const PREPARE_DATA_STEP = {
10
+ message: 'Preparing data for tasks...',
11
+ fn: async (context) => {
12
+ const { manifest, patterns, API_KEY, CORVA_API_ENV, pkg } = context;
13
+ let { assets } = patterns;
14
+
15
+ assets = _.uniq(assets);
16
+
17
+ if (assets.length > MAX_ASSET_IDS_COUNT) {
18
+ throw new StepError(`Please no more than ${MAX_ASSET_IDS_COUNT} asset ids!`);
19
+ }
20
+
21
+ if (!['scheduler', 'stream'].includes(manifest.manifest.application.type)) {
22
+ throw new StepError('Rerun command supports only "scheduler" or "stream" apps');
23
+ }
24
+
25
+ if (!await promptAreYouSure()) {
26
+ throw new StepError('Command stopped');
27
+ }
28
+
29
+ const api = new Api(CORVA_API_ENV, API_KEY);
30
+
31
+ const app = await api.getAppByKey(manifest.manifest.application.key.toLowerCase());
32
+ const appDatasets = await api.getAppDatasetsOperationWrite(app.id);
33
+ const appDatasetsNames = appDatasets.map((dataset) => dataset.attributes.dataset_name);
34
+
35
+ const existingAppRuns = await getExistAppRuns(app.id, assets, appDatasetsNames, api);
36
+ if (existingAppRuns.size) {
37
+ for (let appRunAssetId of existingAppRuns.keys()) {
38
+ // remove asset ID if similar rerun already exists
39
+ assets = assets.filter((assetId) => appRunAssetId != assetId);
40
+
41
+ const run = existingAppRuns.get(appRunAssetId);
42
+
43
+ process.stdout.write(
44
+ `\n\n${chalk.yellow.bold(`Similar rerun with ID ${run.id}, for asset ID ${appRunAssetId} - already exist. Will be skipped!`)}`
45
+ );
46
+ }
47
+ }
48
+
49
+ const {mappedAssetsToWells, mappedAssetsToStreams, assetsToDelete} = await prepareWellAndStreamData(assets, api);
50
+
51
+ if (assetsToDelete.length) {
52
+ // remove asset ID if could not found stream or well
53
+ assets = assets.filter((assetId) => !assetsToDelete.includes(assetId));
54
+ }
55
+
56
+ return {
57
+ appId: app.id,
58
+ assets,
59
+ appDatasetsNames,
60
+ mappedAssetsToStreams,
61
+ mappedAssetsToWells,
62
+ api,
63
+ CORVA_API_ENV,
64
+ version: pkg.version,
65
+ };
66
+ },
67
+ };
68
+
69
+ /**
70
+ * CLI prompt question - Are you sure you want to do this?
71
+ *
72
+ * @returns
73
+ */
74
+ const promptAreYouSure = async () => {
75
+ const answers = await inquirer
76
+ .prompt([
77
+ {
78
+ message:
79
+ '\n This command will create additional load on the server, which may take a long time. \n Are you sure you want to do this?',
80
+ name: 'option',
81
+ type: 'list',
82
+ choices: [
83
+ { value: true, name: 'Yes' },
84
+ { value: false, name: 'No' },
85
+ ],
86
+ default: false,
87
+ },
88
+ ]);
89
+ return answers.option;
90
+ }
91
+
92
+ /**
93
+ * CLI prompt question - Please choose the stream
94
+ *
95
+ * @param {object[]} streams
96
+ *
97
+ * @returns
98
+ */
99
+ const getStreamWithPrompt = async (streams) => {
100
+ let stream = {};
101
+
102
+ const choices = streams.map((stream) => {
103
+ return {
104
+ value: stream,
105
+ name: stream.name
106
+ };
107
+ });
108
+
109
+ await inquirer
110
+ .prompt([
111
+ {
112
+ message:
113
+ '\n Please choose stream?',
114
+ name: 'option',
115
+ type: 'list',
116
+ choices,
117
+ },
118
+ ])
119
+ .then((answers) => {
120
+ stream = answers.option;
121
+ });
122
+
123
+ return stream;
124
+ }
125
+
126
+ /**
127
+ * CLI prompt question - Please choose the well
128
+ *
129
+ * @param {object[]} wells
130
+ * @param {object} api
131
+ *
132
+ * @returns
133
+ */
134
+ const getWellWithPrompt = async (wells, api) => {
135
+ let well = {};
136
+
137
+ const choices = await Promise.all(wells.map( async (well) => {
138
+ const assetDetails = await api.getAssetById(well.data.attributes.asset_id);
139
+ well.data.name = assetDetails.attributes.name;
140
+ return {
141
+ value: well.data,
142
+ name: well.data.name,
143
+ };
144
+ }));
145
+
146
+ await inquirer
147
+ .prompt([
148
+ {
149
+ message:
150
+ '\n Please choose the well?',
151
+ name: 'option',
152
+ type: 'list',
153
+ choices,
154
+ },
155
+ ])
156
+ .then((answers) => {
157
+ well = answers.option;
158
+ });
159
+
160
+ return well;
161
+ }
162
+
163
+
164
+ /**
165
+ * Get stream and well data for assets
166
+ *
167
+ * @param {string[]} assets
168
+ * @param {object} api
169
+ *
170
+ * @returns {object}
171
+ */
172
+ const prepareWellAndStreamData = async (assets, api) => {
173
+ const mappedAssetsToWells = new Map();
174
+ const mappedAssetsToStreams = new Map();
175
+ const assetsToDelete = [];
176
+
177
+ for (const assetId of assets) {
178
+ try {
179
+ process.stdout.write(
180
+ `\n\n${chalk.black.underline.bold(`Process asset ID - ${chalk.green(assetId)}`)}`
181
+ );
182
+
183
+ process.stdout.write(
184
+ '\n Loading wells...'
185
+ );
186
+
187
+ const wells = await api.getWellByAssetId(assetId);
188
+ const well = await getWellWithPrompt(wells, api);
189
+ mappedAssetsToWells.set(well.attributes.asset_id, well.id);
190
+
191
+ process.stdout.write(
192
+ '\n Loading streams...'
193
+ );
194
+
195
+ const streams = await api.getStreamByAssetId(assetId);
196
+ const stream = await getStreamWithPrompt(streams);
197
+ mappedAssetsToStreams.set(stream.asset_id, stream.id);
198
+ } catch (e) {
199
+ process.stdout.write(
200
+ `\n\n${chalk.red.underline.bold(`Skipped the asset ID - ${assetId}, an error occured: ${e.message}`)}`
201
+ );
202
+
203
+ assetsToDelete.push(assetId);
204
+
205
+ if (mappedAssetsToWells.has(assetId)) mappedAssetsToWells.delete(assetId);
206
+ if (mappedAssetsToStreams.has(assetId)) mappedAssetsToStreams.delete(assetId);
207
+ }
208
+ }
209
+
210
+ return {mappedAssetsToWells, mappedAssetsToStreams, assetsToDelete};
211
+ }
212
+
213
+ /**
214
+ * Check if current runs already exist
215
+ *
216
+ * @param {string} appId
217
+ * @param {string[]} assets
218
+ * @param {string[]} appDatasetsNames
219
+ * @param {object} api
220
+ *
221
+ * @returns {map}
222
+ */
223
+ const getExistAppRuns = async (appId, assets, appDatasetsNames, api) => {
224
+ const appRuns = new Map();
225
+ const existingAppRuns = await api.getAppRuns(appId);
226
+
227
+ // go through all assets ids
228
+ for (const assetId of assets) {
229
+ const runsForCurrentAsset = existingAppRuns.filter((run) => run.attributes.well_asset_id == assetId);
230
+ // go through all existing runs for current asset ID
231
+ for (const run of runsForCurrentAsset) {
232
+ const currentAppDatasetsNames = run.attributes.app_run_datasets.map((dataset) => dataset.name);
233
+ // if datasets names matched for the same asset ID - then run already exist
234
+ if (appDatasetsNames.sort().toString() === currentAppDatasetsNames.sort().toString()) {
235
+ appRuns.set(assetId, run);
236
+ }
237
+ }
238
+ }
239
+
240
+ return appRuns;
241
+ };
242
+
243
+ module.exports = { PREPARE_DATA_STEP };
@@ -0,0 +1,8 @@
1
+ const { PREPARE_DATA_STEP } = require('./rerun-prepare-data');
2
+ const { CREATE_TASK_STEP } = require('./rerun-create-task');
3
+
4
+ const RERUN_STEPS = [PREPARE_DATA_STEP, CREATE_TASK_STEP];
5
+
6
+ module.exports = {
7
+ RERUN_STEPS,
8
+ };
package/lib/index.js CHANGED
@@ -30,6 +30,7 @@ const { runFlow } = require('./flow');
30
30
  const { ERROR_ICON } = require('./constants/messages');
31
31
  const { StepError } = require('./flows/lib/step-error');
32
32
  const { RELEASE_FLOW } = require('./flows/release');
33
+ const { RERUN_FLOW } = require('./flows/rerun');
33
34
  const { ZIP_FLOW } = require('./flows/zip');
34
35
  const { bumpVersionOptionDeprecated, bumpVersionOption } = require('./bump-version.option');
35
36
  const { Manifest } = require('./flows/lib/manifest');
@@ -198,6 +199,15 @@ async function initialChecks() {
198
199
  return runFlow(RELEASE_FLOW, { dirName, patterns, options });
199
200
  });
200
201
 
202
+ program
203
+ .command('rerun')
204
+ .description('Rerun app')
205
+ .argument('<project-directory>', 'Project directory to work with')
206
+ .addOption(new Option('--assets [assets...]', 'Assets ids list', []))
207
+ .action(async (dirName, patterns, options) => {
208
+ return runFlow(RERUN_FLOW, { dirName, patterns, options });
209
+ });
210
+
201
211
  try {
202
212
  await program.parseAsync(process.argv);
203
213
  } catch (e) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@corva/create-app",
3
- "version": "0.39.0-2",
3
+ "version": "0.40.0-0",
4
4
  "private": false,
5
5
  "description": "Create app to use it in CORVA.AI",
6
6
  "keywords": [