@nzz/q-cli 1.6.1 → 1.7.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
@@ -164,7 +164,11 @@ Q new-custom-code my-project-name
164
164
  Q new-custom-code my-project-name -d my-project-directory
165
165
  ```
166
166
 
167
- ### Updating existing Q items
167
+ ### Q item actions
168
+
169
+ The `Q` cli can copy and/or update existing Q items.
170
+
171
+ #### Updating existing Q items
168
172
 
169
173
  Once `Q` cli installed one can update one or many Q items by executing:
170
174
 
@@ -193,63 +197,116 @@ Q update-item -r
193
197
  - Credentials can be provided as environment variables to avoid user prompts. The variable names are `Q_ENV_SERVER`, `Q_ENV_USERNAME`, `Q_ENV_PASSWORD`, `Q_ENV_ACCESSTOKEN`, where `ENV` is the uppercase version of the environment name.
194
198
 
195
199
  ```bash
196
- Q_TEST_SERVER=https://q-server.st-test.nzz.ch/ Q_TEST_USERNAME=[username] Q_TEST_PASSWORD=[password] Q update-item
200
+ Q_TEST_SERVER=[server_route] Q_TEST_USERNAME=[username] Q_TEST_PASSWORD=[password] Q update-item
197
201
  ```
198
202
 
199
203
  or
200
204
 
201
205
  ```bash
202
- Q_TEST_SERVER=https://q-server.st-test.nzz.ch/ Q_TEST_ACCESSTOKEN=[accessToken] Q update-item
206
+ Q_TEST_SERVER=[server_route] Q_TEST_ACCESSTOKEN=[accessToken] Q update-item
203
207
  ```
204
208
 
205
- Alternatively
206
-
207
- The config file has to follow [this json-schema](./bin/commands/updateItem/schema.json). Here an example:
209
+ The config file has to follow [this json-schema](./bin/commands/qItem/updateItem/updateSchema.json). This schema will be extended by the respective tool schema of your Q item.
210
+ Here's an example:
208
211
 
209
212
  ```json
