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
6 changes: 4 additions & 2 deletions debug/test.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
var soqlParserJs = require('../dist');

const query = `
SELECT Account.Name, (SELECT Contact.LastName FROM Account.Contact.Foo.Bars) FROM Account
SELECT Id, Name FROM Account WHERE Id IN (SELECT AccountId FROM Contact WHERE LastName LIKE 'apple%') AND Id IN (SELECT AccountId FROM Opportunity WHERE isClosed = false)
`;

const parsedQuery = soqlParserJs.parseQuery(query, { logging: true });

console.log(JSON.stringify(parsedQuery, null, 2));

const composedQuery = soqlParserJs.composeQuery(parsedQuery, { logging: true, format: true });
console.log(composedQuery);

// SELECT Id, Name FROM Account WHERE Id IN (SELECT AccountId FROM Opportunity WHERE StageName = 'Closed Lost')
// SELECT Id FROM Account WHERE Id NOT IN (SELECT AccountId FROM Opportunity WHERE IsClosed = false)
// SELECT Id, Name FROM Account WHERE Id IN (SELECT AccountId FROM Contact WHERE LastName LIKE 'apple%') AND Id IN (SELECT AccountId FROM Opportunity WHERE isClosed = false)
95 changes: 69 additions & 26 deletions lib/SoqlComposer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,25 @@ import {
WithDataCategoryClause,
} from './models/SoqlQuery.model';
import * as utils from './utils';
import { FieldData, Formatter } from './SoqlFormatter';

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

export function composeQuery(soql: Query, config: Partial<SoqlComposeConfig> = {}): string {
config.format = config.format ? true : false;
if (config.logging) {
console.time('parser');
console.log('Parsing Query:', soql);
console.time('composer');
console.log('Composing Query:', soql);
console.log('Format output:', config.format);
}

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

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

return query;
Expand All @@ -35,12 +39,21 @@ export class Compose {
private subqueryFieldReplaceRegex = /^{|}$/g;

public logging: boolean = false;
public format: boolean = false;
public query: string;
public formatter: Formatter;

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

this.formatter = new Formatter({
active: this.format,
logging: this.logging,
});

this.start();
}

Expand All @@ -54,22 +67,42 @@ export class Compose {
}
}

