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: 3 additions & 3 deletions docs/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"react-dom": "^16.5.2",
"react-scripts-ts": "3.1.0",
"react-syntax-highlighter": "^9.0.0",
"soql-parser-js": "^0.3.3"
"soql-parser-js": "^0.4.0"
},
"scripts": {
"start": "react-scripts-ts start",
Expand Down
22 changes: 19 additions & 3 deletions docs/src/components/parse-soql.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Button, DefaultButton } from 'office-ui-fabric-react/lib/Button';
import { TextField } from 'office-ui-fabric-react/lib/TextField';
import { Checkbox } from 'office-ui-fabric-react/lib/Checkbox';
import * as React from 'react';
import * as CopyToClipboard from 'react-copy-to-clipboard';
import { parseQuery, Query, composeQuery } from 'soql-parser-js';
Expand All @@ -17,6 +18,7 @@ interface IParseSoqlState {
parsedSoql: string;
composedQuery?: string;
soql: string;
format: boolean;
}

export class ParseSoql extends React.Component<IParseSoqlProps, IParseSoqlState> {
Expand All @@ -31,6 +33,7 @@ export class ParseSoql extends React.Component<IParseSoqlProps, IParseSoqlState>
parsedSoql: JSON.stringify(parsedSoql || '', null, 4),
composedQuery,
soql: props.soql || '',
format: true,
};
}

Expand Down Expand Up @@ -63,10 +66,11 @@ export class ParseSoql extends React.Component<IParseSoqlProps, IParseSoqlState>
}
};

