Skip to content

Commit f31d18b

Browse files
committed
Implement CLI #17
Added CLI TODO: unit tests resolves #17
1 parent 3d8efd6 commit f31d18b

File tree

10 files changed

+453
-459
lines changed

10 files changed

+453
-459
lines changed

README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,33 @@ export interface FunctionExp {
225225
}
226226
```
227227

228+
## CLI Usage
229+
The CLI can be used to parse a query or compose a previously parsed query back to SOQL.
230+
231+
**Examples:**
232+
```shell
233+
$ npm install -g soql-parser-js
234+
$ soql --help
235+
$ soql --query "SELECT Id FROM Account"
236+
$ soql -query "SELECT Id FROM Account"
237+
$ soql -query "SELECT Id FROM Account" -output some-output-file.json
238+
$ soql -query "SELECT Id FROM Account" -json
239+
$ soql -query some-input-file.txt
240+
$ soql -compose some-input-file.json
241+
$ soql -compose some-input-file.json
242+
$ soql -compose some-input-file.json -output some-output-file.json
243+
```
244+
245+
**Arguments:**
246+
```
247+
--query, -q A SOQL query surrounded in quotes or a file path to a text file containing a SOQL query.
248+
--compose, -c An escaped and quoted parsed SOQL JSON string or a file path to a text file containing a parsed query JSON object.
249+
--output, -o Filepath.
250+
--json, -j Provide all output messages as JSON.
251+
--debug, -d Print additional debug log messages.
252+
--help, -h Show this help message.
253+
```
254+
228255
## Contributing
229256
All contributions are welcome on the project. Please read the [contribution guidelines](https://github.com/paustint/soql-parser-js/blob/master/CONTRIBUTING.md).
230257

debug/cli.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
var argv = require('minimist')(process.argv.slice(2));
2+
3+
console.log('argv:');
4+
console.log(JSON.stringify(argv, null, 2));

lib/cli.ts

Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
import * as soqlParser from '.';
2+
import { isString, pad } from './utils';
3+
import { existsSync, readFileSync, writeFileSync } from 'fs';
4+
import { isObject } from 'util';
5+
6+
const argv = require('minimist')(process.argv.slice(2));
7+
8+
interface Options {
9+
query: string | undefined;
10+
compose: string | undefined;
11+
output: string | undefined;
12+
}
13+
14+
interface Print {
15+
error?: boolean;
16+
message?: string;
17+
data?: string;
18+
debug?: boolean;
19+
overrideColor?: string;
20+
}
21+
22+
const debug: boolean | undefined = argv.debug || argv.d;
23+
const printJson: boolean | undefined = argv.json || argv.j;
24+
25+
log({ data: JSON.stringify(argv, null, 2) });
26+
27+
function log(options: Print) {
28+
if (debug) {
29+
print({ ...options, debug: true });
30+
}
31+
}
32+
33+
function print(options: Print) {
34+
let color = options.error ? '31' : options.overrideColor;
35+
if (printJson && !options.debug) {
36+
if (isString(options.data)) {
37+
try {
38+
options.data = JSON.parse(options.data);
39+
} catch (ex) {}
40+
}
41+
console.log(JSON.stringify(options), '\n');
42+
} else {
43+
if (options.debug && options.message) {
44+
color = color || '33';
45+
options.message = `[DEBUG] ${options.message}`;
46+
console.log(`\x1b[${color}m%s\x1b[0m`, options.message);
47+
} else if (options.message) {
48+
color = color || '32';
49+
console.log(`\x1b[${color}m%s\x1b[0m`, options.message);
50+
}
51+
52+
// reset color to default
53+
color = options.error ? '31' : options.overrideColor;
54+
55+
if (options.data) {
56+
if (isObject(options.data)) {
57+
options.data = JSON.stringify(options.data, null, 2);
58+
}
59+
}
60+
61+
if (options.debug && options.data) {
62+
color = color || '33';
63+
options.data = `[DEBUG]\n${options.data}`;
64+
console.log(`\x1b[${color}m%s\x1b[0m`, options.data);
65+
} else if (options.data) {
66+
color = color || '1';
67+
console.log(`\x1b[${color}m%s\x1b[0m`, options.data);
68+
}
69+
}
70+
}
71+
72+
function run() {
73+
const options: Options = {
74+
query: argv.query || argv.q,
75+
compose: argv.compose || argv.c,
76+
output: argv.output || argv.o,
77+
};
78+
79+
log({ message: 'Options', data: JSON.stringify(options, null, 2) });
80+
81+
const help: boolean | undefined = argv.help || argv.h;
82+
let validParams = false;
83+
84+
if (isString(options.query)) {
85+
log({ message: 'Parsing Query' });
86+
validParams = true;
87+
parseQuery(options);
88+
}
89+
90+
if (isString(options.compose)) {
91+
log({ message: 'Composing Query' });
92+
validParams = true;
93+
composeQuery(options);
94+
}
95+
96+
if (isString(help)) {
97+
log({ message: 'Showing explicit Help' });
98+
validParams = true;
99+
printHelp();
100+
}
101+
102+
if (!validParams) {
103+
log({ message: 'Showing implicit Help' });
104+
printHelp();
105+
}
106+
107+
process.exit(0);
108+
}
109+
110+
function parseQuery(options: Options) {
111+
// if query starts with SELECT we know it is not a file, otherwise we will attempt to parse a file
112+
// Check if query does not look like a query - attempt to parse file if so
113+
let query = options.query;
114+
log({ message: query });
115+
if (
116+
!options.query
117+
.trim()
118+
.toUpperCase()
119+
.startsWith('SELECT')
120+
) {
121+
log({ message: 'Query does not start with select, attempting to read file' });
122+
try {
123+
if (existsSync(options.query)) {
124+
query = readFileSync(options.query, 'utf8');
125+
log({ message: 'Query read from file:', data: query });
126+
if (
127+
!query
128+
.trim()
129+
.toUpperCase()
130+
.startsWith('SELECT')
131+
) {
132+
print({
133+
error: true,
134+
message: `The query contained within the file ${
135+
options.query
136+
} does not appear to be valid, please make sure the query starts with SELECT.`,
137+
});
138+
process.exit(1);
139+
}
140+
} else {
141+
print({
142+
error: true,
143+
message: 'The query must start with SELECT or must be a valid file path to a text file containing the query.',
144+
});
145+
process.exit(1);
146+
}
147+
} catch (ex) {
148+
print({
149+
error: true,
150+
message: `There was an error parsing the file ${
151+
options.query
152+
}. Please ensure the file exists and is a text file containing a single SOQL query.`,
153+
});
154+
log({ error: true, data: ex });
155+
process.exit(1);
156+
}
157+
}
158+
159+
try {
160+
const parsedQuery = soqlParser.parseQuery(query);
161+
const queryJson = JSON.stringify(parsedQuery, null, 2);
162+
log({ data: queryJson });
163+
if (options.output) {
164+
saveOutput({ path: options.output, data: queryJson });
165+
} else {
166+
print({
167+
message: `Parsed Query:`,
168+
data: queryJson,
169+
});
170+
}
171+
} catch (ex) {
172+
print({
173+
error: true,
174+
message: `There was an error parsing your query`,
175+
data: ex.message,
176+
});
177+
log({ error: true, data: ex });
178+
process.exit(1);
179+
}
180+
}
181+
182+
function composeQuery(options: Options) {
183+
// if query starts with SELECT we know it is not a file, otherwise we will attempt to parse a file
184+
// Check if query does not look like a query - attempt to parse file if so
185+
let parsedQueryString = options.compose;
186+
if (!options.compose.trim().startsWith('{')) {
187+
log({ message: 'Compose is a filepath - attempting to read file' });
188+
try {
189+
if (existsSync(options.compose)) {
190+
parsedQueryString = readFileSync(options.compose, 'utf8');
191+
log({
192+
message: 'Parsed query data JSON read from file',
193+
data: parsedQueryString,
194+
});
195+
} else {
196+
print({
197+
error: true,
198+
message: `The file ${
199+
options.compose
200+
} does not exist, Please provide a valid filepath or an escaped JSON string.`,
201+
});
202+
process.exit(1);
203+
}
204+
} catch (ex) {
205+
print({
206+
error: true,
207+
message: `There was an error reading the file ${
208+
options.compose
209+
}. Please ensure the file exists and is a text file containing a single parsed query JSON object.`,
210+
});
211+
log({ error: true, data: ex });
212+
process.exit(1);
213+
}
214+
}
215+
216+
try {
217+
const parsedQuery = JSON.parse(parsedQueryString);
218+
const query = soqlParser.composeQuery(parsedQuery);
219+
if (options.output) {
220+
log({ message: 'Attempting to save query to file' });
221+
saveOutput({ path: options.output, data: query });
222+
} else {
223+
print({
224+
message: `Composed Query:`,
225+
data: query,
226+
});
227+
}
228+
} catch (ex) {
229+
print({
230+
error: true,
231+
message: `There was an error composing your query.`,
232+
data: ex.message,
233+
});
234+
log({ error: true, data: ex });
235+
process.exit(1);
236+
}
237+
}
238+
239+
function saveOutput(options: { path: string; data: string }) {
240+
try {
241+
print({ message: `Saving output to ${options.path}` });
242+
writeFileSync(options.path, options.data);
243+
} catch (ex) {
244+
print({
245+
message: `There was an error saving the file, make sure that you have access to the file location and that any directories in the path already exist.`,
246+
data: ex.message,
247+
});
248+
log({ error: true, data: ex });
249+
process.exit(1);
250+
}
251+
}
252+
253+
function printHelp() {
254+
const help = [
255+
{
256+
param: '--query, -q',
257+
message: 'A SOQL query surrounded in quotes or a file path to a text file containing a SOQL query.',
258+
},
259+
{
260+
param: '--compose, -c',
261+
message:
262+
'An escaped and quoted parsed SOQL JSON string or a file path to a text file containing a parsed query JSON object.',
263+
},
264+
{ param: '--output, -o', message: 'Filepath.' },
265+
{ param: '--json, -j', message: 'Provide all output messages as JSON.' },
266+
{ param: '--debug, -d', message: 'Print additional debug log messages.' },
267+
{ param: '--help, -h', message: 'Show this help message.' },
268+
];
269+
print({ message: 'SOQL Parser JS CLI -- Help' });
270+
print({
271+
message:
272+
'To use the CLI, provide one or more of the following commands. Either one of the query or compose method are required',
273+
});
274+
help.forEach(item => {
275+
print({ overrideColor: '0', data: `${pad(item.param, 20, 4)}${item.message}` });
276+
});
277+
}
278+
279+
run();

lib/utils.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,12 @@ export function getAsArrayStr(val: string | string[], alwaysParens: boolean = fa
3939
return alwaysParens ? `(${val || ''})` : val || '';
4040
}
4141
}
42+
43+
export function pad(val: string, len: number, left: number = 0) {
44+
let leftPad = left > 0 ? new Array(left).fill(' ').join('') : '';
45+
if (val.length > len) {
46+
return `${leftPad}${val}`;
47+
} else {
48+
return `${leftPad}${val}${new Array(len - val.length).fill(' ').join('')}`;
49+
}
50+
}

0 commit comments

Comments
 (0)