Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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