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
36 changes: 36 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,41 @@
# Changelog

## 3.2.0

March 27, 2021

A number of improvements to the formatter have been made with this release.

- The formatter option `whereClauseOperatorsIndented` has been deprecated and will always be applied.
- A new boolean formatter option named `newLineAfterKeywords` has been added and will ensure that there is always a new line after any keyword. (#137)
- `TYPEOF` fields will now always be included on their own line be default, or will span multiple lines, split by keywords if `newLineAfterKeywords` is set to true. (#135)

## Example

`SELECT Id, TYPEOF What WHEN Account THEN Phone, NumberOfEmployees WHEN Opportunity THEN Amount, CloseDate ELSE Name, Email END, Name FROM Event`

`formatOptions: { newLineAfterKeywords: true, fieldMaxLineLength: 1 },`

```sql
SELECT
Id,
TYPEOF What
WHEN
Account
THEN
Phone, NumberOfEmployees
WHEN
Opportunity
THEN
Amount, CloseDate
ELSE
Name, Email
END,
Name
FROM
Event
```

## 3.1.0

March 27, 2021
Expand Down
15 changes: 8 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,13 +96,14 @@ Many of hte utility functions are provided to easily determine the shape of spec

**FormatOptions**

| Property | Type | Description | required | default |
| ---------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------ | -------- | ------- |
| numIndent | number | The number of tab characters to indent. | FALSE | 1 |
| fieldMaxLineLength | number | The number of characters that the fields should take up before making a new line. Set this to 1 to have every field on its own line. | FALSE | 60 |
| fieldSubqueryParensOnOwnLine | boolean | If true, the opening and closing parentheses will be on their own line for subqueries. | FALSE | TRUE |
| whereClauseOperatorsIndented | boolean | If true, indents the where clause operators. | FALSE | FALSE |
| logging | boolean | Print out logging statements to the console about the format operation. | FALSE | FALSE |
| Property | Type | Description | required | default |
| -------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------- |
| numIndent | number | The number of tab characters to indent. | FALSE | 1 |
| fieldMaxLineLength | number | The number of characters that the fields should take up before making a new line. Set this to 1 to have every field on its own line. | FALSE | 60 |
| fieldSubqueryParensOnOwnLine | boolean | If true, the opening and closing parentheses will be on their own line for subqueries. | FALSE | TRUE |
| newLineAfterKeywords | boolean | Adds a new line and indent after all keywords (such as SELECT, FROM, WHERE, ORDER BY, etc..) Setting this to true will add new lines in other places as well, such as complex WHERE clauses | FALSE | FALSE |
| ~~whereClauseOperatorsIndented~~ | boolean | **Deprecated** If true, indents the where clause operators. | FALSE | FALSE |
| logging | boolean | Print out logging statements to the console about the format operation. | FALSE | FALSE |

## Examples

Expand Down
6 changes: 6 additions & 0 deletions docs/src/modules/my/queryComposer/queryComposer.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
textarea,
pre {
-moz-tab-size: 2;
-o-tab-size: 2;
tab-size: 2;
}
4 changes: 2 additions & 2 deletions docs/src/modules/my/queryComposer/queryComposer.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
<ui-checkbox name="fieldSubqueryParensOnOwnLine" value={fieldSubqueryParensOnOwnLine} disabled={formDisabled} onchange={handleChange}>
Subquery parenthesis on own line - <code class="font-semibold">fieldSubqueryParensOnOwnLine</code>
</ui-checkbox>
<ui-checkbox name="whereClauseOperatorsIndented" value={whereClauseOperatorsIndented} disabled={formDisabled} onchange={handleChange}>
Indent items in WHERE clause - <code class="font-semibold">whereClauseOperatorsIndented</code>
<ui-checkbox name="newLineAfterKeywords" value={newLineAfterKeywords} disabled={formDisabled} onchange={handleChange}>
Add newline and indent after keywords - <code class="font-semibold">newLineAfterKeywords</code>
</ui-checkbox>
<ui-input
name="fieldMaxLineLength"
Expand Down
16 changes: 8 additions & 8 deletions docs/src/modules/my/queryComposer/queryComposer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Query, composeQuery } from 'soql-parser-js';
import * as hljs from 'highlight.js/lib/highlight.js';
hljs.registerLanguage('sql', require('highlight.js/lib/languages/sql'));

const DEFAULT_LINE_LEN = 60;
const DEFAULT_LINE_LEN = 1;
const NOT_DIGIT_RGX = /[^\d]/g;

export default class QueryComposer extends LightningElement {
Expand All @@ -21,7 +21,7 @@ export default class QueryComposer extends LightningElement {
@track composedQuery: string;
@track formatOutput = true;
@track fieldSubqueryParensOnOwnLine = true;
@track whereClauseOperatorsIndented = false;
@track newLineAfterKeywords = true;
@track fieldMaxLineLength = DEFAULT_LINE_LEN;
hasRendered = false;
fieldMaxLineLengthTransformFn = (value: string): string => {
Expand All @@ -40,15 +40,15 @@ export default class QueryComposer extends LightningElement {
`// format: ${this.formatOutput},\n` +
`// formatOptions: {\n` +
`// fieldSubqueryParensOnOwnLine: ${this.fieldSubqueryParensOnOwnLine},\n` +
`// whereClauseOperatorsIndented: ${this.whereClauseOperatorsIndented},\n` +
`// newLineAfterKeywords: ${this.newLineAfterKeywords},\n` +
`// fieldMaxLineLength: ${this.fieldMaxLineLength},\n` +
`// }\n` +
`// });\n`;

element.innerText =
`// composeQuery(parsedQuery), {\n` +
`// format: ${this.formatOutput},\n` +
`// formatOptions: { fieldSubqueryParensOnOwnLine: ${this.fieldSubqueryParensOnOwnLine}, whereClauseOperatorsIndented: ${this.whereClauseOperatorsIndented}, fieldMaxLineLength: ${this.fieldMaxLineLength} }\n` +
`// formatOptions: { fieldSubqueryParensOnOwnLine: ${this.fieldSubqueryParensOnOwnLine}, newLineAfterKeywords: ${this.newLineAfterKeywords}, fieldMaxLineLength: ${this.fieldMaxLineLength} }\n` +
`// });\n`;

// element.innerText = `// composeQuery(parsedQuery, { format: ${this.formatOutput}, formatOptions: { fieldSubqueryParensOnOwnLine, whereClauseOperatorsIndented, fieldMaxLineLength } });`;
Expand All @@ -59,10 +59,10 @@ export default class QueryComposer extends LightningElement {
composeQuery() {
try {
if (this.parsedQuery) {
const { fieldSubqueryParensOnOwnLine, whereClauseOperatorsIndented, fieldMaxLineLength } = this;
const { fieldSubqueryParensOnOwnLine, newLineAfterKeywords, fieldMaxLineLength } = this;
this.composedQuery = composeQuery(JSON.parse(JSON.stringify(this.parsedQuery)), {
format: this.formatOutput,
formatOptions: { fieldSubqueryParensOnOwnLine, whereClauseOperatorsIndented, fieldMaxLineLength }
formatOptions: { fieldSubqueryParensOnOwnLine, newLineAfterKeywords, fieldMaxLineLength }
});
this.highlight();
}
Expand All @@ -82,8 +82,8 @@ export default class QueryComposer extends LightningElement {
this.fieldSubqueryParensOnOwnLine = value;
break;
}
case 'whereClauseOperatorsIndented': {
this.whereClauseOperatorsIndented = value;
case 'newLineAfterKeywords': {
this.newLineAfterKeywords = value;
break;
}
case 'fieldMaxLineLength': {
Expand Down
106 changes: 65 additions & 41 deletions src/composer/composer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,79 +138,86 @@ export class Compose {
public parseQuery(query: Query | Subquery): string {
const fieldData: FieldData = {
fields: this.parseFields(query.fields).map(field => ({
text: field,
isSubquery: field.startsWith('('),
text: field.text,
typeOfClause: field.typeOfClause,
isSubquery: field.text.startsWith('('),
prefix: '',
suffix: '',
})),
isSubquery: utils.isSubquery(query),
lineBreaks: [],
};

let output = `SELECT `;
let output = this.formatter.formatClause('SELECT').trimStart();

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

let fieldsOutput = '';
fieldData.fields.forEach(field => {
output += `${field.prefix}${field.text}${field.suffix}`;
if (Array.isArray(field.typeOfClause)) {
fieldsOutput += `${field.prefix}${this.formatter.formatTyeOfField(field.text, field.typeOfClause)}${field.suffix}`;
} else {
fieldsOutput += `${field.prefix}${field.text}${field.suffix}`;
}
});
output += this.formatter.formatText(fieldsOutput);

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

if (utils.isSubquery(query)) {
const sObjectPrefix = query.sObjectPrefix || [];
sObjectPrefix.push(query.relationshipName);
output += ` ${sObjectPrefix.join('.')}${utils.get(query.sObjectAlias, '', ' ')}`;
output += this.formatter.formatText(`${sObjectPrefix.join('.')}${utils.get(query.sObjectAlias, '', ' ')}`);
} else {
output += ` ${query.sObject}${utils.get(query.sObjectAlias, '', ' ')}`;
output += this.formatter.formatText(`${query.sObject}${utils.get(query.sObjectAlias, '', ' ')}`);
}
this.log(output);

if (query.usingScope) {
output += this.formatter.formatClause('USING SCOPE');
output += ` ${query.usingScope}`;
output += this.formatter.formatText(query.usingScope);
this.log(output);
}

if (query.where) {
output += this.formatter.formatClause('WHERE');
output += ` ${this.parseWhereOrHavingClause(query.where)}`;
output += this.formatter.formatText(this.parseWhereOrHavingClause(query.where));
this.log(output);
}

if (query.groupBy) {
output += this.formatter.formatClause('GROUP BY');
output += ` ${this.parseGroupByClause(query.groupBy)}`;
output += this.formatter.formatText(this.parseGroupByClause(query.groupBy));
this.log(output);
if (query.groupBy.having) {
output += this.formatter.formatClause('HAVING');
output += ` ${this.parseWhereOrHavingClause(query.groupBy.having)}`;
output += this.formatter.formatText(this.parseWhereOrHavingClause(query.groupBy.having));
this.log(output);
}
}

if (query.orderBy && (!Array.isArray(query.orderBy) || query.orderBy.length > 0)) {
output += this.formatter.formatClause('ORDER BY');
output += ` ${this.parseOrderBy(query.orderBy)}`;
output += this.formatter.formatText(this.parseOrderBy(query.orderBy));
this.log(output);
}

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

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

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

Expand All @@ -221,13 +228,13 @@ export class Compose {

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

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

Expand All @@ -240,34 +247,44 @@ export class Compose {
* @param fields
* @returns fields
*/
public parseFields(fields: FieldType[]): string[] {
public parseFields(fields: FieldType[]): { text: string; typeOfClause?: string[] }[] {
return fields.map(field => {
let text = '';
let typeOfClause: string[];

const objPrefix = (field as any).objectPrefix ? `${(field as any).objectPrefix}.` : '';
switch (field.type) {
case 'Field': {
return `${objPrefix}${field.field}${field.alias ? ` ${field.alias}` : ''}`;
text = `${objPrefix}${field.field}${field.alias ? ` ${field.alias}` : ''}`;
break;
}
case 'FieldFunctionExpression': {
let params = '';
if (field.parameters) {
params = field.parameters
.map(param => (utils.isString(param) ? param : this.parseFields([param as FieldFunctionExpression])))
.map(param => (utils.isString(param) ? param : this.parseFields([param as FieldFunctionExpression]).map(param => param.text)))
.join(', ');
}
return `${field.functionName}(${params})${field.alias ? ` ${field.alias}` : ''}`;
text = `${field.functionName}(${params})${field.alias ? ` ${field.alias}` : ''}`;
break;
}
case 'FieldRelationship': {
return `${objPrefix}${field.relationships.join('.')}.${field.field}${utils.hasAlias(field) ? ` ${field.alias}` : ''}`;
text = `${objPrefix}${field.relationships.join('.')}.${field.field}${utils.hasAlias(field) ? ` ${field.alias}` : ''}`;
break;
}
case 'FieldSubquery': {
return this.formatter.formatSubquery(this.parseQuery(field.subquery));
text = this.formatter.formatSubquery(this.parseQuery(field.subquery));
break;
}
case 'FieldTypeof': {
return this.parseTypeOfField(field);
typeOfClause = this.parseTypeOfField(field);
text = typeOfClause.join(' ');
break;
}
default:
break;
}
return { text, typeOfClause };
});
}

Expand All @@ -277,14 +294,11 @@ export class Compose {
* @param typeOfField
* @returns type of field
*/
public parseTypeOfField(typeOfField: FieldTypeOf): 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`;
public parseTypeOfField(typeOfField: FieldTypeOf): string[] {
const output = [`TYPEOF ${typeOfField.field}`].concat(
typeOfField.conditions.map(condition => this.formatter.formatTypeofFieldCondition(condition)),
);
output.push(`END`);
return output;
}

Expand All @@ -296,29 +310,39 @@ export class Compose {
* @param priorIsNegationOperator - do not set this when calling manually. Recursive call will set this to ensure proper formatting.
* @returns where clause
*/
public parseWhereOrHavingClause(whereOrHaving: WhereClause | HavingClause): string {
public parseWhereOrHavingClause(whereOrHaving: WhereClause | HavingClause, tabOffset = 0, priorConditionIsNegation = false): string {
let output = '';
const left = whereOrHaving.left;
let trimPrecedingOutput = false;
if (left) {
output += utils.generateParens(left.openParen, '(');
output += this.formatter.formatParens(left.openParen, '(', utils.isNegationCondition(left));
if (!utils.isNegationCondition(left)) {
output += utils.isValueFunctionCondition(left) ? this.parseFn(left.fn) : left.field;
output += ` ${left.operator} `;
tabOffset = tabOffset + (left.openParen || 0) - (left.closeParen || 0);
if (priorConditionIsNegation) {
tabOffset++;
}
let expression = '';
expression += utils.isValueFunctionCondition(left) ? this.parseFn(left.fn) : left.field;
expression += ` ${left.operator} `;

if (utils.isValueQueryCondition(left)) {
output += this.formatter.formatSubquery(this.parseQuery(left.valueQuery), 1, true);
expression += this.formatter.formatSubquery(this.parseQuery(left.valueQuery), 1, true);
} else {
output += utils.getAsArrayStr(utils.getWhereValue(left.value, left.literalType));
expression += utils.getAsArrayStr(utils.getWhereValue(left.value, left.literalType));
}
output += utils.generateParens(left.closeParen, ')');
output += this.formatter.formatWithIndent(expression);
output += this.formatter.formatParens(left.closeParen, ')', priorConditionIsNegation);
}
}
if (utils.isWhereOrHavingClauseWithRightCondition(whereOrHaving)) {
const operator = utils.get(whereOrHaving.operator);
trimPrecedingOutput = operator === 'NOT';
const formattedData = this.formatter.formatWhereClauseOperators(
utils.get(whereOrHaving.operator),
this.parseWhereOrHavingClause(whereOrHaving.right),
operator,
this.parseWhereOrHavingClause(whereOrHaving.right, tabOffset, utils.isNegationCondition(left)),
tabOffset,
);
return `${output}${formattedData}`.trim();
return `${trimPrecedingOutput ? output.trimRight() : output}${formattedData}`.trim();
} else {
return output.trim();
}
Expand Down
Loading