private parseQuery(query: Query): string {
let output = `SELECT`;
// Parse Fields
const fields = this.parseFields(query.fields);
private parseQuery(query: Query, isSubquery: boolean = false): string {
const fieldData: FieldData = {
fields: this.parseFields(query.fields).map(field => ({
text: field,
isSubquery: false,
prefix: '',
suffix: '',
})),
isSubquery,
lineBreaks: [],
};

let output = `SELECT `;

// Replace subquery fields with parsed subqueries
fields.forEach((field, i) => {
if (field.match(this.subqueryFieldRegex)) {
fieldData.fields.forEach((field, i) => {
if (field.text.match(this.subqueryFieldRegex)) {
const subquery = query.subqueries.find(
subquery => subquery.sObjectRelationshipName === field.replace(this.subqueryFieldReplaceRegex, '')
subquery => subquery.sObjectRelationshipName === field.text.replace(this.subqueryFieldReplaceRegex, '')
);
if (subquery) {
fields[i] = `(${this.parseQuery(subquery)})`;
fieldData.fields[i].text = `(${this.parseQuery(subquery, true)})`;
fieldData.fields[i].isSubquery = true;
}
}
});
output += ` ${fields.join(', ').trim()} FROM`;

// Format fields based on configuration
this.formatter.formatFields(fieldData);

fieldData.fields.forEach(field => {
output += `${field.prefix}${field.text}${field.suffix}`;
});

output += this.formatter.formatClause('FROM', isSubquery);

if (query.sObjectRelationshipName) {
const sObjectPrefix = query.sObjectPrefix || [];
sObjectPrefix.push(query.sObjectRelationshipName);
Expand All @@ -80,53 +113,60 @@ export class Compose {
this.log(output);

if (query.where) {
output += ` WHERE ${this.parseWhereClause(query.where)}`;
output += this.formatter.formatClause('WHERE', isSubquery);
output += ` ${this.parseWhereClause(query.where, isSubquery)}`;
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)}`;
output += this.formatter.formatClause('GROUP BY', isSubquery);
output += ` ${this.parseGroupByClause(query.groupBy)}`;
this.log(output);
if (query.having) {
output += ` HAVING ${this.parseHavingClause(query.having)}`;
output += this.formatter.formatClause('HAVING', isSubquery);
output += ` ${this.parseHavingClause(query.having)}`;
this.log(output);
}
}

if (query.orderBy) {
output += ` ORDER BY ${this.parseOrderBy(query.orderBy)}`;
output += this.formatter.formatClause('ORDER BY', isSubquery);
output += ` ${this.parseOrderBy(query.orderBy)}`;
this.log(output);
}

if (utils.isNumber(query.limit)) {
output += ` LIMIT ${query.limit}`;
output += this.formatter.formatClause('LIMIT', isSubquery);
output += ` ${query.limit}`;
this.log(output);
}

if (utils.isNumber(query.offset)) {
output += ` OFFSET ${query.offset}`;
output += this.formatter.formatClause('OFFSET', isSubquery);
output += ` ${query.offset}`;
this.log(output);
}

if (query.withDataCategory) {
output += ` WITH DATA CATEGORY ${this.parseWithDataCategory(query.withDataCategory)}`;
output += this.formatter.formatClause('WITH DATA CATEGORY', isSubquery);
output += ` ${this.parseWithDataCategory(query.withDataCategory)}`;
this.log(output);
}

if (query.for) {
output += ` FOR ${query.for}`;
output += this.formatter.formatClause('FOR', isSubquery);
output += ` ${query.for}`;
this.log(output);
}

if (query.update) {
output += ` UPDATE ${query.update}`;
output += this.formatter.formatClause('UPDATE', isSubquery);
output += ` ${query.update}`;
this.log(output);
}

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

return output;
}

Expand Down Expand Up @@ -163,7 +203,7 @@ export class Compose {
return `${(fn.text || '').replace(/,/g, ', ')} ${fn.alias || ''}`.trim();
}

private parseWhereClause(where: WhereClause): string {
private parseWhereClause(where: WhereClause, isSubquery: boolean): string {
let output = '';
if (where.left) {
output +=
Expand All @@ -174,15 +214,18 @@ export class Compose {
output += where.left.fn ? this.parseFn(where.left.fn) : where.left.field;
output += ` ${where.left.operator} `;
output += where.left.valueQuery
? `(${this.parseQuery(where.left.valueQuery)})`
? `(${this.parseQuery(where.left.valueQuery, true)})`
: 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();
return `${output}${this.formatter.formatAddNewLine()}${utils.get(where.operator)} ${this.parseWhereClause(
where.right,
isSubquery
)}`.trim();
} else {
return output.trim();
}
Expand Down
10 changes: 10 additions & 0 deletions test/SoqlParser.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { parseQuery, composeQuery } from '../lib';
import { expect } from 'chai';
import 'mocha';
import testCases from './TestCases';
import testCasesForFormat from './TestCasesForFormat';
import { formatQuery } from '../lib/SoqlFormatter';

const replacements = [
{ matching: / and /i, replace: ' AND ' },
Expand Down Expand Up @@ -31,3 +33,11 @@ describe('compose queries', () => {
});
});
});
describe('format queries', () => {
testCasesForFormat.forEach(testCase => {
it(`should format query - test case ${testCase.testCase} - ${testCase.soql}`, () => {
const formattedQuery = formatQuery(testCase.soql);
expect(formattedQuery).equal(testCase.formattedSoql);
});
});
});