@dynamicweb/cli 1.1.2 → 2.0.0-beta.2

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.
@@ -1,9 +1,9 @@
1
1
  import fetch from 'node-fetch';
2
- import { setupEnv, getAgent } from './env.js';
2
+ import { setupEnv, getAgent, createCommandError } from './env.js';
3
3
  import { setupUser } from './login.js';
4
4
  import { input } from '@inquirer/prompts';
5
5
 
6
- const exclude = ['_', '$0', 'query', 'list', 'i', 'l', 'interactive', 'verbose', 'v', 'host', 'protocol', 'apiKey', 'env']
6
+ const exclude = ['_', '$0', 'query', 'list', 'i', 'l', 'interactive', 'verbose', 'v', 'host', 'protocol', 'apiKey', 'env', 'output', 'auth', 'clientId', 'clientSecret', 'clientIdEnv', 'clientSecretEnv', 'oauth']
7
7
 
8
8
  export function queryCommand() {
9
9
  return {
@@ -22,30 +22,47 @@ export function queryCommand() {
22
22
  alias: 'i',
23
23
  describe: 'Runs in interactive mode to ask for query parameters one by one'
24
24
  })
25
+ .option('output', {
26
+ choices: ['json'],
27
+ describe: 'Outputs a single JSON response for automation-friendly parsing',
28
+ conflicts: 'interactive'
29
+ })
25
30
  },
26
- handler: (argv) => {
27
- if (argv.verbose) console.info(`Running query ${argv.query}`)
28
- handleQuery(argv)
31
+ handler: async (argv) => {
32
+ const output = createQueryOutput(argv);
33
+
34
+ try {
35
+ output.verboseLog(`Running query ${argv.query}`);
36
+ await handleQuery(argv, output);
37
+ } catch (err) {
38
+ output.fail(err);
39
+ if (!output.json) {
40
+ console.error(err.stack || err.message || String(err));
41
+ }
42
+ process.exitCode = 1;
43
+ } finally {
44
+ output.finish();
45
+ }
29
46
  }
30
47
  }
31
48
  }
32
49
 
