@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.
@@ -0,0 +1,302 @@
1
+ const resourcesService = require("./resourcesService.js");
2
+ const schemaService = require("./schemaService.js");
3
+ const deepmerge = require("deepmerge");
4
+ const fetch = require("node-fetch");
5
+ const chalk = require("chalk");
6
+ const errorColor = chalk.red;
7
+
8
+ async function createItem(item, environment, config) {
9
+ const qServer = config.get(`${environment.name}.qServer`);
10
+ const accessToken = config.get(`${environment.name}.accessToken`);
11
+ const cookie = config.get(`${environment.name}.cookie`);
12
+
13
+ try {
14
+ const response = await fetch(`${qServer}item`, {
15
+ method: "POST",
16
+ body: JSON.stringify(item),
17
+ headers: {
18
+ "user-agent": "Q Command-line Tool",
19
+ Authorization: `Bearer ${accessToken}`,
20
+ "Content-Type": "application/json",
21
+ Cookie: cookie ? cookie : "",
22
+ },
23
+ });
24
+ if (response.ok) {
25
+ return await response.json();
26
+ } else {
27
+ throw new Error(
28
+ `A problem occured while creating item on ${environment.name} environment. Please check your connection and try again.`
29
+ );
30
+ }
31
+ } catch (error) {
32
+ console.error(errorColor(error.message));
33
+ process.exit(1);
34
+ }
35
+ }
36
+
37
+ async function getItem(qServer, environment, accessToken, cookie) {
38
+ try {
39
+ const response = await fetch(`${qServer}item/${environment.id}`, {
40
+ headers: {
41
+ "user-agent": "Q Command-line Tool",
42
+ Authorization: `Bearer ${accessToken}`,
43
+ Cookie: cookie ? cookie : "",
44
+ },
45
+ });
46
+ if (response.ok) {
47
+ return await response.json();
48
+ } else {
49
+ throw new Error(
50
+ `A problem occured while getting item with id ${environment.id} on ${environment.name} environment. Please make sure that the id is correct, you have an internet connection and try again.`
51
+ );
52
+ }
53
+ } catch (error) {
54
+ console.error(errorColor(error.message));
55
+ process.exit(1);
56
+ }
57
+ }
58
+
59
+ function getItems(qConfig, environmentFilter) {
60
+ const items = qConfig.items
61
+ .filter((item) => {
62
+ if (environmentFilter) {
63
+ return item.environments.some(
64
+ (environment) => environment.name === environmentFilter
65
+ );
66
+ }
67
+
68
+ return true;
69
+ })
70
+ .map((item) => {
71
+ if (environmentFilter) {
72
+ item.environments = item.environments.filter(
73
+ (environment) => environment.name === environmentFilter
74
+ );
75
+ }
76
+
77
+ return item;
78
+ });
79
+
80
+ return items;
81
+ }
82
+
83
+ function getDefaultOrNull(schema) {
84
+ if (schema.hasOwnProperty("default")) {
85
+ if (typeof schema.default === "object") {
86
+ return JSON.parse(JSON.stringify(schema.default));
87
+ }
88
+ return schema.default;
89
+ }
90
+ return null;
91
+ }
92
+
93
+ // Returns a default item based on the tool schema
94
+ // The default item is used to derive the file properties of a certain file type
95
+ // These file properties are specified by the tool and are specific to the file type
96
+ // For example an image file has height/width file properties
97
+ function getDefaultItem(schema) {
98
+ schema = JSON.parse(JSON.stringify(schema));
99
+ if (schema.type === "array") {
100
+ let array = [];
101
+ schema.minItems = 1;
102
+ for (let i = 0; i < schema.minItems; i++) {
103
+ let value = getDefaultItem(schema.items);
104
+ if (value) {
105
+ if (
106
+ schema["Q:type"] &&
107
+ schema["Q:type"] === "files" &&
108
+ schema["Q:options"] &&
109
+ schema["Q:options"].fileProperties
110
+ ) {
111
+ array.push(Object.assign(value, schema["Q:options"].fileProperties));
112
+ } else {
113
+ array.push(value);
114
+ }
115
+ }
116
+ }
117
+
118
+ const defaultValue = getDefaultOrNull(schema);
119
+ if (array === null && defaultValue !== null) {
120
+ array = defaultValue;
121
+ }
122
+ return array;
123
+ } else if (schema.type === "object") {
124
+ const defaultValue = getDefaultOrNull(schema);
125
+ if (defaultValue !== null) {
126
+ return defaultValue;
127
+ }
128
+
129
+ if (
130
+ schema["Q:type"] &&
131
+ schema["Q:type"] === "files" &&
132
+ schema["Q:options"] &&
133
+ schema["Q:options"].fileProperties
134
+ ) {
135
+ return schema["Q:options"].fileProperties;
136
+ }
137
+
138
+ if (!schema.hasOwnProperty("properties")) {
139
+ return undefined;
140
+ }
141
+ let object = {};
142
+ Object.keys(schema.properties).forEach((propertyName) => {
143
+ const property = schema.properties[propertyName];
144
+ let value = getDefaultItem(property);
145
+ if (value !== undefined) {
146
+ object[propertyName] = value;
147
+ } else if (
148
+ property["Q:type"] &&
149
+ property["Q:type"] === "files" &&
150
+ property["Q:options"] &&
151
+ property["Q:options"].fileProperties
152
+ ) {
153
+ object[propertyName] = property["Q:options"].fileProperties;
154
+ }
155
+ });
156
+ return object;
157
+ }
158
+
159
+ // if this is not an array or object, we just get the default if any
160
+ const defaultValue = getDefaultOrNull(schema);
161
+ if (defaultValue !== null) {
162
+ return defaultValue;
163
+ }
164
+ return undefined;
165
+ }
166
+
167
+ async function updateItem(item, environment, config, qConfigPath) {
168
+ const qServer = config.get(`${environment.name}.qServer`);
169
+ const accessToken = config.get(`${environment.name}.accessToken`);
170
+ const cookie = config.get(`${environment.name}.cookie`);
171
+ const existingItem = await getItem(qServer, environment, accessToken, cookie);
172
+ const updatedItem = await getUpdatedItem(
173
+ qServer,
174
+ accessToken,
175
+ cookie,
176
+ existingItem,
177
+ item,
178
+ environment,
179
+ qConfigPath
180
+ );
181
+ return await saveItem(qServer, environment, accessToken, cookie, updatedItem);
182
+ }
183
+
184
+ async function getUpdatedItem(
185
+ qServer,
186
+ accessToken,
187
+ cookie,
188
+ existingItem,
189
+ item,
190
+ environment,
191
+ qConfigPath
192
+ ) {
193
+ try {
194
+ const toolSchema = await schemaService.getToolSchema(
195
+ qServer,
196
+ existingItem.tool
197
+ );
198
+ // Removes additional properties not defined in the schema on the top level object of the item
199
+ toolSchema.additionalProperties = false;
200
+ // If options object is available additional properties not defined in the schema are removed
201
+ if (toolSchema.properties && toolSchema.properties.options) {
202
+ toolSchema.properties.options.additionalProperties = false;
203
+ }
204
+ const defaultItem = getDefaultItem(toolSchema);
205
+ item = JSON.parse(JSON.stringify(item));
206
+ item = await resourcesService.handleResources(
207
+ qServer,
208
+ accessToken,
209
+ cookie,
210
+ item,
211
+ defaultItem,
212
+ qConfigPath,
213
+ environment
214
+ );
215
+
216
+ // Merge options:
217
+ // File of files property will be updated (if file exists on destination)
218
+ // If it doesn't exist it is appended to the files array
219
+ // All other properties are overwritten from source config
220
+ const options = {
221
+ arrayMerge: (destArr, srcArr) => srcArr,
222
+ customMerge: (key) => {
223
+ if (key === "files") {
224
+ return (destArr, srcArr) => {
225
+ if (destArr.length <= 0) {
226
+ return srcArr;
227
+ }
228
+
229
+ srcArr.forEach((fileObj) => {
230
+ let destIndex = destArr.findIndex(
231
+ (destFileObj) =>
232
+ destFileObj.file.originalName === fileObj.file.originalName
233
+ );
234
+
235
+ if (destIndex !== -1) {
236
+ destArr[destIndex] = fileObj;
237
+ } else {
238
+ destArr.push(fileObj);
239
+ }
240
+ });
241
+ return destArr;
242
+ };
243
+ }
244
+ },
245
+ };
246
+
247
+ // merges existing item with the item defined in q.config.json
248
+ const updatedItem = deepmerge(existingItem, item, options);
249
+ // normalizes the item which removes additional properties not defined in the schema
250
+ // and validates the item against the schema
251
+ const normalizedItem = schemaService.getNormalizedItem(
252
+ toolSchema,
253
+ updatedItem,
254
+ environment
255
+ );
256
+ // the normalized item is merged with the existing item. This is done because properties such as _id and _rev
257
+ // defined in the existing item are removed during normalization, because they are not defined in the schema
258
+ return deepmerge(existingItem, normalizedItem, options);
259
+ } catch (error) {
260
+ console.error(errorColor(error.message));
261
+ process.exit(1);
262
+ }
263
+ }
264
+
265
+ async function saveItem(
266
+ qServer,
267
+ environment,
268
+ accessToken,
269
+ cookie,
270
+ updatedItem
271
+ ) {
272
+ try {
273
+ delete updatedItem.updatedDate;
274
+ const response = await fetch(`${qServer}item`, {
275
+ method: "PUT",
276
+ body: JSON.stringify(updatedItem),
277
+ headers: {
278
+ "user-agent": "Q Command-line Tool",
279
+ Authorization: `Bearer ${accessToken}`,
280
+ "Content-Type": "application/json",
281
+ Cookie: cookie ? cookie : "",
282
+ },
283
+ });
284
+ if (response.ok) {
285
+ return await response.json();
286
+ } else {
287
+ throw new Error(
288
+ `A problem occured while saving item with id ${environment.id} on ${environment.name} environment. Please check your connection and try again.`
289
+ );
290
+ }
291
+ } catch (error) {
292
+ console.error(errorColor(error.message));
293
+ process.exit(1);
294
+ }
295
+ }
296
+
297
+ module.exports = {
298
+ createItem: createItem,
299
+ getItem: getItem,
300
+ getItems: getItems,
301
+ updateItem: updateItem,
302
+ };
@@ -20,106 +20,6 @@ const path = require("path");
20
20
  const chalk = require("chalk");
