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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
.vscode
build/tests
.rpt2_cache
/dist
dist/

# Created by https://www.gitignore.io/api/node

Expand Down
103 changes: 83 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,24 @@
## Description
SOQL Parser JS will parse a SOQL query string into an object that is easy to work with and has the query broken down into usable parts.

## TODO
- [ ] Assess all property/function/variable names and make any adjustments as needed
- [x] Analyze more SOQL parsing examples to ensure that output is appropriate
- [ ] Include information on how to contribute
- [x] Keep examples up-to-date as the package is finalized
- [x] Figure out proper build/packaging for npm
- [x] ~~Consider Webpack for build~~
- [x] Figure out how/if we can create a bundle that is browser compatible and include examples
- [ ] Provide instructions for using with node, in the browser, using TS and JS
- [ ] Figure out other builds (UMD - minified)
- [x] Create typescript typings for the bundled JS
- [x] Provide a GitHub pages example application
## Future Idea List
- [ ] Provide a CLI interface
- [ ] Provide ability to turn parsed SOQL back to SOQL
This works in the browser as long as npm is used to install the package with dependencies and the browser supports ES6 or a transpiler is used.

*Warning*: antlr4 is a very large library and is required for the parser to function, so be aware of this prior to including in your browser bundles.

## Examples
For an example of the parser, check out the [example application](https://paustint.github.io/soql-parser-js/).

### Typescript / ES6
## Usage

### Available functions
1. `parseQuery(soqlQueryString, options)`
2. `composeQuery(SoqlQuery, options)`

### Parse
The parser takes a SOQL query and returns structured data.
#### Typescript / ES6
```typescript
import { parseQuery } from './SoqlParser';
import { parseQuery } from 'soql-parser-js';

const soql = 'SELECT UserId, COUNT(Id) from LoginHistory WHERE LoginTime > 2010-09-20T22:16:30.000Z AND LoginTime < 2010-09-21T22:16:30.000Z GROUP BY UserId';

Expand All @@ -34,9 +30,9 @@ console.log(JSON.stringify(soqlQuery, null, 2));

```

### Node
#### Node
```javascript
var soqlParserJs = require("soql-parser-js");
var soqlParserJs = require('soql-parser-js');

const soql = 'SELECT UserId, COUNT(Id) from LoginHistory WHERE LoginTime > 2010-09-20T22:16:30.000Z AND LoginTime < 2010-09-21T22:16:30.000Z GROUP BY UserId';

Expand Down Expand Up @@ -83,8 +79,75 @@ This yields an object with the following structure:
}
}
```
### compose
Composing a query turns a parsed query back into a SOQL query. For some operators, they may be converted to upper case (e.x. NOT, AND)

#### Typescript / ES6
```typescript
import { composeQuery } from 'soql-parser-js';

const soqlQuery = {
fields: [
{
text: 'UserId',
},
{
fn: {
text: 'COUNT(Id)',
name: 'COUNT',
parameter: 'Id',
},
},
],
subqueries: [],
sObject: 'LoginHistory',
whereClause: {
left: {
field: 'LoginTime',
operator: '>',
value: '2010-09-20T22:16:30.000Z',
},
operator: 'AND',
right: {
left: {
field: 'LoginTime',
operator: '<',
value: '2010-09-21T22:16:30.000Z',
},
},
},
groupBy: {
field: 'UserId',
},
};

const query = composeQuery(soqlQuery);

console.log(query);

```

This yields an object with the following structure:

```sql
SELECT UserId, COUNT(Id) from LoginHistory WHERE LoginTime > 2010-09-20T22:16:30.000Z AND LoginTime < 2010-09-21T22:16:30.000Z GROUP BY UserId
```

### Options

```typescript
export interface SoqlQueryConfig {
continueIfErrors?: boolean; // default=false
logging: boolean; // default=false
includeSubqueryAsField: boolean; // default=true
}

export interface SoqlComposeConfig {
logging: boolean; // default=false
}
```

### Data Model of Parsed Data
### Data Models
```typescript
export type LogicalOperator = 'AND' | 'OR';
export type Operator = '=' | '<=' | '>=' | '>' | '<' | 'LIKE' | 'IN' | 'NOT IN' | 'INCLUDES' | 'EXCLUDES';
Expand Down
5 changes: 1 addition & 4 deletions debug/test.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
var soqlParserJs = require('../dist');

const query = `
SELECT a.Id, a.Name,
(SELECT a2.Id FROM ChildAccounts a2),
(SELECT a1.Id FROM ChildAccounts1 a1)
FROM Account a
SELECT Account.Name, (SELECT Contact.LastName FROM Account.Contacts) FROM Account
`;

const parsedQuery = soqlParserJs.parseQuery(query, { logging: true });
Expand Down
198 changes: 198 additions & 0 deletions lib/SoqlComposer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import {
Query,
Field,
FunctionExp,
WhereClause,
GroupByClause,
HavingClause,
OrderByClause,
TypeOfField,
} from './models/SoqlQuery.model';
import * as utils from './utils';

export interface SoqlComposeConfig {
logging: boolean; // default=false
}

export function composeQuery(soql: Query, config: Partial<SoqlComposeConfig> = {}): string {
if (config.logging) {
console.time('parser');
console.log('Parsing Query:', soql);
}

const query = new Compose(soql, config).query;

if (config.logging) {
console.timeEnd('parser');
}

return query;
}

export class Compose {
private subqueryFieldRegex = /^{.+}$/;
private subqueryFieldReplaceRegex = /^{|}$/g;

public logging: boolean = false;
public query: string;

constructor(private soql: Query, config: Partial<SoqlComposeConfig> = {}) {
const { logging } = config;
this.logging = logging;
this.query = '';
this.start();
}

public start(): void {
this.query = this.parseQuery(this.soql);
}

private log(soql: string) {
if (this.logging) {
console.log('Current SOQL:', soql);
}
}

private parseQuery(query: Query): string {
let output = `SELECT`;
// Parse Fields
const fields = this.parseFields(query.fields);
// Replace subquery fields with parsed subqueries
fields.forEach((field, i) => {
if (field.match(this.subqueryFieldRegex)) {
const subquery = query.subqueries.find(
subquery => subquery.sObject === field.replace(this.subqueryFieldReplaceRegex, '')
);
if (subquery) {
fields[i] = `(${this.parseQuery(subquery)})`;
}
}
});
output += ` ${fields.join(', ').trim()} FROM`;
output += ` ${utils.get(query.sObjectPrefix, '.')}${query.sObject}${utils.get(query.sObjectAlias, '', ' ')}`;
this.log(output);

if (query.whereClause) {
output += ` WHERE ${this.parseWhereClause(query.whereClause)}`;
this.log(output);
}

// TODO: add WITH support https://github.com/paustint/soql-parser-js/issues/18

if (query.groupBy) {
output += ` GROUP BY ${this.parseGroupByClause(query.groupBy)}`;
this.log(output);
if (query.having) {
output += ` HAVING ${this.parseHavingClause(query.having)}`;
this.log(output);
}
}

if (query.orderBy) {
output += ` ORDER BY ${this.parseOrderBy(query.orderBy)}`;
this.log(output);
}

if (utils.isNumber(query.limit)) {
output += ` LIMIT ${query.limit}`;
this.log(output);
}

if (utils.isNumber(query.offset)) {
output += ` OFFSET ${query.offset}`;
this.log(output);
}

// TODO: add FOR support https://github.com/paustint/soql-parser-js/issues/19

return output;
}

private parseFields(fields: Field[]): string[] {
return fields
.map(field => {
if (utils.isString(field.text)) {
return `${utils.get(field.alias, '.')}${field.text}`;
} else if (utils.isObject(field.fn)) {
// parse fn
return this.parseFn(field.fn);
} else if (utils.isString(field.subqueryObjName)) {
// needs to be replaced with subquery
return `{${field.subqueryObjName}}`;
} else if (utils.isObject(field.typeOf)) {
return this.parseTypeOfField(field.typeOf);
}
})
.filter(field => !utils.isNil(field));
}

private parseTypeOfField(typeOfField: TypeOfField): string {
let output = `TYPEOF ${typeOfField.field} `;
output += typeOfField.conditions
.map(cond => {
return `${cond.type} ${utils.get(cond.objectType, ' THEN ')}${cond.fieldList.join(', ')}`;
})
.join(' ');
output += ` END`;
return output;
}

private parseFn(fn: FunctionExp): string {
return `${(fn.text || '').replace(/,/g, ', ')} ${fn.alias || ''}`.trim();
}

private parseWhereClause(where: WhereClause): string {
let output = '';
if (where.left) {
output +=
utils.isNumber(where.left.openParen) && where.left.openParen > 0
? new Array(where.left.openParen).fill('(').join('')
: '';
output += `${utils.get(where.left.logicalPrefix, ' ')}`;
output += `${where.left.field} ${where.left.operator} ${utils.getAsArrayStr(where.left.value)}`;
output +=
utils.isNumber(where.left.closeParen) && where.left.closeParen > 0
? new Array(where.left.closeParen).fill(')').join('')
: '';
}
if (where.right) {
return `${output} ${utils.get(where.operator)} ${this.parseWhereClause(where.right)}`.trim();
} else {
return output.trim();
}
}

private parseGroupByClause(groupBy: GroupByClause): string {
if (groupBy.type) {
return `${groupBy.type}${utils.getAsArrayStr(groupBy.field, true)}`;
} else {
return (Array.isArray(groupBy.field) ? groupBy.field : [groupBy.field]).join(', ');
}
}

private parseHavingClause(having: HavingClause): string {
let output = '';
if (having.left) {
output += new Array(having.left.openParen || 0).fill('(').join('');
output += having.left.fn ? this.parseFn(having.left.fn) : having.left.field;
output += ` ${having.left.operator} ${having.left.value}`;
output += new Array(having.left.closeParen || 0).fill(')').join('');
}
if (having.right) {
return `${output} ${utils.get(having.operator)} ${this.parseHavingClause(having.right)}`;
} else {
return output.trim();
}
}

private parseOrderBy(orderBy: OrderByClause | OrderByClause[]): string {
if (Array.isArray(orderBy)) {
return orderBy.map(ob => this.parseOrderBy(ob)).join(', ');
} else {
let output = `${utils.get(orderBy.field, ' ')}`;
output += orderBy.fn ? this.parseFn(orderBy.fn) : '';
output += `${utils.get(orderBy.order, ' ')}${utils.get(orderBy.nulls, '', 'NULLS ')}`;
return output.trim();
}
}
}
Loading