@nzz/q-cli 1.6.0 → 1.8.0

Sign up to get free protection for your applications and to get access to all the features.
package/README.md CHANGED
@@ -164,7 +164,29 @@ 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
+ ### Creating new ed-tech utility package project
168
+
169
+ Once `Q` cli is installed one can create the skeleton of a new ed-tech utility package project by executing
170
+
171
+ ```bash
172
+ Q new-et-utils-package package-name package-author package-description
173
+ ```
174
+
175
+ - The directory name where the new ed-tech utility package project is being created defaults to the project name and can be overwritten by using option `-d` or `--dir`
176
+
177
+ ```bash
178
+ Q new-et-utils-package package-name -d my-project-directory
179
+ ```
180
+
181
+ #### Notes
182
+
183
+ New utility package projects should only be created inside the [ed-tech-utilities](https://github.com/nzzdev/ed-tech-utilities) repository.
184
+
185
+ ### Q item actions
186
+
187
+ The `Q` cli can copy and/or update existing Q items.
188
+
189
+ #### Updating existing Q items
168
190
 
169
191
  Once `Q` cli installed one can update one or many Q items by executing:
170
192
 
@@ -193,63 +215,116 @@ Q update-item -r
193
215
  - 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
216
 
195
217
  ```bash
196
- Q_TEST_SERVER=https://q-server.st-test.nzz.ch/ Q_TEST_USERNAME=[username] Q_TEST_PASSWORD=[password] Q update-item
218
+ Q_TEST_SERVER=[server_route] Q_TEST_USERNAME=[username] Q_TEST_PASSWORD=[password] Q update-item
197
219
  ```
198
220
 
199
221
  or
200
222
 
201
223
  ```bash
202
- Q_TEST_SERVER=https://q-server.st-test.nzz.ch/ Q_TEST_ACCESSTOKEN=[accessToken] Q update-item
224
+ Q_TEST_SERVER=[server_route] Q_TEST_ACCESSTOKEN=[accessToken] Q update-item
203
225
  ```
204
226
 
205
- Alternatively
206
-
207
- The config file has to follow [this json-schema](./bin/commands/updateItem/schema.json). Here an example:
227
+ 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.
228
+ Here's an example:
208
229
 
209
230
  ```json
210
231
  {
211
232
  "items": [
212
233
  {
213
234
  "environments": [
235
+ // "environments" references the desired q items to be updated, at least 1 environment is required
214
236
  {
215
237
  "name": "production",
216
- "id": "6dcf203a5c5f74b61aeea0cb0eef7e0b"
238
+ "id": "6dcf203a5c5f74b61aeea0cb0eef7e0b" // Id of your q item in the production environment
217
239
  },
218
240
  {
219
241
  "name": "staging",
220
- "id": "6dcf203a5c5f74b61aeea0cb0ef2ca9f"
242
+ "id": "6dcf203a5c5f74b61aeea0cb0ef2ca9f" // Id of your q item in the staging environment
221
243
  }
222
244
  ],
223
245
  "item": {
246
+ // The actual content you want to update for your referenced q items listed in "environments"
224
247
  "title": "Der Konsum in der Schweiz springt wieder an",
225
248
  "subtitle": "Wöchentliche Ausgaben mittels Bankkarten in Mio. Fr. im Jahr 2020, zum Vergleich 2019",
226
249
  "data": [
250
+ // "data" represents the data table of your q item inside the q-editor
227
251
  ["Datum", "2020", "2019"],
228
252
  ["2020-01-06", "690004302", "641528028"],
229
253
  ["2020-01-13", "662122373", "617653790"],
230
254
  ["2020-01-20", "688208667", "654303249"]
231
255
  ]
232
256
  }
233
- },
257
+ }
258
+ ]
259
+ }
260
+ ```
261
+
262
+ #### Copy existing Q items
263
+
264
+ Once `Q` cli installed one can copy one or many Q items by executing:
265
+
266
+ ```bash
267
+ Q copy-item
268
+ ```
269
+
270
+ - 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
271
+
272
+ ```bash
273
+ Q copy-item -c [path]
274
+ ```
275
+
276
+ - 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
277
+
278
+ ```bash
279
+ Q copy-item -e [env]
280
+ ```
281
+
282
+ - Stored configuration properties like Q-Server url or access tokens can be reset by using option `-r` or `--reset`
283
+
284
+ ```bash
285
+ Q copy-item -r
286
+ ```
287
+
288
+ - 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.
289
+
290
+ ```bash
291
+ Q_TEST_SERVER=[server_route] Q_TEST_USERNAME=[username] Q_TEST_PASSWORD=[password] Q update-item
292
+ ```
293
+
294
+ or
295
+
296
+ ```bash
297
+ Q_TEST_SERVER=[server_route] Q_TEST_ACCESSTOKEN=[accessToken] Q update-item
298
+ ```
299
+
300
+ 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.
301
+ Here's an example:
302
+
303
+ ```json
304
+ {
305
+ "items": [
234
306
  {
235
307
  "environments": [
236
308
  {
237
309
  "name": "production",
238
- "id": "6dcf203a5c5f74b61aeea0cb0ef2edea"
310
+ "id": "6dcf203a5c5f74b61aeea0cb0eef7e0b" // Id of your q item in the production environment
239
311
  },
240
312
  {
241
313
  "name": "staging",
242
- "id": "6dcf203a5c5f74b61aeea0cb0ef68480"
314
+ "id": "6dcf203a5c5f74b61aeea0cb0ef2ca9f" // Id of your q item in the staging environment
243
315
  }
244
316
  ],
245
317
  "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"]
318
+ "title": "Russische Angriffe auf die Ukraine",
319
+ "subtitle": "Verzeichnete Angriffe in der ganzen Ukraine",
320
+ "files": [
321
+ // Adds or overwrites the listed files in your q item
322
+ {
323
+ "loadSyncBeforeInit": false, // Has to be set for the file upload to work
324
+ "file": {
325
+ "path": "./angriffsFlaechen.json" // Your local path to your file. The path is relative to where you execute the command.
326
+ }
327
+ }
253
328
  ]
254
329
  }
255
330
  }
@@ -257,8 +332,6 @@ The config file has to follow [this json-schema](./bin/commands/updateItem/schem
257
332
  }
258
333
  ```
259
334
 
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
335
  [to the top](#table-of-contents)
263
336
 
264
337
  ## License
@@ -2,11 +2,19 @@ const fs = require("fs-extra");
2
2
  const path = require("path");
3
3
  const replaceInFile = require("replace-in-file");
4
4
  const chalk = require("chalk");
5
+ const { replace } = require("nunjucks/src/filters");
5
6
  const errorColor = chalk.red;
6
7
  const successColor = chalk.green;
7
8
  const warningColor = chalk.yellow;
8
9
 
9
- module.exports = async function (type, name, basedir) {
10
+ /**
11
+ *
12
+ * @param {string} type - Skeleton type
13
+ * @param {string} name - Name of the project
14
+ * @param {string} basedir - Base directory name to be created
15
+ * @param {Array.<{regex: RegExp, replaceWith: string}>} textReplacements
16
+ */
17
+ module.exports = async function (type, basedir, textReplacements) {
10
18
  if (fs.existsSync(basedir)) {
11
19
  console.error(
12
20
  errorColor(`directory ${basedir} already exists or is not writable`)
@@ -16,34 +24,46 @@ module.exports = async function (type, name, basedir) {
16
24
  fs.mkdirSync(basedir);
17
25
  }
18
26
 
19
- const replaceOptions = {
20
- files: `${basedir}/**`,
21
- from: new RegExp(`${type}-skeleton`, "g"),
22
- to: name,
23
- glob: {
24
- dot: true, // Include file names starting with a dot
25
- },
26
- };
27
-
28
27
  try {
29
28
  await fs.copySync(
30
29
  path.join(__dirname, `../../skeletons/${type}-skeleton`),
31
30
  basedir
32
31
  );
33
- await replaceInFile(replaceOptions);
32
+
33
+ if (textReplacements) {
34
+ for (const txtRe of textReplacements) {
35
+ await replaceText(txtRe.regex, txtRe.replaceWith, basedir);
36
+ }
37
+ }
38
+
34
39
  console.log(successColor(`Q ${type} is now bootstrapped in ${basedir}`));
35
40
 
36
- if (type === "tool")
41
+ if (type === "tool" || type === "et-utils-package")
37
42
  console.log(
38
- warningColor("Search for 'TODO:' inside the new tool to get started!")
43
+ warningColor(
44
+ "Search for 'TODO' inside the new tool/package to get started!"
45
+ )
39
46
  );
40
47
  } catch (error) {
41
48
  console.error(
42
49
  errorColor(
43
- `An unexpected error occured. Please check the entered information and try again. ${JSON.stringify(
50
+ `An unexpected error occurred. Please check the entered information and try again. ${JSON.stringify(
44
51
  error
45
52
  )}`
46
53
  )
47
54
  );
48
55
  }
49
56
  };
57
+
58
+ async function replaceText(regex, replaceWith, basedir) {
59
+ const replaceOptions = {
60
+ files: `${basedir}/**`, // Replace in all files
61
+ from: regex,
62
+ to: replaceWith,
63
+ glob: {
64
+ dot: true, // Include file names starting with a dot
65
+ },
66
+ };
67
+
68
+ return await replaceInFile(replaceOptions);
69
+ }
@@ -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
+ };