diff --git a/.gitignore b/.gitignore index 1887cd1..d5218d9 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,7 @@ .vscode build/tests .rpt2_cache -/dist +dist/ # Created by https://www.gitignore.io/api/node diff --git a/README.md b/README.md index 965da3d..897e1c9 100644 --- a/README.md +++ b/README.md @@ -3,28 +3,24 @@ ## Description SOQL Parser JS will parse a SOQL query string into an object that is easy to work with and has the query broken down into usable parts. -## TODO -- [ ] Assess all property/function/variable names and make any adjustments as needed -- [x] Analyze more SOQL parsing examples to ensure that output is appropriate -- [ ] Include information on how to contribute -- [x] Keep examples up-to-date as the package is finalized -- [x] Figure out proper build/packaging for npm - - [x] ~~Consider Webpack for build~~ -- [x] Figure out how/if we can create a bundle that is browser compatible and include examples - - [ ] Provide instructions for using with node, in the browser, using TS and JS - - [ ] Figure out other builds (UMD - minified) -- [x] Create typescript typings for the bundled JS -- [x] Provide a GitHub pages example application -## Future Idea List -- [ ] Provide a CLI interface -- [ ] Provide ability to turn parsed SOQL back to SOQL +This works in the browser as long as npm is used to install the package with dependencies and the browser supports ES6 or a transpiler is used. + +*Warning*: antlr4 is a very large library and is required for the parser to function, so be aware of this prior to including in your browser bundles. ## Examples For an example of the parser, check out the [example application](https://paustint.github.io/soql-parser-js/). -### Typescript / ES6 +## Usage + +### Available functions +1. `parseQuery(soqlQueryString, options)` +2. `composeQuery(SoqlQuery, options)` + +### Parse +The parser takes a SOQL query and returns structured data. +#### Typescript / ES6 ```typescript -import { parseQuery } from './SoqlParser'; +import { parseQuery } from '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'; @@ -34,9 +30,9 @@ console.log(JSON.stringify(soqlQuery, null, 2)); ``` -### Node +#### Node ```javascript -var soqlParserJs = require("soql-parser-js"); +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'; @@ -83,8 +79,75 @@ This yields an object with the following structure: } } ``` +### compose +Composing a query turns a parsed query back into a SOQL query. For some operators, they may be converted to upper case (e.x. NOT, AND) + +#### Typescript / ES6 +```typescript +import { composeQuery } from 'soql-parser-js'; + +const soqlQuery = { + fields: [ + { + text: 'UserId', + }, + { + fn: { + text: 'COUNT(Id)', + name: 'COUNT', + parameter: 'Id', + }, + }, + ], + subqueries: [], + sObject: 'LoginHistory', + whereClause: { + left: { + field: 'LoginTime', + operator: '>', + value: '2010-09-20T22:16:30.000Z', + }, + operator: 'AND', + right: { + left: { + field: 'LoginTime', + operator: '<', + value: '2010-09-21T22:16:30.000Z', + }, + }, + }, + groupBy: { + field: 'UserId', + }, +}; + +const query = composeQuery(soqlQuery); + +console.log(query); + +``` + +This yields an object with the following structure: + +```sql +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 +``` + +### Options + +```typescript +export interface SoqlQueryConfig { + continueIfErrors?: boolean; // default=false + logging: boolean; // default=false + includeSubqueryAsField: boolean; // default=true +} + +export interface SoqlComposeConfig { + logging: boolean; // default=false +} +``` -### Data Model of Parsed Data +### Data Models ```typescript export type LogicalOperator = 'AND' | 'OR'; export type Operator = '=' | '<=' | '>=' | '>' | '<' | 'LIKE' | 'IN' | 'NOT IN' | 'INCLUDES' | 'EXCLUDES'; diff --git a/debug/test.js b/debug/test.js index 4851084..1be70fc 100644 --- a/debug/test.js +++ b/debug/test.js @@ -1,10 +1,7 @@ var soqlParserJs = require('../dist'); const query = ` -SELECT a.Id, a.Name, -(SELECT a2.Id FROM ChildAccounts a2), -(SELECT a1.Id FROM ChildAccounts1 a1) -FROM Account a +SELECT Account.Name, (SELECT Contact.LastName FROM Account.Contacts) FROM Account `; const parsedQuery = soqlParserJs.parseQuery(query, { logging: true }); diff --git a/lib/SoqlComposer.ts b/lib/SoqlComposer.ts new file mode 100644 index 0000000..a8fdc3e --- /dev/null +++ b/lib/SoqlComposer.ts @@ -0,0 +1,198 @@ +import { + Query, + Field, + FunctionExp, + WhereClause, + GroupByClause, + HavingClause, + OrderByClause, + TypeOfField, +} from './models/SoqlQuery.model'; +import * as utils from './utils'; + +export interface SoqlComposeConfig { + logging: boolean; // default=false +} + +export function composeQuery(soql: Query, config: Partial = {}): string { + if (config.logging) { + console.time('parser'); + console.log('Parsing Query:', soql); + } + + const query = new Compose(soql, config).query; + + if (config.logging) { + console.timeEnd('parser'); + } + + return query; +} + +export class Compose { + private subqueryFieldRegex = /^{.+}$/; + private subqueryFieldReplaceRegex = /^{|}$/g; + + public logging: boolean = false; + public query: string; + + constructor(private soql: Query, config: Partial = {}) { + const { logging } = config; + this.logging = logging; + this.query = ''; + this.start(); + } + + public start(): void { + this.query = this.parseQuery(this.soql); + } + + private log(soql: string) { + if (this.logging) { + console.log('Current SOQL:', soql); + } + } + + private parseQuery(query: Query): string { + let output = `SELECT`; + // Parse Fields + const fields = this.parseFields(query.fields); + // Replace subquery fields with parsed subqueries + fields.forEach((field, i) => { + if (field.match(this.subqueryFieldRegex)) { + const subquery = query.subqueries.find( + subquery => subquery.sObject === field.replace(this.subqueryFieldReplaceRegex, '') + ); + if (subquery) { + fields[i] = `(${this.parseQuery(subquery)})`; + } + } + }); + output += ` ${fields.join(', ').trim()} FROM`; + output += ` ${utils.get(query.sObjectPrefix, '.')}${query.sObject}${utils.get(query.sObjectAlias, '', ' ')}`; + this.log(output); + + if (query.whereClause) { + output += ` WHERE ${this.parseWhereClause(query.whereClause)}`; + 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)}`; + this.log(output); + if (query.having) { + output += ` HAVING ${this.parseHavingClause(query.having)}`; + this.log(output); + } + } + + if (query.orderBy) { + output += ` ORDER BY ${this.parseOrderBy(query.orderBy)}`; + this.log(output); + } + + if (utils.isNumber(query.limit)) { + output += ` LIMIT ${query.limit}`; + this.log(output); + } + + if (utils.isNumber(query.offset)) { + output += ` OFFSET ${query.offset}`; + this.log(output); + } + + // TODO: add FOR support https://github.com/paustint/soql-parser-js/issues/19 + + return output; + } + + private parseFields(fields: Field[]): string[] { + return fields + .map(field => { + if (utils.isString(field.text)) { + return `${utils.get(field.alias, '.')}${field.text}`; + } else if (utils.isObject(field.fn)) { + // parse fn + return this.parseFn(field.fn); + } else if (utils.isString(field.subqueryObjName)) { + // needs to be replaced with subquery + return `{${field.subqueryObjName}}`; + } else if (utils.isObject(field.typeOf)) { + return this.parseTypeOfField(field.typeOf); + } + }) + .filter(field => !utils.isNil(field)); + } + + private parseTypeOfField(typeOfField: TypeOfField): 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`; + return output; + } + + private parseFn(fn: FunctionExp): string { + return `${(fn.text || '').replace(/,/g, ', ')} ${fn.alias || ''}`.trim(); + } + + private parseWhereClause(where: WhereClause): string { + let output = ''; + if (where.left) { + output += + utils.isNumber(where.left.openParen) && where.left.openParen > 0 + ? new Array(where.left.openParen).fill('(').join('') + : ''; + output += `${utils.get(where.left.logicalPrefix, ' ')}`; + output += `${where.left.field} ${where.left.operator} ${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(); + } else { + return output.trim(); + } + } + + private parseGroupByClause(groupBy: GroupByClause): string { + if (groupBy.type) { + return `${groupBy.type}${utils.getAsArrayStr(groupBy.field, true)}`; + } else { + return (Array.isArray(groupBy.field) ? groupBy.field : [groupBy.field]).join(', '); + } + } + + private parseHavingClause(having: HavingClause): string { + let output = ''; + if (having.left) { + output += new Array(having.left.openParen || 0).fill('(').join(''); + output += having.left.fn ? this.parseFn(having.left.fn) : having.left.field; + output += ` ${having.left.operator} ${having.left.value}`; + output += new Array(having.left.closeParen || 0).fill(')').join(''); + } + if (having.right) { + return `${output} ${utils.get(having.operator)} ${this.parseHavingClause(having.right)}`; + } else { + return output.trim(); + } + } + + private parseOrderBy(orderBy: OrderByClause | OrderByClause[]): string { + if (Array.isArray(orderBy)) { + return orderBy.map(ob => this.parseOrderBy(ob)).join(', '); + } else { + let output = `${utils.get(orderBy.field, ' ')}`; + output += orderBy.fn ? this.parseFn(orderBy.fn) : ''; + output += `${utils.get(orderBy.order, ' ')}${utils.get(orderBy.nulls, '', 'NULLS ')}`; + return output.trim(); + } + } +} diff --git a/lib/SoqlListener.ts b/lib/SoqlListener.ts index c935ebf..d684e74 100644 --- a/lib/SoqlListener.ts +++ b/lib/SoqlListener.ts @@ -13,7 +13,7 @@ import { } from './models/SoqlQuery.model'; import { SoqlQueryConfig } from './SoqlParser'; -export type currItem = 'field' | 'from' | 'where' | 'groupby' | 'orderby' | 'having'; +export type currItem = 'field' | 'typeof' | 'from' | 'where' | 'groupby' | 'orderby' | 'having'; export interface Context { isSubQuery: boolean; @@ -609,7 +609,6 @@ export class Listener implements SOQLListener { console.log('enterField_spec:', ctx.text); } this.context.currentItem = 'field'; - let relatedFields: string[]; if (ctx.text.includes('.')) { this.getSoqlQuery().fields.push({ text: ctx.text, relationshipFields: ctx.text.split('.') }); } else { @@ -717,11 +716,21 @@ export class Listener implements SOQLListener { if (this.config.logging) { console.log('enterTypeof_spec:', ctx.text); } + this.context.currentItem = 'typeof'; + this.context.tempData = { + typeOf: { + field: ctx.getChild(1).text, + conditions: [], + }, + }; } exitTypeof_spec(ctx: Parser.Typeof_specContext) { if (this.config.logging) { console.log('exitTypeof_spec:', ctx.text); } + this.getSoqlQuery().fields.push(this.context.tempData); + this.context.tempData = null; + this.context.currentItem = 'field'; } enterTypeof_when_then_clause_list(ctx: Parser.Typeof_when_then_clause_listContext) { if (this.config.logging) { @@ -737,6 +746,10 @@ export class Listener implements SOQLListener { if (this.config.logging) { console.log('enterTypeof_when_then_clause:', ctx.text); } + this.context.tempData.typeOf.conditions.push({ + type: 'WHEN', + objectType: ctx.getChild(1).text, + }); } exitTypeof_when_then_clause(ctx: Parser.Typeof_when_then_clauseContext) { if (this.config.logging) { @@ -747,6 +760,8 @@ export class Listener implements SOQLListener { if (this.config.logging) { console.log('enterTypeof_then_clause:', ctx.text); } + const whenThenClause = this.context.tempData.typeOf.conditions[this.context.tempData.typeOf.conditions.length - 1]; + whenThenClause.fieldList = ctx.getChild(1).text.split(','); } exitTypeof_then_clause(ctx: Parser.Typeof_then_clauseContext) { if (this.config.logging) { @@ -757,6 +772,10 @@ export class Listener implements SOQLListener { if (this.config.logging) { console.log('enterTypeof_else_clause:', ctx.text); } + this.context.tempData.typeOf.conditions.push({ + type: 'ELSE', + fieldList: ctx.getChild(1).text.split(','), + }); } exitTypeof_else_clause(ctx: Parser.Typeof_else_clauseContext) { if (this.config.logging) { @@ -780,8 +799,10 @@ export class Listener implements SOQLListener { this.getSoqlQuery().sObject = ctx.getChild(0).text; if (this.config.includeSubqueryAsField && this.context.isSubQuery) { if (ctx.getChild(0).text.includes('.')) { + this.getSoqlQuery().sObject = ctx.getChild(1).text; + this.getSoqlQuery().sObjectPrefix = ctx.getChild(0).text.replace('.', ''); this.soqlQuery.fields.push({ - subqueryObjName: ctx.text, + subqueryObjName: ctx.getChild(1).text, }); } else { this.soqlQuery.fields.push({ @@ -850,7 +871,6 @@ export class Listener implements SOQLListener { console.log('enterParenthesis:', ctx.text); } if (this.context.currentItem === 'where' || this.context.currentItem === 'having') { - this.context.tempData.nextHasCloseParen = false; this.context.tempData.nextHasOpenParen = true; } } @@ -859,11 +879,9 @@ export class Listener implements SOQLListener { console.log('exitParenthesis:', ctx.text); } if (this.context.currentItem === 'where' || this.context.currentItem === 'having') { - if (this.context.tempData.nextHasCloseParen) { - this.context.tempData.stack.pop(); - } - this.context.tempData.stack[this.context.tempData.stack.length - 1].left.closeParen = true; - this.context.tempData.nextHasCloseParen = true; + const currConditionOperation = this.context.tempData.currConditionOperation.left; + currConditionOperation.closeParen = currConditionOperation.closeParen || 0; + currConditionOperation.closeParen += 1; } } enterSimple_condition(ctx: Parser.Simple_conditionContext) { @@ -888,7 +906,8 @@ export class Listener implements SOQLListener { if (!this.context.tempData.currConditionOperation.left) { this.context.tempData.currConditionOperation.left = currItem; if (this.context.tempData.nextHasOpenParen) { - currItem.openParen = true; + currItem.openParen = currItem.openParen || 0; + currItem.openParen += 1; this.context.tempData.nextHasOpenParen = false; } if (this.context.tempData.nextHasLogicalPrefix) { @@ -907,7 +926,8 @@ export class Listener implements SOQLListener { if (!this.context.tempData.currConditionOperation.left) { this.context.tempData.currConditionOperation.left = currItem; if (this.context.tempData.nextHasOpenParen) { - currItem.openParen = true; + currItem.openParen = currItem.openParen || 0; + currItem.openParen += 1; this.context.tempData.nextHasOpenParen = false; } if (this.context.tempData.nextHasLogicalPrefix) { @@ -942,7 +962,8 @@ export class Listener implements SOQLListener { if (!this.context.tempData.currConditionOperation.left) { this.context.tempData.currConditionOperation.left = currItem; if (this.context.tempData.nextHasOpenParen) { - currItem.openParen = true; + currItem.openParen = currItem.openParen || 0; + currItem.openParen += 1; this.context.tempData.nextHasOpenParen = false; } if (this.context.tempData.nextHasLogicalPrefix) { @@ -975,7 +996,8 @@ export class Listener implements SOQLListener { if (!this.context.tempData.currConditionOperation.left) { this.context.tempData.currConditionOperation.left = currItem; if (this.context.tempData.nextHasOpenParen) { - currItem.openParen = true; + currItem.openParen = currItem.openParen || 0; + currItem.openParen += 1; this.context.tempData.nextHasOpenParen = false; } if (this.context.tempData.nextHasLogicalPrefix) { diff --git a/lib/index.ts b/lib/index.ts index f913d5e..44e8f23 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -3,4 +3,5 @@ * The software in this package is published under the terms of the MIT license, * a copy of which has been included with this distribution in the LICENSE.txt file. */ -export * from './SoqlParser'; +export { parseQuery } from './SoqlParser'; +export { composeQuery } from './SoqlComposer'; diff --git a/lib/models/SoqlQuery.model.ts b/lib/models/SoqlQuery.model.ts index 18041ef..dc50505 100644 --- a/lib/models/SoqlQuery.model.ts +++ b/lib/models/SoqlQuery.model.ts @@ -6,6 +6,7 @@ export interface Query { subqueries: Query[]; sObject: string; sObjectAlias?: string; + sObjectPrefix?: string; whereClause?: WhereClause; limit?: number; offset?: number; @@ -24,17 +25,29 @@ export interface Field { relationshipFields?: string[]; fn?: FunctionExp; subqueryObjName?: string; // populated if subquery + typeOf?: TypeOfField; +} + +export interface TypeOfField { + field: string; + conditions: TypeOfFieldCondition[]; +} + +export interface TypeOfFieldCondition { + type: 'WHEN' | 'ELSE'; + objectType?: string; // not present when ELSE + fieldList: string[]; } export interface WhereClause { - left: Condition | WhereClause; - right?: Condition | WhereClause; + left: Condition; + right?: WhereClause; operator?: LogicalOperator; } export interface Condition { - openParen?: boolean; - closeParen?: boolean; + openParen?: number; + closeParen?: number; logicalPrefix?: 'NOT'; field: string; operator: Operator; @@ -54,12 +67,14 @@ export interface GroupByClause { } export interface HavingClause { - left: HavingCondition | HavingClause; - right?: HavingCondition | HavingClause; + left: HavingCondition; + right?: HavingClause; operator?: LogicalOperator; } export interface HavingCondition { + openParen?: number; + closeParen?: number; field?: string; fn?: FunctionExp; operator: string; diff --git a/lib/utils.ts b/lib/utils.ts index 2e71ec4..d94ff9b 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -1,7 +1,13 @@ +import { isArray } from 'util'; + export function isString(val: any): boolean { return typeof val === 'string'; } +export function isNumber(val: any): boolean { + return Number.isFinite(val); +} + export function isBoolean(val: any): boolean { return typeof val === typeof true; } @@ -13,3 +19,23 @@ export function isObject(val: any): boolean { export function isNil(val: any): boolean { return val === null || val === undefined; } + +export function get(val: string | null | undefined, suffix?: string, prefix?: string): string { + return isNil(val) ? '' : `${prefix || ''}${val}${suffix || ''}`; +} + +export function getIfTrue(val: boolean | null | undefined, returnStr: string): string { + return isBoolean(val) && val ? returnStr : ''; +} + +export function getAsArrayStr(val: string | string[], alwaysParens: boolean = false): string { + if (isArray(val)) { + if (val.length > 0) { + return `(${val.join(', ')})`; + } else { + return alwaysParens ? '()' : ''; + } + } else { + return alwaysParens ? `(${val || ''})` : val || ''; + } +} diff --git a/rollup.config.js b/rollup.config.js index a80e0da..c5463cb 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -9,15 +9,15 @@ export default [ { file: pkg.main, format: 'cjs', - sourcemap: false, + sourcemap: true, }, { file: pkg.module, format: 'es', - sourcemap: false, + sourcemap: true, }, ], external: [...Object.keys(pkg.dependencies || {}), ...Object.keys(pkg.peerDependencies || {})], - plugins: [typescript({}), minify({ comments: false, sourceMap: false })], + plugins: [typescript({})], }, ]; diff --git a/test/SoqlParser.spec.ts b/test/SoqlParser.spec.ts index 328cf5c..fbce0ca 100644 --- a/test/SoqlParser.spec.ts +++ b/test/SoqlParser.spec.ts @@ -1,8 +1,17 @@ -import { parseQuery } from '../lib/SoqlParser'; +import { parseQuery, composeQuery } from '../lib'; import { expect } from 'chai'; import 'mocha'; import testCases from './TestCases'; +const replacements = [ + { matching: / and /i, replace: ' AND ' }, + { matching: / or /i, replace: ' OR ' }, + { matching: / asc /i, replace: ' ASC ' }, + { matching: / desc /i, replace: ' DESC ' }, + { matching: / first /i, replace: ' FIRST ' }, + { matching: / last /i, replace: ' LAST ' }, +]; + describe('parse queries', () => { testCases.forEach(testCase => { it(`should parse correctly - test case ${testCase.testCase} - ${testCase.soql}`, () => { @@ -11,3 +20,14 @@ describe('parse queries', () => { }); }); }); + +describe('compose queries', () => { + testCases.forEach(testCase => { + it(`should compose correctly - test case ${testCase.testCase} - ${testCase.soql}`, () => { + const soqlQuery = composeQuery(parseQuery(testCase.soql)); + let soql = testCase.soql; + replacements.forEach(replacement => (soql = soql.replace(replacement.matching, replacement.replace))); + expect(soqlQuery).equal(soql); + }); + }); +}); diff --git a/test/TestCases.ts b/test/TestCases.ts index 94cf172..fb167f8 100644 --- a/test/TestCases.ts +++ b/test/TestCases.ts @@ -322,7 +322,7 @@ export const testCases: TestCase[] = [ relationshipFields: ['Account', 'Name'], }, { - subqueryObjName: 'Account.Contacts', + subqueryObjName: 'Contacts', }, ], subqueries: [ @@ -334,7 +334,8 @@ export const testCases: TestCase[] = [ }, ], subqueries: [], - sObject: 'Account.', + sObject: 'Contacts', + sObjectPrefix: 'Account', }, ], sObject: 'Account', @@ -444,7 +445,7 @@ export const testCases: TestCase[] = [ }, { testCase: 18, - soql: "SELECT Id, Owner.Name FROM Task WHERE Owner.FirstName like 'B%'", + soql: "SELECT Id, Owner.Name FROM Task WHERE Owner.FirstName LIKE 'B%'", output: { fields: [ { @@ -516,7 +517,29 @@ export const testCases: TestCase[] = [ soql: 'SELECT TYPEOF What WHEN Account THEN Phone, NumberOfEmployees WHEN Opportunity THEN Amount, CloseDate ELSE Name, Email END FROM Event', output: { - fields: [], + fields: [ + { + typeOf: { + field: 'What', + conditions: [ + { + type: 'WHEN', + objectType: 'Account', + fieldList: ['Phone', 'NumberOfEmployees'], + }, + { + type: 'WHEN', + objectType: 'Opportunity', + fieldList: ['Amount', 'CloseDate'], + }, + { + type: 'ELSE', + fieldList: ['Name', 'Email'], + }, + ], + }, + }, + ], subqueries: [], sObject: 'Event', }, @@ -594,7 +617,7 @@ export const testCases: TestCase[] = [ }, { testCase: 24, - soql: 'SELECT UserId, LoginTime from LoginHistory', + soql: 'SELECT UserId, LoginTime FROM LoginHistory', output: { fields: [ { @@ -611,7 +634,7 @@ export const testCases: TestCase[] = [ { testCase: 25, 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', + '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', output: { fields: [ { @@ -867,7 +890,7 @@ export const testCases: TestCase[] = [ sObject: 'Account', whereClause: { left: { - openParen: true, + openParen: 1, field: 'Id', operator: 'IN', value: ["'1'", "'2'", "'3'"], @@ -875,30 +898,28 @@ export const testCases: TestCase[] = [ operator: 'OR', right: { left: { - openParen: true, + openParen: 1, logicalPrefix: 'NOT', field: 'Id', operator: '=', value: "'2'", - closeParen: true, + closeParen: 1, }, operator: 'OR', right: { left: { - openParen: true, + openParen: 1, field: 'Name', operator: 'LIKE', value: "'%FOO%'", - closeParen: true, }, operator: 'OR', right: { left: { - openParen: true, + openParen: 1, field: 'Name', operator: 'LIKE', value: "'%ARM%'", - closeParen: true, }, operator: 'AND', right: { @@ -906,7 +927,7 @@ export const testCases: TestCase[] = [ field: 'FOO', operator: '=', value: "'bar'", - closeParen: true, + closeParen: 3, }, }, }, diff --git a/test/utils.spec.ts b/test/utils.spec.ts index 3a22790..3cf5521 100644 --- a/test/utils.spec.ts +++ b/test/utils.spec.ts @@ -21,6 +21,24 @@ describe('isString', () => { }); }); +describe('isNumber', () => { + it(`correctly determine number`, () => { + expect(utils.isNumber(-1)).equal(true); + expect(utils.isNumber(0)).equal(true); + expect(utils.isNumber(1)).equal(true); + }); + it(`should correctly determine non-number`, () => { + expect(utils.isNumber(null)).equal(false); + expect(utils.isNumber(undefined)).equal(false); + expect(utils.isNumber(true)).equal(false); + expect(utils.isNumber(false)).equal(false); + expect(utils.isNumber([])).equal(false); + expect(utils.isNumber({})).equal(false); + expect(utils.isNumber(Infinity)).equal(false); + expect(utils.isNumber(NaN)).equal(false); + }); +}); + describe('isBoolean', () => { it(`correctly determine boolean`, () => { expect(utils.isBoolean(true)).equal(true); @@ -80,3 +98,44 @@ describe('isNil', () => { expect(utils.isNil(NaN)).equal(false); }); }); + +describe('get', () => { + it(`correctly get value`, () => { + expect(utils.get('value')).equal('value'); + }); + it(`correctly get value with suffix`, () => { + expect(utils.get('value', '.')).equal('value.'); + }); + it(`correctly get value with suffix and prefix`, () => { + expect(utils.get('value', '.', '.')).equal('.value.'); + }); + it(`should correctly return empty string if no value`, () => { + expect(utils.get(null)).equal(''); + expect(utils.get(null, '.')).equal(''); + expect(utils.get(null, '.', '.')).equal(''); + expect(utils.get(undefined)).equal(''); + expect(utils.get(undefined, '.')).equal(''); + expect(utils.get(undefined, '.', '.')).equal(''); + }); +}); + +describe('getIfTrue', () => { + it(`correctly get value`, () => { + expect(utils.getIfTrue(true, 'retVal')).equal('retVal'); + expect(utils.getIfTrue(false, 'retVal')).equal(''); + }); +}); + +describe('getAsArrayStr', () => { + it(`correctly get value from array`, () => { + expect(utils.getAsArrayStr(['a', 'b'])).equal(`(a, b)`); + expect(utils.getAsArrayStr([])).equal(``); + expect(utils.getAsArrayStr([], true)).equal(`()`); + }); + it(`correctly get value from string`, () => { + expect(utils.getAsArrayStr('a')).equal(`a`); + expect(utils.getAsArrayStr(null)).equal(``); + expect(utils.getAsArrayStr('')).equal(``); + expect(utils.getAsArrayStr(null, true)).equal(`()`); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index d67720e..cf08794 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,7 +12,7 @@ "moduleResolution": "node", "noImplicitAny": true, "outDir": "dist", - "sourceMap": false, + "sourceMap": true, "target": "es2017" }, "include": [