@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.
- package/README.md +413 -171
- package/bin/commands/command.js +102 -41
- package/bin/commands/config.js +14 -2
- package/bin/commands/env.js +181 -38
- package/bin/commands/files.js +393 -110
- package/bin/commands/install.js +55 -47
- package/bin/commands/login.js +384 -45
- package/bin/commands/query.js +130 -38
- package/bin/index.js +41 -8
- package/package.json +3 -2
- package/bin/commands/files.test.js +0 -76
- package/bin/commands/install.test.js +0 -48
package/bin/commands/query.js
CHANGED
|
@@ -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
|
-
|
|
28
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
45
|
-
let
|
|
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
|
|
58
|
-
|
|
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
|
|
77
|
+
return extractQueryPropertyPrompts(body);
|
|
62
78
|
}
|
|
63
|
-
|
|
64
|
-
|
|
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
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: '
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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": "
|
|
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
|
-
});
|