public parseQuery = (query?: string) => {
public parseQuery = (query?: string, format?: boolean) => {
try {
format = typeof format === 'boolean' ? format : this.state.format;
const parsedSoql: Query = parseQuery(query || this.state.soql);
const composedQuery: string = composeQuery(parsedSoql);
const composedQuery: string = composeQuery(parsedSoql, { format });
this.setState({
parsedSoql: JSON.stringify(parsedSoql, null, 4),
composedQuery,
Expand All @@ -84,6 +88,11 @@ export class ParseSoql extends React.Component<IParseSoqlProps, IParseSoqlState>
}
};

public toggleFormat = () => {
this.setState({ format: !this.state.format });
this.parseQuery(this.state.soql, !this.state.format);
};

public render() {
return (
<div className="ms-Fabric" dir="ltr">
Expand Down Expand Up @@ -118,7 +127,11 @@ export class ParseSoql extends React.Component<IParseSoqlProps, IParseSoqlState>
<div className="ms-Grid-row">
<div className="ms-Grid-col ms-sm12">
<div className="ms-font-l">Parsed Query</div>
<SyntaxHighlighter language="json" style={xonokai} customStyle={{ maxHeight: 400, minHeight: 400, marginTop: 0, marginBottom: 5 }}>
<SyntaxHighlighter
language="json"
style={xonokai}
customStyle={{ maxHeight: 400, minHeight: 400, marginTop: 0, marginBottom: 5 }}
>
{this.state.parsedSoql}
</SyntaxHighlighter>
<div>
Expand Down Expand Up @@ -146,6 +159,9 @@ export class ParseSoql extends React.Component<IParseSoqlProps, IParseSoqlState>
<SyntaxHighlighter language="sql" style={xonokai} customStyle={{ marginTop: 0, marginBottom: 5 }}>
{this.state.composedQuery}
</SyntaxHighlighter>
<div style={{ margin: 5 }}>
<Checkbox label="Format Output" checked={this.state.format} onChange={this.toggleFormat} />
</div>
<CopyToClipboard text={this.state.composedQuery}>
<Button
primary={true}
Expand Down
7 changes: 3 additions & 4 deletions lib/SoqlComposer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,10 +222,9 @@ export class Compose {
: '';
}
if (where.right) {
return `${output}${this.formatter.formatAddNewLine()}${utils.get(where.operator)} ${this.parseWhereClause(
where.right,
isSubquery
)}`.trim();
return `${output}${this.formatter.formatAddNewLine(' ', isSubquery)}${utils.get(
where.operator
)} ${this.parseWhereClause(where.right, isSubquery)}`.trim();
} else {
return output.trim();
}
Expand Down
101 changes: 101 additions & 0 deletions lib/SoqlFormatter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { composeQuery } from './SoqlComposer';
import { parseQuery } from './SoqlParser';

export interface FieldData {
fields: {
text: string;
isSubquery: boolean;
prefix: string;
suffix: string;
}[];
isSubquery: boolean;
lineBreaks: number[];
}

export interface FormatOptions {
active?: boolean;
numIndent?: number;
fieldMaxLineLen?: number;
logging?: boolean;
}

export function formatQuery(soql: string) {
return composeQuery(parseQuery(soql), { format: true });
}

export class Formatter {
options: FormatOptions;

constructor(options: FormatOptions) {
this.options = {
active: false,
numIndent: 1,
fieldMaxLineLen: 60,
logging: false,
...options,
};
}

private log(data: any) {
if (this.options.logging) {
console.log(data);
}
}

private getIndent() {
return new Array(this.options.numIndent).fill('\t').join('');
}

formatFields(fieldData: FieldData) {
function trimPrevSuffix(currIdx: number) {
if (fieldData.fields[currIdx - 1]) {
fieldData.fields[currIdx - 1].suffix = fieldData.fields[currIdx - 1].suffix.trim();
}
}

fieldData.fields.forEach((field, i) => {
field.suffix = fieldData.fields.length - 1 === i ? '' : ', ';
});

if (this.options.active) {
let lineLen = 0;
let newLineAndIndentNext = false;
fieldData.fields.forEach((field, i) => {
if (field.isSubquery) {
// Subquery should always be on a stand-alone line
trimPrevSuffix(i);
field.prefix = `\n${this.getIndent()}`;
field.suffix = fieldData.fields.length - 1 === i ? '' : ', ';
lineLen = 0;
newLineAndIndentNext = true;
} else if (this.options.fieldMaxLineLen) {
// If max line length is specified, create a new line when needed
lineLen += field.text.length;
if (lineLen > this.options.fieldMaxLineLen || newLineAndIndentNext) {
trimPrevSuffix(i);
field.prefix += '\n\t';
lineLen = 0;
newLineAndIndentNext = false;
}
}

this.log(field);
});
}
}

formatClause(clause: string, isSubquery: boolean = false) {
if (isSubquery) {
return this.options.active ? `\n${this.getIndent()}${clause}` : ` ${clause}`;
} else {
return this.options.active ? `\n${clause}` : ` ${clause}`;
}
}
formatAddNewLine(alt: string = ' ', isSubquery: boolean = false) {
if (isSubquery) {
return this.options.active ? `\n${this.getIndent()}` : alt;
} else {
return this.options.active ? `\n` : alt;
}
}
}
67 changes: 67 additions & 0 deletions test/TestCasesForFormat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
export interface TestCaseForFormat {
testCase: number;
soql: string;
formattedSoql: string;
}

export const testCases: TestCaseForFormat[] = [
{
testCase: 1,
soql: 'SELECT Id, Name, (SELECT Id, Name FROM Contacts), Foo, Bar, BillingCity FROM Account',
formattedSoql: `SELECT Id, Name,
\t(SELECT Id, Name
\tFROM Contacts),
\tFoo, Bar, BillingCity
FROM Account
`.trim(),
},
{
testCase: 2,
soql: `SELECT Id, Name, Foo, Bar, Baz, Bee, Boo, Bam, Moo, Maz, Man, Name, Id, Name, Foo, Bar, Baz, Bee, Boo, Bam, Moo, Maz, Man, Name,Id, Name, Foo, Bar, Baz, Bee, Boo, Bam, Moo, Maz, Man, Name,Id, Name, Foo, Bar, Baz, Bee, Boo, Bam, Moo, Maz, Man, Name,(SELECT Name FROM Line_Items__r) FROM Merchandise__c WHERE Name LIKE 'Acme%'`,
formattedSoql: `SELECT Id, Name, Foo, Bar, Baz, Bee, Boo, Bam, Moo, Maz, Man, Name, Id, Name, Foo, Bar, Baz, Bee, Boo,
\tBam, Moo, Maz, Man, Name, Id, Name, Foo, Bar, Baz, Bee, Boo, Bam, Moo, Maz, Man, Name, Id, Name, Foo,
\tBar, Baz, Bee, Boo, Bam, Moo, Maz, Man, Name,
\t(SELECT Name
\tFROM Line_Items__r)
FROM Merchandise__c
WHERE Name LIKE 'Acme%'
`.trim(),
},
{
testCase: 3,
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`,
formattedSoql: `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
`.trim(),
},
{
testCase: 2,
soql: `SELECT Id FROM Account WHERE (Id IN ('1', '2', '3') OR (NOT Id = '2') OR (Name LIKE '%FOO%' OR (Name LIKE '%ARM%' AND FOO = 'bar')))`,
formattedSoql: `SELECT Id
FROM Account
WHERE (Id IN ('1', '2', '3')
OR (NOT Id = '2')
OR (Name LIKE '%FOO%'
OR (Name LIKE '%ARM%'
AND FOO = 'bar')))
`.trim(),
},
{
testCase: 2,
soql: `SELECT Id, Name FROM Account WHERE Id IN (SELECT AccountId FROM Contact WHERE LastName LIKE 'apple%' AND foo = 'bar') AND Id IN (SELECT AccountId FROM Opportunity WHERE isClosed = false)`,
formattedSoql: `SELECT Id, Name
FROM Account
WHERE Id IN (SELECT AccountId
\tFROM Contact
\tWHERE LastName LIKE 'apple%'
\tAND foo = 'bar')
AND Id IN (SELECT AccountId
\tFROM Opportunity
\tWHERE isClosed = false)
`.trim(),
},
];
export default testCases;