Skip to content

Commit f5d235c

Browse files
authored
Merge pull request #41 from paustint/bug-36.1
bugfixes for code formatter
2 parents 100f309 + ad71f9e commit f5d235c

File tree

6 files changed

+194
-11
lines changed

6 files changed

+194
-11
lines changed

docs/package-lock.json

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
"react-dom": "^16.5.2",
1414
"react-scripts-ts": "3.1.0",
1515
"react-syntax-highlighter": "^9.0.0",
16-
"soql-parser-js": "^0.3.3"
16+
"soql-parser-js": "^0.4.0"
1717
},
1818
"scripts": {
1919
"start": "react-scripts-ts start",

docs/src/components/parse-soql.tsx

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Button, DefaultButton } from 'office-ui-fabric-react/lib/Button';
22
import { TextField } from 'office-ui-fabric-react/lib/TextField';
3+
import { Checkbox } from 'office-ui-fabric-react/lib/Checkbox';
34
import * as React from 'react';
45
import * as CopyToClipboard from 'react-copy-to-clipboard';
56
import { parseQuery, Query, composeQuery } from 'soql-parser-js';
@@ -17,6 +18,7 @@ interface IParseSoqlState {
1718
parsedSoql: string;
1819
composedQuery?: string;
1920
soql: string;
21+
format: boolean;
2022
}
2123

2224
export class ParseSoql extends React.Component<IParseSoqlProps, IParseSoqlState> {
@@ -31,6 +33,7 @@ export class ParseSoql extends React.Component<IParseSoqlProps, IParseSoqlState>
3133
parsedSoql: JSON.stringify(parsedSoql || '', null, 4),
3234
composedQuery,
3335
soql: props.soql || '',
36+
format: true,
3437
};
3538
}
3639

@@ -63,10 +66,11 @@ export class ParseSoql extends React.Component<IParseSoqlProps, IParseSoqlState>
6366
}
6467
};
6568

