diff --git a/CHANGELOG.md b/CHANGELOG.md index d637ba0..38b292a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 1.2.0 + +- Changed compose methods to public to allow external access (#65) +- Fixed lodash security vulnerability +- Updated README to reflect new changes and other minor changes + ## 1.1.1 - Removed files that accidentally got included with release with update of `release-it` diff --git a/README.md b/README.md index 46ad137..1eff75e 100644 --- a/README.md +++ b/README.md @@ -6,43 +6,90 @@ ## Description +:sunny: This library allows parsing Salesforce SOQL queries using JavaScript or Typescript. Works in the browser and node. :sunny: + SOQL Parser JS provides the following capabilities: 1. Parse a SOQL query into a usable data structure. -2. Turn a parsed query data structure back into well a SOQL query with various format options. -3. Check if a SOQL query is syntactically valid (**note**: some cases may be structurally sound but not allowed by SFDC). +2. Turn a parsed data structure back into a well formed SOQL query with various format options. +3. Check if a SOQL query is syntactically valid (**note**: some cases may be structurally valid but not allowed by Salesforce, e.x. invalid field name). This library is written in Typescript and all type definitions are included with the library for your benefit if you choose to use Typescript or use VSCode's automatic type checking. -_Warning_: antlr4 is dependency for this library and is a rather large library (~600 KB) and is required for the parser to function, use in the browser with care. +:warning: antlr4 is dependency for this library and is a rather large library (~600 KB) and is required for the parser to function. Consider using dynamic imports to achieve lazy loading. ## Examples -For an example of the parser, check out the [example application](https://paustint.github.io/soql-parser-js/). +Want to try it out? [Check out the demo](https://paustint.github.io/soql-parser-js/). -Have a look through the unit tests for many more examples. +Look through the [unit tests](./test/TestCases.ts) for additional examples. # Usage ## Parsing -Parsing a SOQL query can be completed by calling `parseQuery(soqlQueryString, options)` and a `Query` data structure will be returned; +Parsing a SOQL query can be completed by calling `parseQuery(soqlQueryString, options)`. A `Query` data structure will be returned; #### Typescript / ES6 ```typescript -import { parseQuery } from 'soql-parser-js'; // TS / ES6 imports +import { parseQuery } from 'soql-parser-js'; // var soqlParserJs = require('soql-parser-js'); // node's require format - usage: soqlParserJs.parseQuery() -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'; +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'; +`; const soqlQuery = parseQuery(soql); -// const soqlQuery = soqlParserJs.parseQuery(soql); // using require() console.log(JSON.stringify(soqlQuery, null, 2)); ``` +**Results** + +```json +{ + "fields": [ + { + "type": "Field", + "field": "UserId" + }, + { + "type": "FieldFunctionExpression", + "rawValue": "COUNT(Id)", + "fn": "COUNT", + "isAggregateFn": true, + "parameters": ["Id"] + } + ], + "sObject": "LoginHistory", + "where": { + "left": { + "field": "LoginTime", + "operator": ">", + "value": "2010-09-20T22:16:30.000Z", + "literalType": "DATETIME" + }, + "operator": "AND", + "right": { + "left": { + "field": "LoginTime", + "operator": "<", + "value": "2010-09-21T22:16:30.000Z", + "literalType": "DATETIME" + } + } + }, + "groupBy": { + "field": "UserId" + } +} +``` + #### Options ```typescript @@ -54,13 +101,13 @@ export interface SoqlQueryConfig { ## Composing -Composing a query will turn a Query object back to a SOQL query. The exact same data structure returned from `parseQuery()` can be used, -but there are many use-cases where you may need to build your own data structure to compose a query. +Composing a query will turn a Query object back to a SOQL query string. The exact same data structure returned from `parseQuery()` can be used, +but depending on your use-case, you may need to build your own data structure to compose a query. These examples show building your own Query object with the minimum required fields. -**Note:** For some operators, they may be converted to upper case (e.x. NOT, AND) +:page_facing_up: **Note:** Some operators may be converted to upper case (e.x. NOT, AND) -**Note:** There are a number of fields populated on the Query object when `parseQuery()` is called that are not required to compose a query. Look at the examples below and the comments in the data model for more information. +:page_facing_up: **Note:** There are a number of fields populated on the Query object when `parseQuery()` is called that are not required to compose a query. Look at the examples below and the comments in the data model for more information. **The base query object is shaped like this:** @@ -84,7 +131,7 @@ The easiest way to build the fields is to call the utility function `getComposed ### Example -This is the query that will be composed +This is the query that will be composed programmatically ```sql SELECT Id, Name, FORMAT(Amount) MyFormattedAmount, @@ -139,7 +186,7 @@ const soqlQuery = { operator: '=', value: 'Closed Won', // literalType is optional, but if set to STRING and our value is not already wrapped in "'", they will be added - // All other literalType values are ignored in composing a query + // All other literalType values are ignored when composing a query literalType: 'STRING', }, }, @@ -152,8 +199,9 @@ const composedQuery = composeQuery(soqlQuery, { format: true }); console.log(composedQuery); ``` -In the above examples, we made use of `getComposedField(input: string | ComposeFieldInput)` to help easily compose the fields. The input expects a string or one of the following shapes of data below. An error will be thrown if the data passed in is not one of the following shapes: -and will return a `FieldType` object. +In the example above, we made use of `getComposedField(input: string | ComposeFieldInput)` to compose the fields. The input expects a string or one of the following data structures listed below. An error will be thrown if the data passed in is not one of the following shapes. + +This returns a `FieldType` object. ```typescript export interface ComposeField { @@ -183,9 +231,53 @@ export interface ComposeFieldTypeof { } ``` +### Composing a partial query + +If you need to compose just a part of a query instead of the entire query, you can create an instance of the Compose class directly. + +For example, if you just need the "WHERE" clause from a query as a string, you can do the following: + +```typescript +import { Compose, getComposedField, parseQuery } from 'soql-parser-js'; + +const soql = `SELECT Id FROM Account WHERE Name = 'Foo'`; +const parsedQuery = parseQuery(soql); + +// Results of Parsed Query: + // const parsedQuery = { + // fields: [ + // { + // type: 'Field', + // field: 'Id', + // }, + // ], + // sObject: 'Account', + // where: { + // left: { + // field: 'Name', + // operator: '=', + // value: "'Foo'", + // literalType: 'STRING', + // }, + // }, + // }; + + // Create a new instance of the compose class and set the autoCompose to false to avoid composing the entire query + const composer = new Compose(parsedQuery, { autoCompose: false }); + + + const whereClause = composer.parseWhereClause(parsedQuery.where); + + console.log(whereClause); + // Name = 'Foo' + +} + +``` + ## Checking if a Query is Valid -This will parse the AST tree to confirm the syntax is valid, but will not parse the tree into a data structure. +This will parse a Query to confirm valid syntax, but will not parse into the Query data structure, which will have a small performance gain. This method is faster than parsing the full query. Options: @@ -204,25 +296,13 @@ const soql = const isValid = isQueryValid(soql); -console.log('isValid', isValid); -``` - -#### Node - -```javascript -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'; - -const isValid = isQueryValid(soql); - -console.log('isValid', isValid); +console.log(isValid); ``` ## Format Query -This function is provided as a convenience and just calls parse and compose under the hood. +This function is provided as a convenience and just calls parse and compose. +[Check out the demo](https://paustint.github.io/soql-parser-js/) to see the outcome of the various format options. ```typescript import { formatQuery } from 'soql-parser-js'; @@ -317,6 +397,7 @@ export interface SoqlComposeConfig { logging: boolean; // default=false format: boolean; // default=false formatOptions?: FormatOptions; + autoCompose: boolean; // default=true } export interface FormatOptions { @@ -333,18 +414,19 @@ export interface FormatOptions { The following utility functions are available: 1. `getComposedField(input: string | ComposeFieldInput)` -1. Convenience method to construct fields in the correct data format. See example usage in the Compose example. -1. `isSubquery(query: Query | Subquery)` -1. Returns true if the data passed in is a subquery -1. `getFlattenedFields(query: Query)` -1. This provides a list of fields that are stringified and flattened in order to access data from a returned API call from Salesforce. Refer to `tests/publicUtils.spec.ts` for usage examples. + 1. Convenience method to construct fields in the correct data format. See example usage in the Compose example. +2. `isSubquery(query: Query | Subquery)` + 1. Returns true if the data passed in is a subquery +3. `getFlattenedFields(query: Query)` + 1. Flatten a Salesforce record based on the parsed SOQL Query. this is useful if you have relationships in your query and want to show the results in a table, using `.` dot notation for the relationship fields. + 2. Refer to `tests/publicUtils.spec.ts` for usage examples. ## Data Models -### Query - These are all available for import in your typescript projects +### Query + ```typescript export type LogicalOperator = 'AND' | 'OR'; export type Operator = '=' | '!=' | '<=' | '>=' | '>' | '<' | 'LIKE' | 'IN' | 'NOT IN' | 'INCLUDES' | 'EXCLUDES'; @@ -498,6 +580,33 @@ export interface WithDataCategoryCondition { } ``` +### Compose Class + +You only need to interact with the compose class if you want to compose part of a SOQL query + +```typescript +// Instance Properties +logging: boolean; // default to false +format: boolean; // default to false +query: string; +formatter: Formatter; + +// Instance Methods: +constructor(private soql: Query, config?: Partial) +start(): void +// Pass in part of the parsed query to get the string representation for a given segment of a query +parseQuery(query: Query | Subquery): string +parseFields(fields: FieldType[]): string[] +parseTypeOfField(typeOfField: FieldTypeOf): string +parseFn(fn: FunctionExp): string +parseWhereClause(where: WhereClause): string +parseGroupByClause(groupBy: GroupByClause): string +parseHavingClause(having: HavingClause): string +parseOrderBy(orderBy: OrderByClause | OrderByClause[]): string +parseWithDataCategory(withDataCategory: WithDataCategoryClause): string + +``` + ### Utils ```typescript diff --git a/docs/package-lock.json b/docs/package-lock.json index ad4a75c..07fd677 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -50,6 +50,11 @@ "resolved": "http://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=" }, + "@types/lodash": { + "version": "4.14.136", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.136.tgz", + "integrity": "sha512-0GJhzBdvsW2RUccNHOBkabI8HZVdOXmXbXhuKlDEd5Vv12P7oAVGfomGp3Ne21o5D/qu1WmthlNKFaoZJJeErA==" + }, "@types/node": { "version": "10.11.7", "resolved": "https://registry.npmjs.org/@types/node/-/node-10.11.7.tgz", @@ -1811,7 +1816,7 @@ }, "chalk": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", "requires": { "ansi-styles": "^2.2.1", @@ -2232,7 +2237,7 @@ "dependencies": { "minimist": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" } } @@ -3305,7 +3310,7 @@ }, "jest-diff": { "version": "22.4.3", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-22.4.3.tgz", + "resolved": "http://registry.npmjs.org/jest-diff/-/jest-diff-22.4.3.tgz", "integrity": "sha512-/QqGvCDP5oZOF6PebDuLwrB2BMD8ffJv6TAGAdEVuDx1+uEgrHpSFrfrOiMRx2eJ1hgNjlQrOQEHetVwij90KA==", "requires": { "chalk": "^2.0.1", @@ -3316,7 +3321,7 @@ }, "jest-matcher-utils": { "version": "22.4.3", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-22.4.3.tgz", + "resolved": "http://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-22.4.3.tgz", "integrity": "sha512-lsEHVaTnKzdAPR5t4B6OcxXo9Vy4K+kRRbG5gtddY8lBEC+Mlpvm1CJcsMESRjzUhzkz568exMV1hTB76nAKbA==", "requires": { "chalk": "^2.0.1", @@ -3343,7 +3348,7 @@ }, "pretty-format": { "version": "22.4.3", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-22.4.3.tgz", + "resolved": "http://registry.npmjs.org/pretty-format/-/pretty-format-22.4.3.tgz", "integrity": "sha512-S4oT9/sT6MN7/3COoOy+ZJeA92VmOnveLHgrwBE3Z1W5N9S2A1QGNYiE1z75DAENbJrXXUb+OWXhpJcg05QKQQ==", "requires": { "ansi-regex": "^3.0.0", @@ -3689,7 +3694,7 @@ "dependencies": { "combined-stream": { "version": "1.0.6", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", + "resolved": "http://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", "requires": { "delayed-stream": "~1.0.0" @@ -5947,9 +5952,9 @@ } }, "lodash": { - "version": "4.17.11", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", - "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==" + "version": "4.17.14", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.14.tgz", + "integrity": "sha512-mmKYbW3GLuJeX+iGP+Y7Gp1AiGHGbXHCOh/jZmrawMmsE7MS4znI3RL2FsjbqOyMayHInjOeykW7PEajUk1/xw==" }, "lodash._reinterpolate": { "version": "3.0.0", @@ -6180,7 +6185,7 @@ "dependencies": { "minimist": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" } } @@ -6272,7 +6277,7 @@ }, "minimist": { "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" }, "mississippi": { @@ -6727,7 +6732,7 @@ }, "os-locale": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", + "resolved": "http://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", "requires": { "lcid": "^1.0.0" @@ -6987,7 +6992,7 @@ "dependencies": { "async": { "version": "1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "resolved": "http://registry.npmjs.org/async/-/async-1.5.2.tgz", "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=" } } @@ -8413,7 +8418,7 @@ "dependencies": { "minimist": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" } } @@ -9259,7 +9264,7 @@ }, "minimist": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" } } @@ -9874,7 +9879,7 @@ }, "strip-ansi": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "requires": { "ansi-regex": "^2.0.0" @@ -9938,7 +9943,7 @@ "dependencies": { "minimist": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" } } @@ -10322,7 +10327,7 @@ }, "jest-diff": { "version": "22.4.3", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-22.4.3.tgz", + "resolved": "http://registry.npmjs.org/jest-diff/-/jest-diff-22.4.3.tgz", "integrity": "sha512-/QqGvCDP5oZOF6PebDuLwrB2BMD8ffJv6TAGAdEVuDx1+uEgrHpSFrfrOiMRx2eJ1hgNjlQrOQEHetVwij90KA==", "requires": { "chalk": "^2.0.1", @@ -10370,7 +10375,7 @@ }, "jest-matcher-utils": { "version": "22.4.3", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-22.4.3.tgz", + "resolved": "http://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-22.4.3.tgz", "integrity": "sha512-lsEHVaTnKzdAPR5t4B6OcxXo9Vy4K+kRRbG5gtddY8lBEC+Mlpvm1CJcsMESRjzUhzkz568exMV1hTB76nAKbA==", "requires": { "chalk": "^2.0.1", @@ -10402,7 +10407,7 @@ }, "jest-resolve": { "version": "22.4.3", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-22.4.3.tgz", + "resolved": "http://registry.npmjs.org/jest-resolve/-/jest-resolve-22.4.3.tgz", "integrity": "sha512-u3BkD/MQBmwrOJDzDIaxpyqTxYH+XqAXzVJP51gt29H8jpj3QgKof5GGO2uPGKGeA1yTMlpbMs1gIQ6U4vcRhw==", "requires": { "browser-resolve": "^1.11.2", @@ -10411,7 +10416,7 @@ }, "jest-snapshot": { "version": "22.4.3", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-22.4.3.tgz", + "resolved": "http://registry.npmjs.org/jest-snapshot/-/jest-snapshot-22.4.3.tgz", "integrity": "sha512-JXA0gVs5YL0HtLDCGa9YxcmmV2LZbwJ+0MfyXBBc5qpgkEYITQFJP7XNhcHFbUvRiniRpRbGVfJrOoYhhGE0RQ==", "requires": { "chalk": "^2.0.1", @@ -10506,7 +10511,7 @@ }, "pretty-format": { "version": "22.4.3", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-22.4.3.tgz", + "resolved": "http://registry.npmjs.org/pretty-format/-/pretty-format-22.4.3.tgz", "integrity": "sha512-S4oT9/sT6MN7/3COoOy+ZJeA92VmOnveLHgrwBE3Z1W5N9S2A1QGNYiE1z75DAENbJrXXUb+OWXhpJcg05QKQQ==", "requires": { "ansi-regex": "^3.0.0", @@ -10646,7 +10651,7 @@ }, "minimist": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" }, "strip-bom": { @@ -11676,7 +11681,7 @@ "dependencies": { "yargs": { "version": "3.10.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", + "resolved": "http://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", "requires": { "camelcase": "^1.0.2", @@ -12190,7 +12195,7 @@ }, "yargs": { "version": "6.6.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-6.6.0.tgz", + "resolved": "http://registry.npmjs.org/yargs/-/yargs-6.6.0.tgz", "integrity": "sha1-eC7CHvQDNF+DCoCMo9UTr1YGUgg=", "requires": { "camelcase": "^3.0.0", @@ -12210,7 +12215,7 @@ }, "yargs-parser": { "version": "4.2.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-4.2.1.tgz", + "resolved": "http://registry.npmjs.org/yargs-parser/-/yargs-parser-4.2.1.tgz", "integrity": "sha1-KczqwNxPA8bIe0qfIX3RjJ90hxw=", "requires": { "camelcase": "^3.0.0" @@ -12241,7 +12246,7 @@ }, "jsonfile": { "version": "2.4.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", + "resolved": "http://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", "integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug=", "requires": { "graceful-fs": "^4.1.6" diff --git a/docs/package.json b/docs/package.json index 5a40cc2..b466f22 100644 --- a/docs/package.json +++ b/docs/package.json @@ -4,9 +4,11 @@ "private": true, "homepage": "http://paustint.github.io/soql-parser-js", "dependencies": { + "@types/lodash": "^4.14.136", "@types/react-copy-to-clipboard": "^4.2.6", "@types/react-syntax-highlighter": "0.0.7", "gh-pages": "^2.0.1", + "lodash": "^4.17.14", "office-ui-fabric-react": "^6.84.0", "react": "^16.5.2", "react-copy-to-clipboard": "^5.0.1", diff --git a/lib/SoqlComposer.ts b/lib/SoqlComposer.ts index 05e91e0..766cf09 100644 --- a/lib/SoqlComposer.ts +++ b/lib/SoqlComposer.ts @@ -19,6 +19,7 @@ export interface SoqlComposeConfig { logging: boolean; // default=false format: boolean; // default=false formatOptions?: FormatOptions; + autoCompose: boolean; // default=true } /** * Formats query - This will compose and then parse a query with the provided format options @@ -33,6 +34,8 @@ export function formatQuery(soql: string, formatOptions?: FormatOptions) { /** * Composes a parsed query back to a SOQL query + * The parsing methods are public in case there is a need to parse just a part of a query, + * but the common case is to call the "start()" method * @param soql * @param [config] * @returns query @@ -66,6 +69,7 @@ export class Compose { public formatter: Formatter; constructor(private soql: Query, config: Partial = {}) { + config = { autoCompose: true, ...config }; const { logging } = config; this.logging = logging; this.format = config.format; @@ -75,8 +79,9 @@ export class Compose { logging: this.logging, ...config.formatOptions, }); - - this.start(); + if (config.autoCompose) { + this.start(); + } } /** @@ -86,6 +91,10 @@ export class Compose { this.query = this.parseQuery(this.soql); } + /** + * If logging is enabled, print the query to the console + * @param soql + */ private log(soql: string) { if (this.logging) { console.log('Current SOQL:', soql); @@ -99,7 +108,7 @@ export class Compose { * @param query * @returns query */ - private parseQuery(query: Query | Subquery): string { + public parseQuery(query: Query | Subquery): string { const fieldData: FieldData = { fields: this.parseFields(query.fields).map(field => ({ text: field, @@ -137,7 +146,6 @@ export class Compose { this.log(output); } - // TODO: add WITH support https://github.com/paustint/soql-parser-js/issues/18 if (query.groupBy) { output += this.formatter.formatClause('GROUP BY'); output += ` ${this.parseGroupByClause(query.groupBy)}`; @@ -199,7 +207,7 @@ export class Compose { * @param fields * @returns fields */ - private parseFields(fields: FieldType[]): string[] { + public parseFields(fields: FieldType[]): string[] { return fields.map(field => { const objPrefix = (field as any).objectPrefix ? `${(field as any).objectPrefix}.` : ''; switch (field.type) { @@ -238,7 +246,7 @@ export class Compose { * @param typeOfField * @returns type of field */ - private parseTypeOfField(typeOfField: FieldTypeOf): string { + public parseTypeOfField(typeOfField: FieldTypeOf): string { let output = `TYPEOF ${typeOfField.field} `; output += typeOfField.conditions .map(cond => { @@ -265,7 +273,7 @@ export class Compose { * @param where * @returns where clause */ - private parseWhereClause(where: WhereClause): string { + public parseWhereClause(where: WhereClause): string { let output = ''; if (where.left) { output += @@ -289,9 +297,6 @@ export class Compose { this.parseWhereClause(where.right) ); return `${output}${formattedData}`.trim(); - // return `${output}${this.formatter.formatAddNewLine(' ')}${utils.get(where.operator)} ${this.parseWhereClause( - // where.right - // )}`.trim(); } else { return output.trim(); } @@ -303,7 +308,7 @@ export class Compose { * @param groupBy * @returns group by clause */ - private parseGroupByClause(groupBy: GroupByClause): string { + public parseGroupByClause(groupBy: GroupByClause): string { if (groupBy.type) { return `${groupBy.type}${utils.getAsArrayStr(groupBy.field, true)}`; } else { @@ -317,7 +322,7 @@ export class Compose { * @param having * @returns having clause */ - private parseHavingClause(having: HavingClause): string { + public parseHavingClause(having: HavingClause): string { let output = ''; if (having.left) { output += new Array(having.left.openParen || 0).fill('(').join(''); @@ -338,7 +343,7 @@ export class Compose { * @param orderBy * @returns order by */ - private parseOrderBy(orderBy: OrderByClause | OrderByClause[]): string { + public parseOrderBy(orderBy: OrderByClause | OrderByClause[]): string { if (Array.isArray(orderBy)) { return this.formatter.formatOrderByArray(orderBy.map(ob => this.parseOrderBy(ob))); } else { @@ -355,7 +360,7 @@ export class Compose { * @param withDataCategory * @returns with data category */ - private parseWithDataCategory(withDataCategory: WithDataCategoryClause): string { + public parseWithDataCategory(withDataCategory: WithDataCategoryClause): string { return withDataCategory.conditions .map(condition => { const params = diff --git a/package-lock.json b/package-lock.json index 0d7340d..47d52d5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -133,6 +133,11 @@ "@types/node": "*" } }, + "@types/lodash": { + "version": "4.14.136", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.136.tgz", + "integrity": "sha512-0GJhzBdvsW2RUccNHOBkabI8HZVdOXmXbXhuKlDEd5Vv12P7oAVGfomGp3Ne21o5D/qu1WmthlNKFaoZJJeErA==" + }, "@types/minimatch": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", @@ -1742,7 +1747,7 @@ }, "is-obj": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "resolved": "http://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", "dev": true }, @@ -1904,10 +1909,9 @@ } }, "lodash": { - "version": "4.17.11", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", - "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", - "dev": true + "version": "4.17.14", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.14.tgz", + "integrity": "sha512-mmKYbW3GLuJeX+iGP+Y7Gp1AiGHGbXHCOh/jZmrawMmsE7MS4znI3RL2FsjbqOyMayHInjOeykW7PEajUk1/xw==" }, "lodash.find": { "version": "4.6.0", @@ -2388,7 +2392,7 @@ }, "got": { "version": "6.7.1", - "resolved": "https://registry.npmjs.org/got/-/got-6.7.1.tgz", + "resolved": "http://registry.npmjs.org/got/-/got-6.7.1.tgz", "integrity": "sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA=", "dev": true, "requires": { @@ -2660,6 +2664,12 @@ "ms": "^2.1.1" } }, + "lodash": { + "version": "4.17.11", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", + "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", + "dev": true + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -3219,7 +3229,7 @@ }, "through": { "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "resolved": "http://registry.npmjs.org/through/-/through-2.3.8.tgz", "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", "dev": true }, diff --git a/package.json b/package.json index dc7ed1e..cdd29c5 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,9 @@ "author": "Austin Turner ", "license": "MIT", "dependencies": { + "@types/lodash": "^4.14.136", "antlr4ts": "0.4.1-alpha.0", + "lodash": "^4.17.14", "minimist": "^1.2.0" }, "devDependencies": { diff --git a/test/SoqlParser.spec.ts b/test/SoqlParser.spec.ts index bca3c44..e42b6ca 100644 --- a/test/SoqlParser.spec.ts +++ b/test/SoqlParser.spec.ts @@ -1,4 +1,4 @@ -import { parseQuery, composeQuery, isQueryValid, Query, FieldType, WhereClause, formatQuery } from '../lib'; +import { parseQuery, composeQuery, isQueryValid, Query, FieldType, WhereClause, formatQuery, Compose } from '../lib'; import { expect } from 'chai'; import 'mocha'; import testCases from './TestCases'; @@ -92,6 +92,25 @@ describe('validate queries', () => { }); }); +describe('calls individual compose methods', () => { + // TODO: add more tests + // We have adequate coverage of overall queries, but these are public and should have adequate coverage individually + it(`Should compose the where clause properly`, () => { + const soql = `SELECT Id FROM Account WHERE Name = 'Foo'`; + const parsedQuery = parseQuery(soql); + const composer = new Compose(parsedQuery, { autoCompose: false }); + const whereClause = composer.parseWhereClause(parsedQuery.where); + expect(whereClause).to.equal(`Name = 'Foo'`); + }); + it(`Should compose the where clause properly with semi-join`, () => { + const soql = `SELECT Id FROM Account WHERE Id IN (SELECT AccountId FROM Contact WHERE Name LIKE '%foo%')`; + const parsedQuery = parseQuery(soql); + const composer = new Compose(parsedQuery, { autoCompose: false }); + const whereClause = composer.parseWhereClause(parsedQuery.where); + expect(whereClause).to.equal(`Id IN (SELECT AccountId FROM Contact WHERE Name LIKE '%foo%')`); + }); +}); + function removeComposeOnlyFields(query: Query): Query { query.fields.forEach(removeComposeOnlyField); query.fields.forEach(field => {