21
21
  const errorColor = chalk.red;
22
22
 
23
- async function getToolSchema(qServer, tool) {
24
- try {
25
- const response = await fetch(`${qServer}tools/${tool}/schema.json`);
26
- if (response.ok) {
27
- return await response.json();
28
- } else {
29
- throw new Error(
30
- `A problem occured while getting the schema of the ${tool} tool. Please check your connection and try again.`
31
- );
32
- }
33
- } catch (error) {
34
- console.error(errorColor(error.message));
35
- process.exit(1);
36
- }
37
- }
38
-
39
- function getDefaultOrNull(schema) {
40
- if (schema.hasOwnProperty("default")) {
41
- if (typeof schema.default === "object") {
42
- return JSON.parse(JSON.stringify(schema.default));
43
- }
44
- return schema.default;
45
- }
46
- return null;
47
- }
48
-
49
- // Returns a default item based on the tool schema
50
- // The default item is used to derive the file properties of a certain file type
51
- // These file properties are specified by the tool and are specific to the file type
52
- // For example an image file has height/width file properties
53
- function getDefaultItem(schema) {
54
- schema = JSON.parse(JSON.stringify(schema));
55
- if (schema.type === "array") {
56
- let array = [];
57
- schema.minItems = 1;
58
- for (let i = 0; i < schema.minItems; i++) {
59
- let value = getDefaultItem(schema.items);
60
- if (value) {
61
- if (
62
- schema["Q:type"] &&
63
- schema["Q:type"] === "files" &&
64
- schema["Q:options"] &&
65
- schema["Q:options"].fileProperties
66
- ) {
67
- array.push(Object.assign(value, schema["Q:options"].fileProperties));
68
- } else {
69
- array.push(value);
70
- }
71
- }
72
- }
73
-
74
- const defaultValue = getDefaultOrNull(schema);
75
- if (array === null && defaultValue !== null) {
76
- array = defaultValue;
77
- }
78
- return array;
79
- } else if (schema.type === "object") {
80
- const defaultValue = getDefaultOrNull(schema);
81
- if (defaultValue !== null) {
82
- return defaultValue;
83
- }
84
-
85
- if (
86
- schema["Q:type"] &&
87
- schema["Q:type"] === "files" &&
88
- schema["Q:options"] &&
89
- schema["Q:options"].fileProperties
90
- ) {
91
- return schema["Q:options"].fileProperties;
92
- }
93
-
94
- if (!schema.hasOwnProperty("properties")) {
95
- return undefined;
96
- }
97
- let object = {};
98
- Object.keys(schema.properties).forEach((propertyName) => {
99
- const property = schema.properties[propertyName];
100
- let value = getDefaultItem(property);
101
- if (value !== undefined) {
102
- object[propertyName] = value;
103
- } else if (
104
- property["Q:type"] &&
105
- property["Q:type"] === "files" &&
106
- property["Q:options"] &&
107
- property["Q:options"].fileProperties
108
- ) {
109
- object[propertyName] = property["Q:options"].fileProperties;
110
- }
111
- });
112
- return object;
113
- }
114
-
115
- // if this is not an array or object, we just get the default if any
116
- const defaultValue = getDefaultOrNull(schema);
117
- if (defaultValue !== null) {
118
- return defaultValue;
119
- }
120
- return undefined;
121
- }
122
-
123
23
  // Recursively traverses the item object