66-
public parseQuery = (query?: string) => {
69+
public parseQuery = (query?: string, format?: boolean) => {
6770
try {
71+
format = typeof format === 'boolean' ? format : this.state.format;
6872
const parsedSoql: Query = parseQuery(query || this.state.soql);
69-
const composedQuery: string = composeQuery(parsedSoql);
73+
const composedQuery: string = composeQuery(parsedSoql, { format });
7074
this.setState({
7175
parsedSoql: JSON.stringify(parsedSoql, null, 4),
7276
composedQuery,
@@ -84,6 +88,11 @@ export class ParseSoql extends React.Component<IParseSoqlProps, IParseSoqlState>
8488
}
8589
};
8690

91+
public toggleFormat = () => {
92+
this.setState({ format: !this.state.format });
93+
this.parseQuery(this.state.soql, !this.state.format);
94+
};
95+
8796
public render() {
8897
return (
8998
<div className="ms-Fabric" dir="ltr">
@@ -118,7 +127,11 @@ export class ParseSoql extends React.Component<IParseSoqlProps, IParseSoqlState>
118127
<div className="ms-Grid-row">
119128
<div className="ms-Grid-col ms-sm12">
120129
<div className="ms-font-l">Parsed Query</div>
121-
<SyntaxHighlighter language="json" style={xonokai} customStyle={{ maxHeight: 400, minHeight: 400, marginTop: 0, marginBottom: 5 }}>
130+
<SyntaxHighlighter
131+
language="json"
132+
style={xonokai}
133+
customStyle={{ maxHeight: 400, minHeight: 400, marginTop: 0, marginBottom: 5 }}
134+
>
122135
{this.state.parsedSoql}
123136
</SyntaxHighlighter>
124137
<div>
@@ -146,6 +159,9 @@ export class ParseSoql extends React.Component<IParseSoqlProps, IParseSoqlState>
146159
<SyntaxHighlighter language="sql" style={xonokai} customStyle={{ marginTop: 0, marginBottom: 5 }}>
147160
{this.state.composedQuery}
148161
</SyntaxHighlighter>
162+
<div style={{ margin: 5 }}>
163+
<Checkbox label="Format Output" checked={this.state.format} onChange={this.toggleFormat} />
164+
</div>
149165
<CopyToClipboard text={this.state.composedQuery}>
150166
<Button
151167
primary={true}

lib/SoqlComposer.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -222,10 +222,9 @@ export class Compose {
222222
: '';
223223
}
224224
if (where.right) {
225-
return `${output}${this.formatter.formatAddNewLine()}${utils.get(where.operator)} ${this.parseWhereClause(
226-
where.right,
227-
isSubquery
228-
)}`.trim();
225+
return `${output}${this.formatter.formatAddNewLine(' ', isSubquery)}${utils.get(
226+
where.operator
227+
)} ${this.parseWhereClause(where.right, isSubquery)}`.trim();
229228
} else {
230229
return output.trim();
231230
}

lib/SoqlFormatter.ts

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { composeQuery } from './SoqlComposer';
2+
import { parseQuery } from './SoqlParser';
3+
4+
export interface FieldData {
5+
fields: {
6+
text: string;
7+
isSubquery: boolean;
8+
prefix: string;
9+
suffix: string;
10+
}[];
11+
isSubquery: boolean;
12+
lineBreaks: number[];
13+
}
14+
15+
export interface FormatOptions {
16+
active?: boolean;
17+
numIndent?: number;
18+
fieldMaxLineLen?: number;
19+
logging?: boolean;
20+
}
21+
22+
export function formatQuery(soql: string) {
23+
return composeQuery(parseQuery(soql), { format: true });
24+
}
25+
26+
export class Formatter {
27+
options: FormatOptions;
28+
29+
constructor(options: FormatOptions) {
30+
this.options = {
31+
active: false,
32+
numIndent: 1,
33+
fieldMaxLineLen: 60,
34+
logging: false,
35+
...options,
36+
};
37+
}
38+
39+
private log(data: any) {
40+
if (this.options.logging) {
41+
console.log(data);
42+
}
43+
}
44+
45+
private getIndent() {
46+
return new Array(this.options.numIndent).fill('\t').join('');
47+
}
48+
49+
formatFields(fieldData: FieldData) {
50+
function trimPrevSuffix(currIdx: number) {
51+
if (fieldData.fields[currIdx - 1]) {
52+
fieldData.fields[currIdx - 1].suffix = fieldData.fields[currIdx - 1].suffix.trim();
53+
}
54+
}
55+
56+
fieldData.fields.forEach((field, i) => {
57+
field.suffix = fieldData.fields.length - 1 === i ? '' : ', ';
58+
});
59+
60+
if (this.options.active) {
61+
let lineLen = 0;
62+
let newLineAndIndentNext = false;
63+
fieldData.fields.forEach((field, i) => {
64+
if (field.isSubquery) {
65+
// Subquery should always be on a stand-alone line
66+
trimPrevSuffix(i);
67+
field.prefix = `\n${this.getIndent()}`;
68+
field.suffix = fieldData.fields.length - 1 === i ? '' : ', ';
69+
lineLen = 0;
70+
newLineAndIndentNext = true;
71+
} else if (this.options.fieldMaxLineLen) {
72+
// If max line length is specified, create a new line when needed
73+
lineLen += field.text.length;
74+
if (lineLen > this.options.fieldMaxLineLen || newLineAndIndentNext) {
75+
trimPrevSuffix(i);
76+
field.prefix += '\n\t';
77+
lineLen = 0;
78+
newLineAndIndentNext = false;
79+
}
80+
}
81+
82+
this.log(field);
83+
});
84+
}
85+
}
86+
87+
formatClause(clause: string, isSubquery: boolean = false) {
88+
if (isSubquery) {
89+
return this.options.active ? `\n${this.getIndent()}${clause}` : ` ${clause}`;
90+
} else {
91+
return this.options.active ? `\n${clause}` : ` ${clause}`;
92+
}
93+
}
94+
formatAddNewLine(alt: string = ' ', isSubquery: boolean = false) {
95+
if (isSubquery) {
96+
return this.options.active ? `\n${this.getIndent()}` : alt;
97+
} else {
98+
return this.options.active ? `\n` : alt;
99+
}
100+
}
101+
}

test/TestCasesForFormat.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
export interface TestCaseForFormat {
2+
testCase: number;
3+
soql: string;
4+
formattedSoql: string;
5+
}
6+
7+
export const testCases: TestCaseForFormat[] = [
8+
{
9+
testCase: 1,
10+
soql: 'SELECT Id, Name, (SELECT Id, Name FROM Contacts), Foo, Bar, BillingCity FROM Account',
11+
formattedSoql: `SELECT Id, Name,
12+
\t(SELECT Id, Name
13+
\tFROM Contacts),
14+
\tFoo, Bar, BillingCity
15+
FROM Account
16+
`.trim(),
17+
},
18+
{
19+
testCase: 2,
20+
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%'`,
21+
formattedSoql: `SELECT Id, Name, Foo, Bar, Baz, Bee, Boo, Bam, Moo, Maz, Man, Name, Id, Name, Foo, Bar, Baz, Bee, Boo,
22+
\tBam, Moo, Maz, Man, Name, Id, Name, Foo, Bar, Baz, Bee, Boo, Bam, Moo, Maz, Man, Name, Id, Name, Foo,
23+
\tBar, Baz, Bee, Boo, Bam, Moo, Maz, Man, Name,
24+
\t(SELECT Name
25+
\tFROM Line_Items__r)
26+
FROM Merchandise__c
27+
WHERE Name LIKE 'Acme%'
28+
`.trim(),
29+
},
30+
{
31+
testCase: 3,
32+
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`,
33+
formattedSoql: `SELECT UserId, COUNT(Id)
34+
FROM LoginHistory
35+
WHERE LoginTime > 2010-09-20T22:16:30.000Z
36+
AND LoginTime < 2010-09-21T22:16:30.000Z
37+
GROUP BY UserId
38+
`.trim(),
39+
},
40+
{
41+
testCase: 2,
42+
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')))`,
43+
formattedSoql: `SELECT Id
44+
FROM Account
45+
WHERE (Id IN ('1', '2', '3')
46+
OR (NOT Id = '2')
47+
OR (Name LIKE '%FOO%'
48+
OR (Name LIKE '%ARM%'
49+
AND FOO = 'bar')))
50+
`.trim(),
51+
},
52+
{
53+
testCase: 2,
54+
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)`,
55+
formattedSoql: `SELECT Id, Name
56+
FROM Account
57+
WHERE Id IN (SELECT AccountId
58+
\tFROM Contact
59+
\tWHERE LastName LIKE 'apple%'
60+
\tAND foo = 'bar')
61+
AND Id IN (SELECT AccountId
62+
\tFROM Opportunity
63+
\tWHERE isClosed = false)
64+
`.trim(),
65+
},
66+
];
67+
export default testCases;

0 commit comments

Comments
 (0)