Skip to content

Commit

Permalink
Merge pull request #29 from paustint/feature-17
Browse files Browse the repository at this point in the history
Implement CLI #17
  • Loading branch information
paustint authored Oct 14, 2018
2 parents 3d8efd6 + f31d18b commit 295f4d1
Show file tree
Hide file tree
Showing 10 changed files with 453 additions and 459 deletions.
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,33 @@ export interface FunctionExp {
}
```

## CLI Usage
The CLI can be used to parse a query or compose a previously parsed query back to SOQL.

**Examples:**
```shell
$ npm install -g soql-parser-js
$ soql --help
$ soql --query "SELECT Id FROM Account"
$ soql -query "SELECT Id FROM Account"
$ soql -query "SELECT Id FROM Account" -output some-output-file.json
$ soql -query "SELECT Id FROM Account" -json
$ soql -query some-input-file.txt
$ soql -compose some-input-file.json
$ soql -compose some-input-file.json
$ soql -compose some-input-file.json -output some-output-file.json
```

**Arguments:**
```
--query, -q A SOQL query surrounded in quotes or a file path to a text file containing a SOQL query.
--compose, -c An escaped and quoted parsed SOQL JSON string or a file path to a text file containing a parsed query JSON object.
--output, -o Filepath.
--json, -j Provide all output messages as JSON.
--debug, -d Print additional debug log messages.
--help, -h Show this help message.
```

## Contributing
All contributions are welcome on the project. Please read the [contribution guidelines](https://github.com/paustint/soql-parser-js/blob/master/CONTRIBUTING.md).

Expand Down
4 changes: 4 additions & 0 deletions debug/cli.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
var argv = require('minimist')(process.argv.slice(2));

console.log('argv:');
console.log(JSON.stringify(argv, null, 2));
279 changes: 279 additions & 0 deletions lib/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,279 @@
import * as soqlParser from '.';
import { isString, pad } from './utils';
import { existsSync, readFileSync, writeFileSync } from 'fs';
import { isObject } from 'util';

const argv = require('minimist')(process.argv.slice(2));

interface Options {
query: string | undefined;
compose: string | undefined;
output: string | undefined;
}

interface Print {
error?: boolean;
message?: string;
data?: string;
debug?: boolean;
overrideColor?: string;
}

const debug: boolean | undefined = argv.debug || argv.d;
const printJson: boolean | undefined = argv.json || argv.j;

log({ data: JSON.stringify(argv, null, 2) });

function log(options: Print) {
if (debug) {
print({ ...options, debug: true });
}
}

function print(options: Print) {
let color = options.error ? '31' : options.overrideColor;
if (printJson && !options.debug) {
if (isString(options.data)) {
try {
options.data = JSON.parse(options.data);
} catch (ex) {}
}
console.log(JSON.stringify(options), '\n');
} else {
if (options.debug && options.message) {
color = color || '33';
options.message = `[DEBUG] ${options.message}`;
console.log(`\x1b[${color}m%s\x1b[0m`, options.message);
} else if (options.message) {
color = color || '32';
console.log(`\x1b[${color}m%s\x1b[0m`, options.message);
}

// reset color to default
color = options.error ? '31' : options.overrideColor;

if (options.data) {
if (isObject(options.data)) {
options.data = JSON.stringify(options.data, null, 2);
}
}

if (options.debug && options.data) {
color = color || '33';
options.data = `[DEBUG]\n${options.data}`;
console.log(`\x1b[${color}m%s\x1b[0m`, options.data);
} else if (options.data) {
color = color || '1';
console.log(`\x1b[${color}m%s\x1b[0m`, options.data);
}
}
}

function run() {
const options: Options = {
query: argv.query || argv.q,
compose: argv.compose || argv.c,
output: argv.output || argv.o,
};

log({ message: 'Options', data: JSON.stringify(options, null, 2) });

const help: boolean | undefined = argv.help || argv.h;
let validParams = false;

if (isString(options.query)) {
log({ message: 'Parsing Query' });
validParams = true;
parseQuery(options);
}

if (isString(options.compose)) {
log({ message: 'Composing Query' });
validParams = true;
composeQuery(options);
}

if (isString(help)) {
log({ message: 'Showing explicit Help' });
validParams = true;
printHelp();
}

if (!validParams) {
log({ message: 'Showing implicit Help' });
printHelp();
}

process.exit(0);
}

function parseQuery(options: Options) {
// if query starts with SELECT we know it is not a file, otherwise we will attempt to parse a file
// Check if query does not look like a query - attempt to parse file if so
let query = options.query;
log({ message: query });
if (
!options.query
.trim()
.toUpperCase()
.startsWith('SELECT')
) {
log({ message: 'Query does not start with select, attempting to read file' });
try {
if (existsSync(options.query)) {
query = readFileSync(options.query, 'utf8');
log({ message: 'Query read from file:', data: query });
if (
!query
.trim()
.toUpperCase()
.startsWith('SELECT')
) {
print({
error: true,
message: `The query contained within the file ${
options.query
} does not appear to be valid, please make sure the query starts with SELECT.`,
});
process.exit(1);
}
} else {
print({
error: true,
message: 'The query must start with SELECT or must be a valid file path to a text file containing the query.',
});
process.exit(1);
}
} catch (ex) {
print({
error: true,
message: `There was an error parsing the file ${
options.query
}. Please ensure the file exists and is a text file containing a single SOQL query.`,
});
log({ error: true, data: ex });
process.exit(1);
}
}

try {
const parsedQuery = soqlParser.parseQuery(query);
const queryJson = JSON.stringify(parsedQuery, null, 2);
log({ data: queryJson });
if (options.output) {
saveOutput({ path: options.output, data: queryJson });
} else {
print({
message: `Parsed Query:`,
data: queryJson,
});
}
} catch (ex) {
print({
error: true,
message: `There was an error parsing your query`,
data: ex.message,
});
log({ error: true, data: ex });
process.exit(1);
}
}

function composeQuery(options: Options) {
// if query starts with SELECT we know it is not a file, otherwise we will attempt to parse a file
// Check if query does not look like a query - attempt to parse file if so
let parsedQueryString = options.compose;
if (!options.compose.trim().startsWith('{')) {
log({ message: 'Compose is a filepath - attempting to read file' });
try {
if (existsSync(options.compose)) {
parsedQueryString = readFileSync(options.compose, 'utf8');
log({
message: 'Parsed query data JSON read from file',
data: parsedQueryString,
});
} else {
print({
error: true,
message: `The file ${
options.compose
} does not exist, Please provide a valid filepath or an escaped JSON string.`,
});
process.exit(1);
}
} catch (ex) {
print({
error: true,
message: `There was an error reading the file ${
options.compose
}. Please ensure the file exists and is a text file containing a single parsed query JSON object.`,
});
log({ error: true, data: ex });
process.exit(1);
}
}

try {
const parsedQuery = JSON.parse(parsedQueryString);
const query = soqlParser.composeQuery(parsedQuery);
if (options.output) {
log({ message: 'Attempting to save query to file' });
saveOutput({ path: options.output, data: query });
} else {
print({
message: `Composed Query:`,
data: query,
});
}
} catch (ex) {
print({
error: true,
message: `There was an error composing your query.`,
data: ex.message,
});
log({ error: true, data: ex });
process.exit(1);
}
}

function saveOutput(options: { path: string; data: string }) {
try {
print({ message: `Saving output to ${options.path}` });
writeFileSync(options.path, options.data);
} catch (ex) {
print({
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.`,
data: ex.message,
});
log({ error: true, data: ex });
process.exit(1);
}
}

function printHelp() {
const help = [
{
param: '--query, -q',
message: 'A SOQL query surrounded in quotes or a file path to a text file containing a SOQL query.',
},
{
param: '--compose, -c',
message:
'An escaped and quoted parsed SOQL JSON string or a file path to a text file containing a parsed query JSON object.',
},
{ param: '--output, -o', message: 'Filepath.' },
{ param: '--json, -j', message: 'Provide all output messages as JSON.' },
{ param: '--debug, -d', message: 'Print additional debug log messages.' },
{ param: '--help, -h', message: 'Show this help message.' },
];
print({ message: 'SOQL Parser JS CLI -- Help' });
print({
message:
'To use the CLI, provide one or more of the following commands. Either one of the query or compose method are required',
});
help.forEach(item => {
print({ overrideColor: '0', data: `${pad(item.param, 20, 4)}${item.message}` });
});
}

run();
9 changes: 9 additions & 0 deletions lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,12 @@ export function getAsArrayStr(val: string | string[], alwaysParens: boolean = fa
return alwaysParens ? `(${val || ''})` : val || '';
}
}

export function pad(val: string, len: number, left: number = 0) {
let leftPad = left > 0 ? new Array(left).fill(' ').join('') : '';
if (val.length > len) {
return `${leftPad}${val}`;
} else {
return `${leftPad}${val}${new Array(len - val.length).fill(' ').join('')}`;
}
}
Loading

0 comments on commit 295f4d1

Please sign in to comment.