124
24
  // If a property called "path" is found, the resource is uploaded
125
25
  // and the metadata of that resource is inserted at that place in the item object
@@ -245,6 +145,4 @@ async function uploadResource(qServer, accessToken, cookie, resourcePath) {
245
145
 
246
146
  module.exports = {
247
147
  handleResources: handleResources,
248
- getDefaultItem: getDefaultItem,
249
- getToolSchema: getToolSchema,
250
148
  };
@@ -0,0 +1,64 @@
1
+ const Ajv = require("ajv");
2
+ // Remove additional properties which are not defined by the json schema
3
+ // See https://ajv.js.org/options.html#removeadditional for details
4
+ const ajv = new Ajv({ schemaId: "auto", removeAdditional: true });
5
+ ajv.addMetaSchema(require("ajv/lib/refs/json-schema-draft-04.json"));
6
+ const fetch = require("node-fetch");
7
+ const chalk = require("chalk");
8
+ const errorColor = chalk.red;
9
+
10
+ async function getToolSchema(qServer, tool) {
11
+ try {
12
+ const response = await fetch(`${qServer}tools/${tool}/schema.json`);
13
+ if (response.ok) {
14
+ return await response.json();
15
+ } else {
16
+ throw new Error(
17
+ `A problem occured while getting the schema of the ${tool} tool. Please check your connection and try again.`
18
+ );
19
+ }
20
+ } catch (error) {
21
+ console.error(errorColor(error.message));
22
+ process.exit(1);
23
+ }
24
+ }
25
+
26
+ function getSchemaPathFor(commandName) {
27
+ const pathFor = {
28
+ copyItem: "./copyItem/copySchema.json",
29
+ updateItem: "./updateItem/updateSchema.json",
30
+ };
31
+
32
+ if (pathFor[commandName]) {
33
+ return pathFor[commandName];
34
+ } else {
35
+ throw new Error(`Unhandled schema path for commandName: '${commandName}'`);
36
+ }
37
+ }
38
+
39
+ function validateConfig(config, commandName = "updateItem") {
40
+ const isValid = ajv.validate(require(getSchemaPathFor(commandName)), config);
41
+ return {
42
+ isValid: isValid,
43
+ errorsText: ajv.errorsText(),
44
+ };
45
+ }
46
+
47
+ function getNormalizedItem(schema, item, environment) {
48
+ const isValid = ajv.validate(schema, item);
49
+ if (!isValid) {
50
+ throw new Error(
51
+ `A problem occured while validating item with id ${environment.id} on ${
52
+ environment.name
53
+ } environment: ${ajv.errorsText()}`
54
+ );
55
+ }
56
+
57
+ return item;
58
+ }
59
+
60
+ module.exports = {
61
+ getToolSchema: getToolSchema,
62
+ validateConfig: validateConfig,
63
+ getNormalizedItem: getNormalizedItem,
64
+ };
@@ -1,4 +1,6 @@
1
- const helpers = require("./helpers.js");
1
+ const schemaService = require("./../schemaService.js");
2
+ const configStore = require("./../configStore.js");
3
+ const itemService = require("./../itemService.js");
2
4
  const fs = require("fs");
3
5
  const path = require("path");
4
6
  const chalk = require("chalk");
@@ -10,21 +12,24 @@ module.exports = async function (command) {
10
12
  const qConfigPath = path.resolve(command.config);
11
13
  if (fs.existsSync(qConfigPath)) {
12
14
  const qConfig = JSON.parse(fs.readFileSync(qConfigPath));
13
- const validationResult = helpers.validateConfig(qConfig);
15
+ const validationResult = schemaService.validateConfig(qConfig);
16
+
14
17
  if (validationResult.isValid) {
15
- const config = await helpers.setupConfig(
18
+ const config = await configStore.setupStore(
16
19
  qConfig,
17
20
  command.environment,
18
21
  command.reset
19
22
  );
20
- for (const item of helpers.getItems(qConfig, command.environment)) {
23
+
24
+ for (const item of itemService.getItems(qConfig, command.environment)) {
21
25
  for (const environment of item.environments) {
22
- const result = await helpers.updateItem(
26
+ const result = await itemService.updateItem(
23
27
  item.item,
24
28
  environment,
25
29
  config,
26
30
  qConfigPath
27
31
  );
32
+
28
33
  if (result) {
29
34
  console.log(
30
35
  successColor(
package/bin/q.js CHANGED
@@ -6,7 +6,8 @@ const errorColor = chalk.red;
6
6
  const version = require("../package.json").version;
7
7
  const runServer = require("./commands/server.js");
8
8
  const bootstrap = require("./commands/bootstrap.js");
9
- const updateItem = require("./commands/updateItem/updateItem.js");
9
+ const updateItem = require("./commands/qItem/updateItem/updateItem.js");
10
+ const copyItem = require("./commands/qItem/copyItem/copyItem.js");
10
11
 
11
12
  async function main() {
12
13
  program.version(version).description("Q Toolbox cli");
@@ -45,7 +46,11 @@ async function main() {
45
46
  process.exit(1);
46
47
  }
47
48
  const baseDir = program.dir || name;
48
- await bootstrap("server", name, baseDir);
49
+ const textReplacements = [
50
+ { regex: new RegExp(`${type}-skeleton`, "g"), replaceWith: name },
51
+ ];
52
+
53
+ await bootstrap("server", baseDir, textReplacements);
49
54
  });
50
55
 
51
56
  program
@@ -62,7 +67,11 @@ async function main() {
62
67
  process.exit(1);
63
68
  }
64
69
  const baseDir = program.dir || name;
65
- await bootstrap("tool", name, baseDir);
70
+ const textReplacements = [
71
+ { regex: new RegExp(`${type}-skeleton`, "g"), replaceWith: name },
72
+ ];
73
+
74
+ await bootstrap("tool", baseDir, textReplacements);
66
75
  });
67
76
 
68
77
  program
@@ -79,7 +88,42 @@ async function main() {
79
88
  process.exit(1);
80
89
  }
81
90
  const baseDir = program.dir || name;
82
- await bootstrap("custom-code", name, baseDir);
91
+ const textReplacements = [
92
+ { regex: new RegExp(`${type}-skeleton`, "g"), replaceWith: name },
93
+ ];
94
+
95
+ await bootstrap("custom-code", baseDir, textReplacements);
96
+ });
97
+
98
+ program
99
+ .command("new-et-utils-package")
100
+ .option(
101
+ "-d, --dir <path>",
102
+ "the base directory to bootstrap the new tool in, defaults to the tools name"
103
+ )
104
+ .description("bootstrap a new ed-tech utility package")
105
+ .action(async () => {
106
+ const name = program.args[1];
107
+ const author = program.args[2] || "TODO: Set package author name";
108
+ const description =
109
+ program.args[3] || "TODO: Write a package description";
110
+
111
+ if (!name) {
112
+ console.error(errorColor("no package name given"));
113
+ process.exit(1);
114
+ }
115
+
116
+ const baseDir = program.dir || name;
117
+ const textReplacements = [
118
+ { regex: new RegExp("<package-name>", "g"), replaceWith: name },
119
+ { regex: new RegExp("<author-name>", "g"), replaceWith: author },
120
+ {
121
+ regex: new RegExp("<package-description>", "g"),
122
+ replaceWith: description,
123
+ },
124
+ ];
125
+
126
+ await bootstrap("et-utils-package", baseDir, textReplacements);
83
127
  });
84
128
 
85
129
  program
@@ -99,6 +143,23 @@ async function main() {
99
143
  await updateItem(command);
100
144
  });
101
145
 
146
+ program
147
+ .command("copy-item")
148
+ .description("copies an existing q item")
149
+ .option(
150
+ "-c, --config [path]",
151
+ "set config path which defines the q items to be copied. defaults to ./q.config.json",
152
+ `${process.cwd()}/q.config.json`
153
+ )
154
+ .option(
155
+ "-e, --environment [env]",
156
+ "set environment where the existing q item is found, defaults to copy all items of all environments defined in config"
157
+ )
158
+ .option("-r, --reset", "reset stored configuration properties")
159
+ .action(async (command) => {
160
+ await copyItem(command);
161
+ });
162
+
102
163
  await program.parseAsync(process.argv);
103
164
  }
104
165
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nzz/q-cli",
3
- "version": "1.6.0",
3
+ "version": "1.8.0",
4
4
  "description": "Cli tool to setup new Q tools, new Q server implementations and start Q dev server to test developing Q tools",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -2,7 +2,7 @@
2
2
  "name": "custom-code-skeleton",
3
3
  "version": "1.0.0",
4
4
  "devDependencies": {
5
- "@nzz/nzz.ch-static": "github:nzzdev/nzz.ch-static#v1.0.0",
5
+ "@nzz/nzz.ch-static": "github:nzzdev/ed-tech-nzz.ch-static#v1.0.0",
6
6
  "@nzz/q-cli": "^1.4.11",
7
7
  "@rollup/plugin-alias": "^3.1.9",
8
8
  "@rollup/plugin-commonjs": "^21.0.0",
@@ -1,15 +1,15 @@
1
1
  {
2
- "extends": "@tsconfig/svelte/tsconfig.json",
3
- "include": ["src/**/*"],
4
- "exclude": ["node_modules/*", "__sapper__/*", "public/*"],
5
- "compilerOptions": {
6
- "lib": ["es2015", "DOM"],
7
- "types": ["svelte"],
8
- "outDir": "public",
9
- "declaration": false,
10
- "paths": {
11
- "@src": ["./src/"],
12
- "@interfaces": ["./src/interfaces.ts"]
13
- }
2
+ "extends": "@tsconfig/svelte/tsconfig.json",
3
+ "include": ["src/**/*"],
4
+ "exclude": ["node_modules/*", "__sapper__/*", "public/*"],
5
+ "compilerOptions": {
6
+ "lib": ["es2015", "DOM"],
7
+ "types": ["svelte"],
8
+ "outDir": "public",
9
+ "declaration": false,
10
+ "paths": {
11
+ "@src": ["./src/"],
12
+ "@interfaces": ["./src/interfaces.ts"]
14
13
  }
14
+ }
15
15
  }
@@ -0,0 +1,7 @@
1
+ # @nzz/et-utils-<package-name>
2
+
3
+ <package-description>
4
+
5
+ ## Content
6
+
7
+ TODO: Package documentation