210
213
  {
211
214
  "items": [
212
215
  {
213
216
  "environments": [
217
+ // "environments" references the desired q items to be updated, at least 1 environment is required
214
218
  {
215
219
  "name": "production",
216
- "id": "6dcf203a5c5f74b61aeea0cb0eef7e0b"
220
+ "id": "6dcf203a5c5f74b61aeea0cb0eef7e0b" // Id of your q item in the production environment
217
221
  },
218
222
  {
219
223
  "name": "staging",
220
- "id": "6dcf203a5c5f74b61aeea0cb0ef2ca9f"
224
+ "id": "6dcf203a5c5f74b61aeea0cb0ef2ca9f" // Id of your q item in the staging environment
221
225
  }
222
226
  ],
223
227
  "item": {
228
+ // The actual content you want to update for your referenced q items listed in "environments"
224
229
  "title": "Der Konsum in der Schweiz springt wieder an",
225
230
  "subtitle": "Wöchentliche Ausgaben mittels Bankkarten in Mio. Fr. im Jahr 2020, zum Vergleich 2019",
226
231
  "data": [
232
+ // "data" represents the data table of your q item inside the q-editor
227
233
  ["Datum", "2020", "2019"],
228
234
  ["2020-01-06", "690004302", "641528028"],
229
235
  ["2020-01-13", "662122373", "617653790"],
230
236
  ["2020-01-20", "688208667", "654303249"]
231
237
  ]
232
238
  }
233
- },
239
+ }
240
+ ]
241
+ }
242
+ ```
243
+
244
+ #### Copy existing Q items
245
+
246
+ Once `Q` cli installed one can copy one or many Q items by executing:
247
+
248
+ ```bash
249
+ Q copy-item
250
+ ```
251
+
252
+ - The path to the config file can be set by using option `-c` or `--config`. By default the `copy-item` command will look for a config file called `q.config.json` in the current directory
253
+
254
+ ```bash
255
+ Q copy-item -c [path]
256
+ ```
257
+
258
+ - Items of a specified environment can be updated by using the option `-e` or `--environment`. By default the `copy-item` command updates all item specified in the config file
259
+
260
+ ```bash
261
+ Q copy-item -e [env]
262
+ ```
263
+
264
+ - Stored configuration properties like Q-Server url or access tokens can be reset by using option `-r` or `--reset`
265
+
266
+ ```bash
267
+ Q copy-item -r
268
+ ```
269
+
270
+ - Credentials can be provided as environment variables to avoid user prompts. The variable names are `Q_ENV_SERVER`, `Q_ENV_USERNAME`, `Q_ENV_PASSWORD`, `Q_ENV_ACCESSTOKEN`, where `ENV` is the uppercase version of the environment name.
271
+
272
+ ```bash
273
+ Q_TEST_SERVER=[server_route] Q_TEST_USERNAME=[username] Q_TEST_PASSWORD=[password] Q update-item
274
+ ```
275
+
276
+ or
277
+
278
+ ```bash
279
+ Q_TEST_SERVER=[server_route] Q_TEST_ACCESSTOKEN=[accessToken] Q update-item
280
+ ```
281
+
282
+ The config file has to follow [this json-schema](./bin/commands/qItem/updateItem/updateSchema.json). This schema will be extended by the respective tool schema of your Q item.
283
+ Here's an example:
284
+
285
+ ```json
286
+ {
287
+ "items": [
234
288
  {
235
289
  "environments": [
236
290
  {
237
291
  "name": "production",
238
- "id": "6dcf203a5c5f74b61aeea0cb0ef2edea"
292
+ "id": "6dcf203a5c5f74b61aeea0cb0eef7e0b" // Id of your q item in the production environment
239
293
  },
240
294
  {
241
295
  "name": "staging",
242
- "id": "6dcf203a5c5f74b61aeea0cb0ef68480"
296
+ "id": "6dcf203a5c5f74b61aeea0cb0ef2ca9f" // Id of your q item in the staging environment
243
297
  }
244
298
  ],
245
299
  "item": {
246
- "title": "Der Lastwagenverkehr in Deutschland nimmt wieder zu",
247
- "subtitle": "Täglicher Lkw-Maut-Fahrleistungsindex (2015 = 100, saison- und kalenderbereinigt) im Jahr 2020, zum Vergleich 2019\t\t",
248
- "data": [
249
- ["Datum", "2020", "2019"],
250
- ["2020-01-07", "105.9", "112.1"],
251
- ["2020-01-08", "108.9", "111.4"],
252
- ["2020-01-09", "112.2", "113.5"]
300
+ "title": "Russische Angriffe auf die Ukraine",
301
+ "subtitle": "Verzeichnete Angriffe in der ganzen Ukraine",
302
+ "files": [
303
+ // Adds or overwrites the listed files in your q item
304
+ {
305
+ "loadSyncBeforeInit": false, // Has to be set for the file upload to work
306
+ "file": {
307
+ "path": "./angriffsFlaechen.json" // Your local path to your file. The path is relative to where you execute the command.
308
+ }
309
+ }
253
310
  ]
254
311
  }
255
312
  }
@@ -257,8 +314,6 @@ The config file has to follow [this json-schema](./bin/commands/updateItem/schem
257
314
  }
258
315
  ```
259
316
 
260
- The configuration object has a property `items` which contains an object for each Q item. A Q item has a property `environments` and `item`. The `environments` array contains an objects with properties `name` and `id` for each environment the item is deployed on. The `item` contains the data of the Q item. The structure of the item can vary between each graphic type (chart, map, table ect.).
261
-
262
317
  [to the top](#table-of-contents)
263
318
 
264
319
  ## License
@@ -0,0 +1,142 @@
1
+ const helpers = require("./helpers.js");
2
+ const promptly = require("promptly");
3
+ const Configstore = require("configstore");
4
+ const package = require("./../../../package.json");
5
+ const configStore = new Configstore(package.name, {});
6
+
7
+ async function setAuthenticationConfig(environment, qServer) {
8
+ const result = await authenticate(environment, qServer);
9
+ configStore.set(`${environment}.accessToken`, result.accessToken);
10
+ configStore.set(`${environment}.cookie`, result.cookie);
11
+ }
12
+
13
+ async function setupStore(qConfig, environmentFilter, reset) {
14
+ if (reset) {
15
+ configStore.clear();
16
+ }
17
+ for (const environment of helpers.getEnvironments(
18
+ qConfig,
19
+ environmentFilter
20
+ )) {
21
+ await setupConfigFromEnvVars(environment);
22
+
23
+ if (!configStore.get(`${environment}.qServer`)) {
24
+ const qServer = await promptly.prompt(
25
+ `Enter the Q-Server url for ${environment} environment: `,
26
+ {
27
+ validator: (qServer) => {
28
+ return new URL(qServer).toString();
29
+ },
30
+ retry: true,
31
+ }
32
+ );
33
+ configStore.set(`${environment}.qServer`, qServer);
34
+ }
35
+
36
+ const qServer = configStore.get(`${environment}.qServer`);
37
+ if (!configStore.get(`${environment}.accessToken`)) {
38
+ await setAuthenticationConfig(environment, qServer);
39
+ }
40
+
41
+ const accessToken = configStore.get(`${environment}.accessToken`);
42
+ const cookie = configStore.get(`${environment}.cookie`);
43
+ const isAccessTokenValid = await helpers.checkValidityOfAccessToken(
44
+ environment,
45
+ qServer,
46
+ accessToken,
47
+ cookie
48
+ );
49
+
50
+ // Get a new access token in case its not valid anymore
51
+ if (!isAccessTokenValid) {
52
+ await setAuthenticationConfig(environment, qServer);
53
+ }
54
+ }
55
+
56
+ return configStore;
57
+ }
58
+
59
+ async function setupConfigFromEnvVars(environment) {
60
+ const environmentPrefix = environment.toUpperCase();
61
+
62
+ const qServer = process.env[`Q_${environmentPrefix}_SERVER`];
63
+ if (qServer) {
64
+ configStore.set(`${environment}.qServer`, qServer);
65
+ }
66
+ const accessToken = process.env[`Q_${environmentPrefix}_ACCESSTOKEN`];
67
+ const username = process.env[`Q_${environmentPrefix}_USERNAME`];
68
+ const password = process.env[`Q_${environmentPrefix}_PASSWORD`];
69
+ if (qServer && accessToken) {
70
+ configStore.set(`${environment}.accessToken`, accessToken);
71
+ } else if (qServer && username && password) {
72
+ const cookie = configStore.get(`${environment}.cookie`);
73
+ const result = await helpers.getAccessToken(
74
+ environment,
75
+ qServer,
76
+ username,
77
+ password,
78
+ cookie
79
+ );
80
+
81
+ if (!result) {
82
+ console.error(
83
+ errorColor(
84
+ `A problem occured while authenticating to the ${environment} environment using environment variables. Please check your credentials and try again.`
85
+ )
86
+ );
87
+ process.exit(1);
88
+ }
89
+
90
+ configStore.set(`${environment}.accessToken`, result.accessToken);
91
+ configStore.set(`${environment}.cookie`, result.cookie);
92
+ }
93
+ }
94
+
95
+ async function authenticate(environment, qServer) {
96
+ let username = configStore.get(`${environment}.username`);
97
+ if (!username) {
98
+ username = await promptly.prompt(
99
+ `Enter your username on ${environment} environment: `,
100
+ { validator: (username) => username.trim() }
101
+ );
102
+ configStore.set(`${environment}.username`, username);
103
+ }
104
+
105
+ const password = await promptly.password(
106
+ `Enter your password on ${environment} environment: `,
107
+ {
108
+ validator: async (password) => password.trim(),
109
+ replace: "*",
110
+ }
111
+ );
112
+
113
+ const cookie = configStore.get(`${environment}.cookie`);
114
+ let result = await helpers.getAccessToken(
115
+ environment,
116
+ qServer,
117
+ username,
118
+ password,
119
+ cookie
120
+ );
121
+
122
+ while (!result) {
123
+ console.error(
124
+ errorColor(
125
+ "A problem occured while authenticating. Please check your credentials and try again."
126
+ )
127
+ );
128
+
129
+ result = await authenticate(environment, qServer);
130
+
131
+ if (result.accessToken) {
132
+ break;
133
+ }
134
+ }
135
+
136
+ return result;
137
+ }
138
+
139
+ module.exports = {
140
+ store: configStore,
141
+ setupStore: setupStore,
142
+ };
@@ -0,0 +1,103 @@
1
+ const schemaService = require("./../schemaService.js");
2
+ const configStore = require("./../configStore.js");
3
+ const itemService = require("./../itemService.js");
4
+ const fs = require("fs");
5
+ const path = require("path");
6
+ const chalk = require("chalk");
7
+ const errorColor = chalk.red;
8
+ const successColor = chalk.green;
9
+
10
+ module.exports = async function (command) {
11
+ try {
12
+ const qConfigPath = path.resolve(command.config);
13
+
14
+ if (fs.existsSync(qConfigPath)) {
15
+ const qConfig = JSON.parse(fs.readFileSync(qConfigPath));
16
+ const validationResult = schemaService.validateConfig(
17
+ qConfig,
18
+ "copyItem"
19
+ );
20
+
21
+ if (validationResult.isValid) {
22
+ const config = await configStore.setupStore(
23
+ qConfig,
24
+ command.environment,
25
+ command.reset
26
+ );
27
+
28
+ for (const item of itemService.getItems(qConfig, command.environment)) {
29
+ for (const environment of item.environments) {
30
+ const qServer = config.get(`${environment.name}.qServer`);
31
+ const accessToken = config.get(`${environment.name}.accessToken`);
32
+ const cookie = config.get(`${environment.name}.cookie`);
33
+
34
+ const existingItem = await itemService.getItem(
35
+ qServer,
36
+ environment,
37
+ accessToken,
38
+ cookie
39
+ );
40
+
41
+ delete existingItem.updatedBy;
42
+ delete existingItem.createdBy;
43
+ delete existingItem.createdDate;
44
+ delete existingItem._id;
45
+ delete existingItem._rev;
46
+
47
+ let newItem = await itemService.createItem(
48
+ existingItem,
49
+ environment,
50
+ config
51
+ );
52
+ // Save for success message
53
+ const newItemId = newItem._id;
54
+ const existingItemId = environment.id;
55
+
56
+ const hasOverwrites =
57
+ item.item &&
58
+ Object.keys(item.item).length > 0 &&
59
+ Object.getPrototypeOf(item.item) === Object.prototype;
60
+
61
+ if (hasOverwrites) {
62
+ environment.id = newItemId;
63
+
64
+ newItem = await itemService.updateItem(
65
+ item.item,
66
+ environment,
67
+ config,
68
+ qConfigPath
69
+ );
70
+ }
71
+
72
+ if (newItem) {
73
+ console.log(
74
+ successColor(
75
+ `Successfully copied item with id ${existingItemId} on ${environment.name} environment. Copied item id ${newItemId}`
76
+ )
77
+ );
78
+ }
79
+ }
80
+ }
81
+ } else {
82
+ console.error(
83
+ errorColor(
84
+ `A problem occured while validating the config file: ${validationResult.errorsText}`
85
+ )
86
+ );
87
+ process.exit(1);
88
+ }
89
+ } else {
90
+ console.error(
91
+ errorColor(
92
+ "Couldn't find config file named q.config.json in the current directory. Create a config file in the current directory or pass the path to the config file with the option -c <path>"
93
+ )
94
+ );
95
+ }
96
+ } catch (error) {
97
+ console.error(
98
+ errorColor(
99
+ `A problem occured while parsing the config file at ${command.config}. Please make sure it is valid JSON.`
100
+ )
101
+ );
102
+ }
103
+ };
@@ -0,0 +1,37 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "title": "Q Config",
4
+ "description": "Config used by the Q CLI to copy items",
5
+ "type": "object",
6
+ "properties": {
7
+ "items": {
8
+ "description": "Array of Q items",
9
+ "type": "array",
10
+ "minItems": 1,
11
+ "items": {
12
+ "type": "object",
13
+ "properties": {
14
+ "environments": {
15
+ "type": "array",
16
+ "minItems": 1,
17
+ "items": {
18
+ "type": "object",
19
+ "properties": {
20
+ "id": {
21
+ "type": "string",
22
+ "description": "Id of Q item"
23
+ }
24
+ },
25
+ "required": ["id"]
26
+ }
27
+ },
28
+ "item": {
29
+ "type": "object"
30
+ }
31
+ },
32
+ "required": ["environments", "item"]
33
+ }
34
+ }
35
+ },
36
+ "required": ["items"]
37
+ }
@@ -0,0 +1,102 @@
1
+ const fetch = require("node-fetch");
2
+ const chalk = require("chalk");
3
+ const errorColor = chalk.red;
4
+
5
+ function getEnvironments(qConfig, environmentFilter) {
6
+ try {
7
+ const environments = new Set();
8
+ for (const item of qConfig.items) {
9
+ for (const environment of item.environments) {
10
+ if (environmentFilter) {
11
+ if (environmentFilter === environment.name) {
12
+ environments.add(environment.name);
13
+ }
14
+ } else {
15
+ environments.add(environment.name);
16
+ }
17
+ }
18
+ }
19
+
20
+ if (environments.size > 0) {
21
+ return Array.from(environments);
22
+ } else {
23
+ throw new Error(
24
+ `No items with environment ${environmentFilter} found. Please check your configuration and try again.`
25
+ );
26
+ }
27
+ } catch (error) {
28
+ console.error(errorColor(error.message));
29
+ process.exit(1);
30
+ }
31
+ }
32
+
33
+ async function getAccessToken(
34
+ environment,
35
+ qServer,
36
+ username,
37
+ password,
38
+ cookie
39
+ ) {
40
+ try {
41
+ const response = await fetch(`${qServer}authenticate`, {
42
+ method: "POST",
43
+ headers: {
44
+ "user-agent": "Q Command-line Tool",
45
+ origin: qServer,
46
+ Cookie: cookie ? cookie : "",
47
+ },
48
+ body: JSON.stringify({
49
+ username: username,
50
+ password: password,
51
+ }),
52
+ });
53
+
54
+ if (response.ok) {
55
+ const body = await response.json();
56
+ return {
57
+ accessToken: body.access_token,
58
+ cookie: response.headers.get("set-cookie"),
59
+ };
60
+ }
61
+
62
+ return false;
63
+ } catch (error) {
64
+ console.error(
65
+ errorColor(
66
+ `A problem occured while authenticating on ${environment} environment. Please check your connection and try again.`
67
+ )
68
+ );
69
+ process.exit(1);
70
+ }
71
+ }
72
+
73
+ async function checkValidityOfAccessToken(
74
+ environment,
75
+ qServer,
76
+ accessToken,
77
+ cookie
78
+ ) {
79
+ try {
80
+ const response = await fetch(`${qServer}user`, {
81
+ headers: {
82
+ "user-agent": "Q Command-line Tool",
83
+ Authorization: `Bearer ${accessToken}`,
84
+ Cookie: cookie ? cookie : "",
85
+ },
86
+ });
87
+ return response.ok;
88
+ } catch (error) {
89
+ console.error(
90
+ errorColor(
91
+ `A problem occured while checking the validity of your access token on ${environment} environment. Please check your connection and try again.`
92
+ )
93
+ );
94
+ process.exit(1);
95
+ }
96
+ }
97
+
98
+ module.exports = {
99
+ getEnvironments: getEnvironments,
100
+ getAccessToken,
101
+ checkValidityOfAccessToken,
102
+ };