33
- async function handleQuery(argv) {
34
- let env = await setupEnv(argv);
50
+ async function handleQuery(argv, output) {
51
+ let env = await setupEnv(argv, output);
35
52
  let user = await setupUser(argv, env);
36
53
  if (argv.list) {
37
- console.log(await getProperties(argv))
54
+ const properties = await getProperties(env, user, argv.query);
55
+ output.addData(properties);
56
+ output.log(properties);
38
57
  } else {
39
- let response = await runQuery(env, user, argv.query, await getQueryParams(argv))
40
- console.log(response)
58
+ let response = await runQuery(env, user, argv.query, await getQueryParams(env, user, argv, output));
59
+ output.addData(response);
60
+ output.log(response);
41
61
  }
42
62
  }
43
63
 
44
- async function getProperties(argv) {
45
- let env = await setupEnv(argv);
46
- let user = await setupUser(argv, env);
47
-
48
- let res = await fetch(`${env.protocol}://${env.host}/Admin/Api/QueryByName?name=${argv.query}`, {
64
+ async function getProperties(env, user, query) {
65
+ let res = await fetch(`${env.protocol}://${env.host}/Admin/Api/QueryByName?name=${encodeURIComponent(query)}`, {
49
66
  method: 'GET',
50
67
  headers: {
51
68
  'Authorization': `Bearer ${user.apiKey}`
@@ -54,38 +71,60 @@ async function getProperties(argv) {
54
71
  })
55
72
  if (res.ok) {
56
73
  let body = await res.json()
57
- if (body.model.properties.groups === undefined) {
58
- console.log('Unable to fetch query parameters');
59
- process.exit(1);
74
+ if (body?.model?.properties?.groups === undefined) {
75
+ throw createCommandError('Unable to fetch query parameters.', res.status, body);
60
76
  }
61
- return body.model.properties.groups.filter(g => g.name === 'Properties')[0].fields.map(field => `${field.name} (${field.typeName})`)
77
+ return extractQueryPropertyPrompts(body);
62
78
  }
63
- console.log('Unable to fetch query parameters');
64
- console.log(res);
65
- process.exit(1);
79
+
80
+ throw createCommandError('Unable to fetch query parameters.', res.status, await parseJsonSafe(res));
66
81
  }
67
82
 
68
- async function getQueryParams(argv) {
83
+ export async function getQueryParams(env, user, argv, output, deps = {}) {
69
84
  let params = {}
85
+ const getPropertiesFn = deps.getPropertiesFn || getProperties;
86
+ const promptFn = deps.promptFn || input;
70
87
  if (argv.interactive) {
71
- let properties = await getProperties(argv);
72
- console.log('The following properties will be requested:')
73
- console.log(properties)
74
- for (const p of properties) {
75
- const value = await input({ message: p });
76
- if (value) {
77
- const fieldName = p.split(' (')[0];
78
- params[fieldName] = value;
79
- }
80
- }
88
+ let properties = await getPropertiesFn(env, user, argv.query);
89
+ output.log('The following properties will be requested:')
90
+ output.log(properties)
91
+ params = await buildInteractiveQueryParams(properties, promptFn);
81
92
  } else {
82
- Object.keys(argv).filter(k => !exclude.includes(k)).forEach(k => params[k] = argv[k])
93
+ params = buildQueryParamsFromArgv(argv);
94
+ }
95
+ return params
96
+ }
97
+
98
+ export function extractQueryPropertyPrompts(body) {
99
+ const fields = body?.model?.properties?.groups?.find(g => g.name === 'Properties')?.fields || [];
100
+ return fields.map(field => `${field.name} (${field.typeName})`);
101
+ }
102
+
103
+ export function getFieldNameFromPropertyPrompt(prompt) {
104
+ return prompt.replace(/\s+\([^)]+\)$/, '');
105
+ }
106
+
107
+ export async function buildInteractiveQueryParams(properties, promptFn = input) {
108
+ const params = {};
109
+
110
+ for (const propertyPrompt of properties) {
111
+ const value = await promptFn({ message: propertyPrompt });
112
+ if (value) {
113
+ params[getFieldNameFromPropertyPrompt(propertyPrompt)] = value;
114
+ }
83
115
  }
116
+
117
+ return params;
118
+ }
119
+
120
+ export function buildQueryParamsFromArgv(argv) {
121
+ let params = {}
122
+ Object.keys(argv).filter(k => !exclude.includes(k)).forEach(k => params[k] = argv[k])
84
123
  return params
85
124
  }
86
125
 
87
126
  async function runQuery(env, user, query, params) {
88
- let res = await fetch(`${env.protocol}://${env.host}/Admin/Api/${query}?` + new URLSearchParams(params), {
127
+ let res = await fetch(`${env.protocol}://${env.host}/Admin/Api/${encodeURIComponent(query)}?` + new URLSearchParams(params), {
89
128
  method: 'GET',
90
129
  headers: {
91
130
  'Authorization': `Bearer ${user.apiKey}`
@@ -93,8 +132,61 @@ async function runQuery(env, user, query, params) {
93
132
  agent: getAgent(env.protocol)
94
133
  })
95
134
  if (!res.ok) {
96
- console.log(`Error when doing request ${res.url}`)
97
- process.exit(1);
135
+ throw createCommandError(`Error when doing request ${res.url}`, res.status, await parseJsonSafe(res));
98
136
  }
99
137
  return await res.json()
100
- }
138
+ }
139
+
140
+ function createQueryOutput(argv) {
141
+ const response = {
142
+ ok: true,
143
+ command: 'query',
144
+ operation: argv.list ? 'list' : 'run',
145
+ status: 200,
146
+ data: [],
147
+ errors: [],
148
+ meta: {
149
+ query: argv.query
150
+ }
151
+ };
152
+
153
+ return {
154
+ json: argv.output === 'json',
155
+ response,
156
+ log(value) {
157
+ if (!this.json) {
158
+ console.log(value);
159
+ }
160
+ },
161
+ verboseLog(...args) {
162
+ if (argv.verbose && !this.json) {
163
+ console.info(...args);
164
+ }
165
+ },
166
+ addData(entry) {
167
+ response.data.push(entry);
168
+ },
169
+ fail(err) {
170
+ response.ok = false;
171
+ response.status = err?.status || 1;
172
+ response.errors.push({
173
+ message: err?.message || 'Unknown query command error.',
174
+ details: err?.details ?? null
175
+ });
176
+ },
177
+ finish() {
178
+ if (this.json) {
179
+ console.log(JSON.stringify(response, null, 2));
180
+ }
181
+ }
182
+ };
183
+ }
184
+
185
+
186
+ async function parseJsonSafe(res) {
187
+ try {
188
+ return await res.json();
189
+ } catch {
190
+ return null;
191
+ }
192
+ }
package/bin/index.js CHANGED
@@ -33,14 +33,30 @@ yargs(hideBin(process.argv))
33
33
  description: 'Run with verbose logging'
34
34
  })
35
35
  .option('protocol', {
36
- description: 'Allows setting the protocol used, only used together with --host, defaulting to https'
36
+ description: 'Set the protocol used with --host (defaults to https)'
37
37
  })
38
38
  .option('host', {
39
- description: 'Allows setting the host used, only allowed if an --apiKey is specified'
39
+ description: 'Allows setting the host used, only allowed if an --apiKey or OAuth client credentials are specified'
40
40
  })
41
41
  .option('apiKey', {
42
42
  description: 'Allows setting the apiKey for an environmentless execution of the CLI command'
43
43
  })
44
+ .option('auth', {
45
+ choices: ['user', 'oauth'],
46
+ description: 'Overrides the authentication mode for the command'
47
+ })
48
+ .option('clientId', {
49
+ description: 'OAuth client ID used together with --auth oauth'
50
+ })
51
+ .option('clientSecret', {
52
+ description: 'OAuth client secret used together with --auth oauth. WARNING: passing this on the command line can expose the secret via shell history and process listings. Prefer using --clientSecretEnv to reference a secret stored in an environment variable instead.'
53
+ })
54
+ .option('clientIdEnv', {
55
+ description: 'Environment variable name that contains the OAuth client ID'
56
+ })
57
+ .option('clientSecretEnv', {
58
+ description: 'Environment variable name that contains the OAuth client secret'
59
+ })
44
60
  .demandCommand()
45
61
  .parse()
46
62
 
@@ -49,14 +65,31 @@ function baseCommand() {
49
65
  command: '$0',
50
66
  describe: 'Shows the current env and user being used',
51
67
  handler: () => {
52
- if (Object.keys(getConfig()).length === 0) {
68
+ const cfg = getConfig();
69
+ if (Object.keys(cfg).length === 0) {
53
70
  console.log('To login to a solution use `dw login`')
54
71
  return;
55
- }
56
- console.log(`Environment: ${getConfig()?.current?.env}`)
57
- console.log(`User: ${getConfig()?.env[getConfig()?.current?.env]?.current?.user}`)
58
- console.log(`Protocol: ${getConfig()?.env[getConfig()?.current?.env]?.protocol}`)
59
- console.log(`Host: ${getConfig()?.env[getConfig()?.current?.env]?.host}`)
72
+ }
73
+ const currentEnv = cfg?.env?.[cfg?.current?.env];
74
+ if (!currentEnv) {
75
+ console.log(`Environment '${cfg?.current?.env}' is not configured.`);
76
+ console.log('To login to a solution use `dw login`');
77
+ return;
78
+ }
79
+ const authType = currentEnv?.current?.authType;
80
+
81
+ console.log(`Environment: ${cfg?.current?.env}`);
82
+ if (authType === 'oauth_client_credentials') {
83
+ console.log('Authentication: OAuth client credentials');
84
+ } else if (currentEnv?.current?.user) {
85
+ console.log(`User: ${currentEnv.current.user}`);
86
+ }
87
+ if (currentEnv.protocol) {
88
+ console.log(`Protocol: ${currentEnv.protocol}`);
89
+ }
90
+ if (currentEnv.host) {
91
+ console.log(`Host: ${currentEnv.host}`);
92
+ }
60
93
  }
61
94
  }
62
95
  }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@dynamicweb/cli",
3
3
  "type": "module",
4
4
  "description": "CLI for interacting with Dynamicweb 10 solutions from the command line.",
5
- "version": "1.1.2",
5
+ "version": "2.0.0-beta.2",
6
6
  "main": "bin/index.js",
7
7
  "files": [
8
8
  "bin/**"
@@ -15,7 +15,8 @@
15
15
  "devops"
16
16
  ],
17
17
  "scripts": {
18
- "test": "node --test"
18
+ "test": "node --test",
19
+ "qa:smoke": "node qa/run-smoke.mjs"
19
20
  },
20
21
  "author": "Dynamicweb A/S (https://www.dynamicweb.com)",
21
22
  "repository": {
@@ -1,76 +0,0 @@
1
- import test from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import fs from 'node:fs';
4
- import os from 'node:os';
5
- import path from 'node:path';
6
-
7
- import { resolveFilePath, resolveUploadOutput } from './files.js';
8
-
9
- test('resolveUploadOutput falls back to a console-compatible output object', () => {
10
- const output = resolveUploadOutput();
11
-
12
- assert.equal(typeof output.log, 'function');
13
- assert.equal(typeof output.addData, 'function');
14
- assert.equal(typeof output.mergeMeta, 'function');
15
- assert.deepEqual(output.response.meta, {});
16
-
17
- output.mergeMeta({ chunks: 1, filesProcessed: 2 });
18
-
19
- assert.deepEqual(output.response.meta, {
20
- chunks: 1,
21
- filesProcessed: 2
22
- });
23
- });
24
-
25
- test('resolveUploadOutput preserves custom logging and merges meta when mergeMeta is absent', () => {
26
- const calls = [];
27
- const data = [];
28
- const output = {
29
- log: (...args) => calls.push(args),
30
- addData: (entry) => data.push(entry),
31
- response: {
32
- meta: {
33
- existing: true
34
- }
35
- }
36
- };
37
-
38
- const resolved = resolveUploadOutput(output);
39
-
40
- resolved.log('Uploading chunk 1 of 1');
41
- resolved.addData({ file: 'addon.nupkg' });
42
- resolved.mergeMeta({ chunks: 1 });
43
-
44
- assert.deepEqual(calls, [[ 'Uploading chunk 1 of 1' ]]);
45
- assert.deepEqual(data, [{ file: 'addon.nupkg' }]);
46
- assert.deepEqual(resolved.response.meta, {
47
- existing: true,
48
- chunks: 1
49
- });
50
- });
51
-
52
- test('resolveUploadOutput initializes response.meta for partial output objects', () => {
53
- const resolved = resolveUploadOutput({
54
- log: () => {},
55
- response: {}
56
- });
57
-
58
- resolved.mergeMeta({ chunks: 2 });
59
-
60
- assert.deepEqual(resolved.response.meta, {
61
- chunks: 2
62
- });
63
- });
64
-
65
- test('resolveFilePath throws when no matching file exists', () => {
66
- const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'dw-cli-files-test-'));
67
-
68
- try {
69
- assert.throws(
70
- () => resolveFilePath(path.join(tempDir, 'missing*.nupkg')),
71
- /Could not find any files with the name/
72
- );
73
- } finally {
74
- fs.rmSync(tempDir, { recursive: true, force: true });
75
- }
76
- });
@@ -1,48 +0,0 @@
1
- import test from 'node:test';
2
- import assert from 'node:assert/strict';
3
-
4
- import { createInstallOutput } from './install.js';
5
-
6
- test('createInstallOutput suppresses regular logs in json mode and emits the final envelope', () => {
7
- const logCalls = [];
8
- const infoCalls = [];
9
- const originalLog = console.log;
10
- const originalInfo = console.info;
11
-
12
- console.log = (...args) => logCalls.push(args);
13
- console.info = (...args) => infoCalls.push(args);
14
-
15
- try {
16
- const output = createInstallOutput({
17
- output: 'json',
18
- queue: true,
19
- verbose: true
20
- });
21
-
22
- output.log('hidden');
23
- output.verboseLog('hidden verbose');
24
- output.addData({ type: 'install', filename: 'addon.nupkg' });
25
- output.mergeMeta({ resolvedPath: '/tmp/addon.nupkg' });
26
- output.finish();
27
-
28
- assert.deepEqual(infoCalls, []);
29
- assert.equal(logCalls.length, 1);
30
-
31
- const rendered = JSON.parse(logCalls[0][0]);
32
- assert.deepEqual(rendered, {
33
- ok: true,
34
- command: 'install',
35
- operation: 'install',
36
- status: 200,
37
- data: [{ type: 'install', filename: 'addon.nupkg' }],
38
- errors: [],
39
- meta: {
40
- queued: true,
41
- resolvedPath: '/tmp/addon.nupkg'
42
- }
43
- });
44
- } finally {
45
- console.log = originalLog;
46
- console.info = originalInfo;
47
- }
48
- });