├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .npmignore ├── .prettierrc ├── .release-it.json ├── .vscode ├── launch.json └── settings.json ├── AUTHORS.md ├── CHANGELOG.md ├── CONTRIBUTING.md ├── CONTRIBUTORS.md ├── ISSUE_TEMPLATE ├── LICENSE.txt ├── README.md ├── assets └── soql-parser-js-logo.svg ├── bin └── soql-parser-js ├── cli └── index.ts ├── docs ├── .gitignore ├── README.md ├── babel.config.js ├── docs │ ├── api.md │ ├── cli.md │ ├── examples.md │ └── overview.md ├── docusaurus.config.js ├── package-lock.json ├── package.json ├── sidebars.js ├── src │ ├── components │ │ ├── ComposeQueries │ │ │ ├── ParsedOutput │ │ │ │ ├── index.tsx │ │ │ │ └── styles.module.css │ │ │ ├── QueryInput │ │ │ │ ├── index.tsx │ │ │ │ └── styles.module.css │ │ │ └── index.tsx │ │ ├── HomepageFeatures │ │ │ ├── index.tsx │ │ │ └── styles.module.css │ │ ├── ParseQueries │ │ │ ├── ParsedOutput │ │ │ │ ├── index.tsx │ │ │ │ └── styles.module.css │ │ │ ├── SoqlInput │ │ │ │ ├── index.tsx │ │ │ │ └── styles.module.css │ │ │ └── index.tsx │ │ ├── SoqlList │ │ │ ├── index.tsx │ │ │ └── styles.module.css │ │ ├── Tabs │ │ │ ├── index.tsx │ │ │ └── styles.module.css │ │ └── Utilities │ │ │ ├── Highlight.tsx │ │ │ └── index.tsx │ ├── css │ │ └── custom.css │ └── pages │ │ ├── index.module.css │ │ ├── index.tsx │ │ ├── playground.module.css │ │ └── playground.tsx ├── static │ ├── .nojekyll │ ├── CNAME │ ├── img │ │ ├── docusaurus.png │ │ ├── favicon.ico │ │ ├── logo.svg │ │ ├── soql-parser-js-logo.svg │ │ ├── undraw_docusaurus_mountain.svg │ │ ├── undraw_docusaurus_react.svg │ │ └── undraw_docusaurus_tree.svg │ └── sample-queries-json.json └── tsconfig.json ├── package-lock.json ├── package.json ├── src ├── api │ ├── api-models.ts │ └── public-utils.ts ├── composer │ └── composer.ts ├── formatter │ └── formatter.ts ├── index.ts ├── models.ts ├── parser │ ├── lexer.ts │ ├── parser.ts │ └── visitor.ts └── utils.ts ├── tasks └── copy-test-cases-to-docs.ts ├── test ├── performance-test.spec.ts ├── public-utils-test-data.ts ├── public-utils.spec.ts ├── test-cases-compose.ts ├── test-cases-for-format.ts ├── test-cases-for-is-valid.ts ├── test-cases-for-partial-parse.ts ├── test-cases.ts ├── test-partial-query.spec.ts ├── test.spec.ts └── tsconfig.json ├── tsconfig.json └── vitest.config.ts /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Build and test 2 | on: [push] 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | timeout-minutes: 60 7 | steps: 8 | - uses: actions/checkout@v3 9 | name: Checkout [master] 10 | with: 11 | fetch-depth: 0 12 | 13 | - name: Install dependencies 14 | run: npm install 15 | 16 | - name: Build application 17 | run: npm run build 18 | 19 | - name: Run Tests 20 | run: npm run test 21 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Create Release 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | increment: 7 | type: choice 8 | description: Increment 9 | required: true 10 | options: 11 | - major 12 | - minor 13 | - patch 14 | 15 | jobs: 16 | release: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Checkout [main] 20 | uses: actions/checkout@v3 21 | with: 22 | fetch-depth: 0 23 | - name: Init npm cache 24 | uses: actions/setup-node@v3 25 | with: 26 | node-version: '18' 27 | cache: 'yarn' 28 | - name: install dependencies 29 | run: yarn install --frozen-lockfile 30 | - name: git config 31 | run: | 32 | git config --global user.name "Release Workflow" 33 | git config --global user.email "support@getjetstream.app" 34 | - name: Run Release 35 | run: yarn release ${{ github.event.inputs.increment }} --ci 36 | env: 37 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # File created using '.gitignore Generator' for Visual Studio Code: https://bit.ly/vscode-gig 2 | 3 | # Created by https://www.toptal.com/developers/gitignore/api/node,linux,macos,visualstudiocode,webstorm,windows 4 | # Edit at https://www.toptal.com/developers/gitignore?templates=node,linux,macos,visualstudiocode,webstorm,windows 5 | 6 | ### Linux ### 7 | *~ 8 | 9 | # temporary files which can be created if a process still has a handle open of a deleted file 10 | .fuse_hidden* 11 | 12 | # KDE directory preferences 13 | .directory 14 | 15 | # Linux trash folder which might appear on any partition or disk 16 | .Trash-* 17 | 18 | # .nfs files are created when an open file is removed but is still being accessed 19 | .nfs* 20 | 21 | ### macOS ### 22 | # General 23 | .DS_Store 24 | .AppleDouble 25 | .LSOverride 26 | 27 | # Icon must end with two \r 28 | Icon 29 | 30 | 31 | # Thumbnails 32 | ._* 33 | 34 | # Files that might appear in the root of a volume 35 | .DocumentRevisions-V100 36 | .fseventsd 37 | .Spotlight-V100 38 | .TemporaryItems 39 | .Trashes 40 | .VolumeIcon.icns 41 | .com.apple.timemachine.donotpresent 42 | 43 | # Directories potentially created on remote AFP share 44 | .AppleDB 45 | .AppleDesktop 46 | Network Trash Folder 47 | Temporary Items 48 | .apdisk 49 | 50 | ### Node ### 51 | # Logs 52 | logs 53 | *.log 54 | npm-debug.log* 55 | yarn-debug.log* 56 | yarn-error.log* 57 | lerna-debug.log* 58 | 59 | # Diagnostic reports (https://nodejs.org/api/report.html) 60 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 61 | 62 | # Runtime data 63 | pids 64 | *.pid 65 | *.seed 66 | *.pid.lock 67 | 68 | # Directory for instrumented libs generated by jscoverage/JSCover 69 | lib-cov 70 | 71 | # Coverage directory used by tools like istanbul 72 | coverage 73 | *.lcov 74 | 75 | # nyc test coverage 76 | .nyc_output 77 | 78 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 79 | .grunt 80 | 81 | # Bower dependency directory (https://bower.io/) 82 | bower_components 83 | 84 | # node-waf configuration 85 | .lock-wscript 86 | 87 | # Compiled binary addons (https://nodejs.org/api/addons.html) 88 | build/Release 89 | 90 | # Dependency directories 91 | node_modules/ 92 | jspm_packages/ 93 | 94 | # TypeScript v1 declaration files 95 | typings/ 96 | 97 | # TypeScript cache 98 | *.tsbuildinfo 99 | 100 | # Optional npm cache directory 101 | .npm 102 | 103 | # Optional eslint cache 104 | .eslintcache 105 | 106 | # Optional stylelint cache 107 | .stylelintcache 108 | 109 | # Microbundle cache 110 | .rpt2_cache/ 111 | .rts2_cache_cjs/ 112 | .rts2_cache_es/ 113 | .rts2_cache_umd/ 114 | 115 | # Optional REPL history 116 | .node_repl_history 117 | 118 | # Output of 'npm pack' 119 | *.tgz 120 | 121 | # Yarn Integrity file 122 | .yarn-integrity 123 | 124 | # dotenv environment variables file 125 | .env 126 | .env.test 127 | .env*.local 128 | 129 | # parcel-bundler cache (https://parceljs.org/) 130 | .cache 131 | .parcel-cache 132 | 133 | # Next.js build output 134 | .next 135 | 136 | # Nuxt.js build / generate output 137 | .nuxt 138 | dist 139 | 140 | # Storybook build outputs 141 | .out 142 | .storybook-out 143 | storybook-static 144 | 145 | # rollup.js default build output 146 | dist/ 147 | dist_esm/ 148 | dist_cli/ 149 | 150 | # Gatsby files 151 | .cache/ 152 | # Comment in the public line in if your project uses Gatsby and not Next.js 153 | # https://nextjs.org/blog/next-9-1#public-directory-support 154 | # public 155 | 156 | # vuepress build output 157 | .vuepress/dist 158 | 159 | # Serverless directories 160 | .serverless/ 161 | 162 | # FuseBox cache 163 | .fusebox/ 164 | 165 | # DynamoDB Local files 166 | .dynamodb/ 167 | 168 | # TernJS port file 169 | .tern-port 170 | 171 | # Stores VSCode versions used for testing VSCode extensions 172 | .vscode-test 173 | 174 | # Temporary folders 175 | tmp/ 176 | temp/ 177 | 178 | ### VisualStudioCode ### 179 | .vscode/* 180 | !.vscode/settings.json 181 | !.vscode/tasks.json 182 | !.vscode/launch.json 183 | !.vscode/extensions.json 184 | *.code-workspace 185 | 186 | ### VisualStudioCode Patch ### 187 | # Ignore all local history of files 188 | .history 189 | .ionide 190 | 191 | ### WebStorm ### 192 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 193 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 194 | 195 | # User-specific stuff 196 | .idea/**/workspace.xml 197 | .idea/**/tasks.xml 198 | .idea/**/usage.statistics.xml 199 | .idea/**/dictionaries 200 | .idea/**/shelf 201 | 202 | # Generated files 203 | .idea/**/contentModel.xml 204 | 205 | # Sensitive or high-churn files 206 | .idea/**/dataSources/ 207 | .idea/**/dataSources.ids 208 | .idea/**/dataSources.local.xml 209 | .idea/**/sqlDataSources.xml 210 | .idea/**/dynamic.xml 211 | .idea/**/uiDesigner.xml 212 | .idea/**/dbnavigator.xml 213 | 214 | # Gradle 215 | .idea/**/gradle.xml 216 | .idea/**/libraries 217 | 218 | # Gradle and Maven with auto-import 219 | # When using Gradle or Maven with auto-import, you should exclude module files, 220 | # since they will be recreated, and may cause churn. Uncomment if using 221 | # auto-import. 222 | # .idea/artifacts 223 | # .idea/compiler.xml 224 | # .idea/jarRepositories.xml 225 | # .idea/modules.xml 226 | # .idea/*.iml 227 | # .idea/modules 228 | # *.iml 229 | # *.ipr 230 | 231 | # CMake 232 | cmake-build-*/ 233 | 234 | # Mongo Explorer plugin 235 | .idea/**/mongoSettings.xml 236 | 237 | # File-based project format 238 | *.iws 239 | 240 | # IntelliJ 241 | out/ 242 | 243 | # mpeltonen/sbt-idea plugin 244 | .idea_modules/ 245 | 246 | # JIRA plugin 247 | atlassian-ide-plugin.xml 248 | 249 | # Cursive Clojure plugin 250 | .idea/replstate.xml 251 | 252 | # Crashlytics plugin (for Android Studio and IntelliJ) 253 | com_crashlytics_export_strings.xml 254 | crashlytics.properties 255 | crashlytics-build.properties 256 | fabric.properties 257 | 258 | # Editor-based Rest Client 259 | .idea/httpRequests 260 | 261 | # Android studio 3.1+ serialized cache file 262 | .idea/caches/build_file_checksums.ser 263 | 264 | ### WebStorm Patch ### 265 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 266 | 267 | # *.iml 268 | # modules.xml 269 | # .idea/misc.xml 270 | # *.ipr 271 | 272 | # Sonarlint plugin 273 | # https://plugins.jetbrains.com/plugin/7973-sonarlint 274 | .idea/**/sonarlint/ 275 | 276 | # SonarQube Plugin 277 | # https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin 278 | .idea/**/sonarIssues.xml 279 | 280 | # Markdown Navigator plugin 281 | # https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced 282 | .idea/**/markdown-navigator.xml 283 | .idea/**/markdown-navigator-enh.xml 284 | .idea/**/markdown-navigator/ 285 | 286 | # Cache file creation bug 287 | # See https://youtrack.jetbrains.com/issue/JBR-2257 288 | .idea/$CACHE_FILE$ 289 | 290 | # CodeStream plugin 291 | # https://plugins.jetbrains.com/plugin/12206-codestream 292 | .idea/codestream.xml 293 | 294 | ### Windows ### 295 | # Windows thumbnail cache files 296 | Thumbs.db 297 | Thumbs.db:encryptable 298 | ehthumbs.db 299 | ehthumbs_vista.db 300 | 301 | # Dump file 302 | *.stackdump 303 | 304 | # Folder config file 305 | [Dd]esktop.ini 306 | 307 | # Recycle Bin used on file shares 308 | $RECYCLE.BIN/ 309 | 310 | # Windows Installer files 311 | *.cab 312 | *.msi 313 | *.msix 314 | *.msm 315 | *.msp 316 | 317 | # Windows shortcuts 318 | *.lnk 319 | 320 | # End of https://www.toptal.com/developers/gitignore/api/node,linux,macos,visualstudiocode,webstorm,windows 321 | 322 | # Custom rules (everything added below won't be overriden by 'Generate .gitignore File' if you use 'Update' option) 323 | 324 | docs-old -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .env 2 | .git 3 | .prettierrc 4 | .release-it.json 5 | .rpt2_cache 6 | .sfdx/ 7 | .travis.yml 8 | .vscode/ 9 | cli/ 10 | coverage/ 11 | debug/ 12 | docs/ 13 | lib/ 14 | src/ 15 | tasks/ 16 | test/ 17 | AUTHORS.md 18 | CHANGELOG.md 19 | CONTRIBUTING.md 20 | CONTRIBUTORS.md 21 | ISSUE_TEMPLATE 22 | tsconfig.json 23 | vitest.config.ts -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 140, 3 | "semi": true, 4 | "tabWidth": 2, 5 | "useTabs": false, 6 | "singleQuote": true, 7 | "trailingComma": "all", 8 | "bracketSpacing": true, 9 | "arrowParens": "avoid", 10 | "insertPragma": false 11 | } 12 | -------------------------------------------------------------------------------- /.release-it.json: -------------------------------------------------------------------------------- 1 | { 2 | "git": { 3 | "commitMessage": "chore: release v${version}" 4 | }, 5 | "github": { 6 | "release": true 7 | }, 8 | "hooks": { 9 | "before:init": ["npm test"], 10 | "after:bump": "npm run build", 11 | "after:release": "npm run copy-tc-to-docs && cd docs && npm install @jetstreamapp/soql-parser-js@${version} && git add package*.json && git add static/sample-queries-json.json && git commit -m \"Updated docs version\" && git push && npm run deploy" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [] 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "workbench.colorCustomizations": { 3 | "activityBar.activeBackground": "#f87081", 4 | "activityBar.activeBorder": "#79f867", 5 | "activityBar.background": "#f87081", 6 | "activityBar.foreground": "#15202b", 7 | "activityBar.inactiveForeground": "#15202b99", 8 | "activityBarBadge.background": "#79f867", 9 | "activityBarBadge.foreground": "#15202b", 10 | "statusBar.background": "#f54056", 11 | "statusBar.foreground": "#e7e7e7", 12 | "statusBarItem.hoverBackground": "#f87081", 13 | "titleBar.activeBackground": "#f54056", 14 | "titleBar.activeForeground": "#e7e7e7", 15 | "titleBar.inactiveBackground": "#f5405699", 16 | "titleBar.inactiveForeground": "#e7e7e799", 17 | "sash.hoverBorder": "#f87081", 18 | "statusBarItem.remoteBackground": "#f54056", 19 | "statusBarItem.remoteForeground": "#e7e7e7", 20 | "commandCenter.border": "#e7e7e799" 21 | }, 22 | "peacock.color": "#f54056", 23 | "jest.jestCommandLine": "node_modules/jest/bin/jest.js" 24 | } 25 | -------------------------------------------------------------------------------- /AUTHORS.md: -------------------------------------------------------------------------------- 1 | * Austin Turner @paustint -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | Contributions of any kind are welcome and appreciated. 4 | 5 | # What features are allowed 6 | 7 | Any feature that is part of the core project and does not deviate from what the product is will be considered to be accepted. 8 | 9 | # Making Changes 10 | 11 | - Fork the repository 12 | - Create a new branch from master (usually) and with a meaningful name for your changes 13 | - Make your changes 14 | - Commit and push your change 15 | - Please run `prettier` on all modified files prior to opening your pull request 16 | - open a Pull Request for the master branch 17 | 18 | # Re-generating parse after grammar change 19 | 20 | :beetle: There is a bug in the generated ANTLR output that must be corrected by hand in the generated output to make this work. 21 | 22 | - Note: I am not an expert with language parsing/antler and I did not create the grammar, so I was not able to figure this bug out. I believe the problem relies with `antlr4ts-cli` and the way imports are always added to the end of the file 23 | 24 | ## Generate lexer and parser 25 | 26 | - Run `npm run antlr` 27 | - This will remove and re-generate all of the lexer and parser files required to parse SOQL 28 | - :beetle: Manual steps: 29 | - Open `SOQLParser.ts` after the file generation 30 | - Find the class `Keywords_alias_allowedContext` somewhere near line ~44K 31 | - Cut this class and all remaining classes through the end of the file and paste right underneath the `import` statements 32 | - Run all unit tests afterwards `npm run test` - if anything is broken, please troubleshoot or create a PR and ask for assistance 33 | 34 | ## Update code to support new grammar 35 | 36 | - Start with adding a new unit test 37 | - This will ensure the parser and composer support the syntax 38 | - Update the parser to listen for the new grammar (this depends on grammar change that was implemented) 39 | - Assuming there is a new clause somewhere, look in `lib/generated/SOQLListener.ts` to find the new enter/exit methods that were added 40 | - Implement these methods in `lib/SOQLListener.ts` to handle parsing the logic 41 | - Update `lib/SOQLComposer.ts` to handle creating the SOQL query from the parsed query 42 | 43 | ## Unit tests 44 | 45 | - ensure your new unit tests cover all the cases 46 | - Ensure unit tests are all passing, or open a PR and ask for assistance 47 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | - Austin Turner @paustint 2 | - Alfredo Benassi @ABenassi87 3 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE: -------------------------------------------------------------------------------- 1 | - [ ] Feature 2 | - [ ] Bug 3 | 4 | 5 | ## Description 6 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2024 Austin Turner 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), 4 | to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, 5 | and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 10 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 11 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 12 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /assets/soql-parser-js-logo.svg: -------------------------------------------------------------------------------- 1 | soql-parser-js-logo -------------------------------------------------------------------------------- /bin/soql-parser-js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('../dist/cli'); 4 | -------------------------------------------------------------------------------- /cli/index.ts: -------------------------------------------------------------------------------- 1 | import { parseQuery, composeQuery, isQueryValid, formatQuery, FormatOptions } from '../src'; 2 | import { Command } from 'commander'; 3 | import { ParseQueryConfig } from '../src/parser/parser'; 4 | 5 | interface ParseCommandActions { 6 | allowApex: boolean; 7 | allowPartial: boolean; 8 | ignoreErrors: boolean; 9 | } 10 | 11 | interface ComposeCommandOptions { 12 | allowApex: boolean; 13 | allowPartial: boolean; 14 | format: boolean; 15 | indent: number; 16 | lineLength: number; 17 | subqueryParensNewLine: boolean; 18 | keywordsNewLine: boolean; 19 | json: boolean; 20 | } 21 | 22 | type FormatCommandOptions = Omit; 23 | type IsValidCommandOptions = Pick; 24 | 25 | const program = new Command(); 26 | 27 | program 28 | .command('parse ') 29 | .option('-a, --allow-apex', 'allow apex bind variables') 30 | .option('-p, --allow-partial', 'allow partial queries') 31 | .option('-i, --ignore-errors', 'ignore parse errors, return as much of query as possible') 32 | .action((query: string, options: ParseCommandActions) => { 33 | console.log( 34 | JSON.stringify( 35 | parseQuery(query, { 36 | allowApexBindVariables: options.allowApex, 37 | allowPartialQuery: options.allowPartial, 38 | ignoreParseErrors: options.ignoreErrors, 39 | }), 40 | ), 41 | ); 42 | }); 43 | 44 | program 45 | .command('compose ') 46 | .option('-f, --format', 'format output') 47 | .option('-i --indent ', 'number of tab characters to indent', 1) 48 | .option('-m --line-length ', 'max number of characters per lins', 60) 49 | .option('-s --subquery-parens-new-line', 'subquery parens on own line') 50 | .option('-k --keywords-new-line', 'new line after keywords') 51 | .option('-j, --json', 'output as JSON') 52 | .action((query: string, options: ComposeCommandOptions) => { 53 | const formatOptions: FormatOptions = {}; 54 | if (options.indent) { 55 | formatOptions.numIndent = options.indent; 56 | } 57 | 58 | if (options.lineLength) { 59 | formatOptions.fieldMaxLineLength = options.lineLength; 60 | } 61 | 62 | if (options.subqueryParensNewLine) { 63 | formatOptions.fieldSubqueryParensOnOwnLine = options.subqueryParensNewLine; 64 | } 65 | 66 | if (options.keywordsNewLine) { 67 | formatOptions.newLineAfterKeywords = options.keywordsNewLine; 68 | } 69 | 70 | let output = composeQuery(JSON.parse(query), { format: options.format, formatOptions }); 71 | if (options.json) { 72 | console.log(JSON.stringify({ query: output })); 73 | } else { 74 | console.log(output); 75 | } 76 | }); 77 | 78 | program 79 | .command('format ') 80 | .option('-a, --allow-apex', 'allow apex bind variables') 81 | .option('-p, --allow-partial', 'allow partial queries') 82 | .option('-i --indent ', 'number of tab characters to indent', 1) 83 | .option('-m --line-length ', 'max number of characters per lins', 60) 84 | .option('-s --subquery-parens-new-line', 'subquery parens on own line') 85 | .option('-k --keywords-new-line', 'new line after keywords') 86 | .option('-j, --json', 'output as JSON') 87 | .action((query: string, options: FormatCommandOptions) => { 88 | const parseQueryConfig: ParseQueryConfig = {}; 89 | const formatOptions: FormatOptions = {}; 90 | 91 | if (options.allowApex) { 92 | parseQueryConfig.allowApexBindVariables = true; 93 | } 94 | if (options.allowPartial) { 95 | parseQueryConfig.allowPartialQuery = true; 96 | } 97 | 98 | if (options.indent) { 99 | formatOptions.numIndent = options.indent; 100 | } 101 | 102 | if (options.lineLength) { 103 | formatOptions.fieldMaxLineLength = options.lineLength; 104 | } 105 | 106 | if (options.subqueryParensNewLine) { 107 | formatOptions.fieldSubqueryParensOnOwnLine = options.subqueryParensNewLine; 108 | } 109 | 110 | if (options.keywordsNewLine) { 111 | formatOptions.newLineAfterKeywords = options.keywordsNewLine; 112 | } 113 | 114 | const output = formatQuery(query, formatOptions, parseQueryConfig); 115 | if (options.json) { 116 | console.log(JSON.stringify({ query: output })); 117 | } else { 118 | console.log(output); 119 | } 120 | }); 121 | 122 | program 123 | .command('valid ') 124 | .option('-a, --allow-apex', 'allow apex bind variables') 125 | .option('-p, --allow-partial', 'allow partial queries') 126 | .option('-j, --json', 'output as JSON') 127 | .action((query: string, options: IsValidCommandOptions) => { 128 | const parseQueryConfig: ParseQueryConfig = {}; 129 | 130 | if (options.allowApex) { 131 | parseQueryConfig.allowApexBindVariables = true; 132 | } 133 | if (options.allowPartial) { 134 | parseQueryConfig.allowPartialQuery = true; 135 | } 136 | 137 | const isValid = isQueryValid(query, parseQueryConfig); 138 | if (options.json) { 139 | console.log(JSON.stringify({ isValid })); 140 | } else { 141 | if (isValid) { 142 | console.log('true'); 143 | } else { 144 | console.error('false'); 145 | process.exit(1); 146 | } 147 | } 148 | }); 149 | 150 | program.parse(process.argv); 151 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | 11 | # Misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Website 2 | 3 | This website is built using [Docusaurus 2](https://docusaurus.io/), a modern static website generator. 4 | 5 | ### Installation 6 | 7 | ``` 8 | $ yarn 9 | ``` 10 | 11 | ### Local Development 12 | 13 | ``` 14 | $ yarn start 15 | ``` 16 | 17 | This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server. 18 | 19 | ### Build 20 | 21 | ``` 22 | $ yarn build 23 | ``` 24 | 25 | This command generates static content into the `build` directory and can be served using any static contents hosting service. 26 | 27 | ### Deployment 28 | 29 | Using SSH: 30 | 31 | ``` 32 | $ USE_SSH=true yarn deploy 33 | ``` 34 | 35 | Not using SSH: 36 | 37 | ``` 38 | $ GIT_USER= yarn deploy 39 | ``` 40 | 41 | If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. 42 | -------------------------------------------------------------------------------- /docs/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')], 3 | }; 4 | -------------------------------------------------------------------------------- /docs/docs/api.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 3 3 | --- 4 | 5 | # API 6 | 7 | ## Core 8 | 9 | ### ParseQuery 10 | 11 | Parse a SOQL query string into a Query data structure. 12 | 13 | `function parseQuery(soql: string, options?: ParseQueryConfig): Query` 14 | 15 | **ParseQueryConfig** 16 | 17 | - `allowPartialQuery?: boolean;` - If provided, you can provide an incomplete soql query. This is useful if you need to parse WHERE clauses, for example. Subqueries are required to be valid. 18 | - `allowApexBindVariables?: boolean;` - Determines if apex variables are allowed in parsed query. Example: `WHERE Id IN :accountIds`. Only simple Apex is supported. Function calls are not supported. (e.x. `accountMap.keyset()` is not supported) 19 | - `ignoreParseErrors?: boolean;` - If set to true, then queries with partially invalid syntax will still be parsed, but any clauses with invalid parts will be omitted. The SELECT clause and FROM clause must always be valid, but all other clauses can contain invalid parts. 20 | - `logErrors?: boolean;` - If true, parsing and lexing errors will be logged to the console. 21 | 22 | ### ComposeQuery 23 | 24 | `function composeQuery(soql: Query, config?: Partial): string` 25 | 26 | - `format?: boolean` - Apply formatting the the composed query. This will result in a multi-line soql statement. 27 | - `formatOptions?: boolean` - Only applies if `format` is set to true. Options to apply to the formatter. 28 | - `numIndent: number` - Number of tabs to indent on new lines. 29 | - `fieldMaxLineLength: number` - Number of characters before wrapping fields. 30 | - `fieldSubqueryParensOnOwnLine: boolean` - If true and the query includes a subquery, parentheses will be on their own line . 31 | - `whereClauseOperatorsIndented: boolean` - If true, operators (such as `=` or `IN`) in WHERE clauses will be inputted on their own line. 32 | - `newLineAfterKeywords: boolean` - If true, a new line will be inserted after keywords 33 | - `logging: boolean` 34 | - `autoCompose?: boolean` - (superseded by `allowPartialQuery`, you normally don't need to change this setting.) If you need to compose just part of a query, you can create your own instance of the Compose class and set this to false, then call any methods that you need to just for what you would like to turn into a SOQL query. 35 | - `logging?: boolean` - Print out logging statements to the console about the format operation. 36 | 37 | ### IsQueryValid 38 | 39 | Return a boolean if the query was able to be parsed. Same options as `parseQuery`. 40 | 41 | `function isQueryValid(soql: string, options?: ParseQueryConfig): boolean` 42 | 43 | ### FormatQuery 44 | 45 | Format a provided query. This parses the query then composes the query again to apply formatting. 46 | 47 | `function formatQuery(soql: string, formatOptions?: FormatOptions, parseOptions?: ParseQueryConfig): string` 48 | 49 | ## Utility 50 | 51 | Refer to the [playground](/playground) for usage examples. 52 | 53 | #### `getFlattenedFields(value)` 54 | 55 | Turn a query object into a list of fields using dot notation. This is useful if you want to flatten a Salesforce record and display in a table, this function will provide the fields based on the query. 56 | 57 | #### `getField(value)` 58 | 59 | Helper function easily get a a field for a compose function 60 | 61 | `function getField(input: string | ComposeFieldInput): SoqlModels.FieldType` 62 | 63 | ```typescript 64 | const soqlQuery: Query = { 65 | fields: [ 66 | getField('Id'), 67 | getField('Name'), 68 | getField({ 69 | functionName: 'FORMAT', 70 | parameters: 'Amount', 71 | alias: 'MyFormattedAmount', 72 | }), 73 | getField({ subquery: oppLineItemsSubquery }), 74 | ], 75 | sObject: 'Opportunity', 76 | where: { 77 | left: { 78 | field: 'CreatedDate', 79 | operator: '>', 80 | value: 'LAST_N_YEARS:1', 81 | }, 82 | operator: 'AND', 83 | right: { 84 | left: { 85 | field: 'StageName', 86 | operator: '=', 87 | value: 'Closed Won', 88 | // literalType is optional, but if set to STRING and our value is not already wrapped in "'", they will be added 89 | // All other literalType values are ignored when composing a query 90 | literalType: 'STRING', 91 | }, 92 | }, 93 | }, 94 | limit: 150, 95 | }; 96 | ``` 97 | 98 | #### Other utility functions 99 | 100 | The following functions are generally useful for typescript type narrowing. 101 | 102 | These are used internally in the compose function but can be used to aid in processing a query object. 103 | 104 | - `hasAlias(value)` 105 | - `isSubquery(value)` 106 | - `isFieldSubquery(value)` 107 | - `isWhereClauseWithRightCondition(value)` 108 | - `isHavingClauseWithRightCondition(value)` 109 | - `isWhereOrHavingClauseWithRightCondition(value)` 110 | - `isValueCondition(value)` 111 | - `isValueWithDateLiteralCondition(value)` 112 | - `isValueWithDateNLiteralCondition(value)` 113 | - `isValueFunctionCondition(value)` 114 | - `isNegationCondition(value)` 115 | - `isValueQueryCondition(value)` 116 | - `isOrderByField(value)` 117 | - `isOrderByFn(value)` 118 | - `isGroupByField(value)` 119 | - `isGroupByFn(value)` 120 | -------------------------------------------------------------------------------- /docs/docs/cli.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 3 3 | --- 4 | 5 | # CLI 6 | 7 | Install globally or use `npx` to interact with the cli. 8 | 9 | ### Available Commands 10 | 11 | - `soql-parser-js --help` (or using `npx`: `npx soql-parser-js --help`) 12 | - `soql-parser-js parse --help` 13 | - `soql-parser-js compose --help` 14 | - `soql-parser-js format --help` 15 | 16 | ### Examples 17 | 18 | #### Parse 19 | 20 | `npx soql-parser-js parse "SELECT Id FROM Account"` 21 | 22 | ```bash 23 | {"fields":[{"type":"Field","field":"Id"}],"sObject":"Account"} 24 | ``` 25 | 26 | #### Compose 27 | 28 | `npx soql-parser-js compose "{\"fields\":[{\"type\":\"Field\",\"field\":\"Id\"}],\"sObject\":\"Account\"}"` 29 | 30 | ```bash 31 | SELECT Id FROM Account 32 | ``` 33 | 34 | `npx soql-parser-js compose "{\"fields\":[{\"type\":\"Field\",\"field\":\"Id\"}],\"sObject\":\"Account\"}" --json` or -j 35 | 36 | ```json 37 | { "query": "SELECT Id FROM Account" } 38 | ``` 39 | 40 | #### Format 41 | 42 | `npx soql-parser-js format "SELECT Name, COUNT(Id) FROM Account GROUP BY Name HAVING COUNT(Id) > 1"` 43 | 44 | ```bash 45 | SELECT Name, COUNT(Id) 46 | FROM Account 47 | GROUP BY Name 48 | HAVING COUNT(Id) > 1 49 | ``` 50 | 51 | `npx soql-parser-js format "SELECT Name, COUNT(Id) FROM Account GROUP BY Name HAVING COUNT(Id) > 1 -j` 52 | 53 | ```json 54 | { "query": "SELECT Name, COUNT(Id)\nFROM Account\nGROUP BY Name\nHAVING COUNT(Id) > 1" } 55 | ``` 56 | 57 | #### Is Valid 58 | 59 | `npx soql-parser-js valid "SELECT Id FROM Account"` 60 | 61 | ```bash 62 | true 63 | ``` 64 | 65 | `npx soql-parser-js valid "SELECT Id invalid FROM Account"` 66 | 67 | ℹ️ this returns an exit code of 1 68 | 69 | ```bash 70 | false 71 | ``` 72 | 73 | `npx soql-parser-js valid "SELECT Id FROM Account" -j` 74 | 75 | ```json 76 | { "isValid": true } 77 | ``` 78 | 79 | `npx soql-parser-js valid "SELECT Id invalid invalid FROM Account" -j` 80 | 81 | ℹ️ this returns an exit code of 0 82 | 83 | ```json 84 | { "isValid": false } 85 | ``` 86 | 87 | ### List of options 88 | 89 | `soql-parser-js --help` 90 | 91 | ```bash 92 | Usage: soql-parser-js [options] [command] 93 | 94 | Options: 95 | -h, --help output usage information 96 | 97 | Commands: 98 | parse [options] 99 | compose [options] 100 | format [options] 101 | valid 102 | ``` 103 | 104 | `soql-parser-js parse --help` 105 | 106 | ```bash 107 | Usage: parse [options] 108 | 109 | Options: 110 | -a, --allow-apex allow apex bind variables 111 | -p, --allow-partial allow partial queries 112 | -i, --ignore-errors ignore parse errors, return as much of query as possible 113 | -h, --help output usage information 114 | ``` 115 | 116 | `soql-parser-js compose --help` 117 | 118 | ```bash 119 | Usage: compose [options] 120 | 121 | Options: 122 | -f, --format format output 123 | -i --indent number of tab characters to indent (default: 1) 124 | -m --line-length max number of characters per lins (default: 60) 125 | -s --subquery-parens-new-line subquery parens on own line 126 | -k --keywords-new-line new line after keywords 127 | -j, --json output as JSON 128 | -h, --help output usage information 129 | ``` 130 | 131 | `soql-parser-js format --help` 132 | 133 | ```bash 134 | Usage: format [options] 135 | 136 | Options: 137 | -a, --allow-apex allow apex bind variables 138 | -p, --allow-partial allow partial queries 139 | -i --indent number of tab characters to indent (default: 1) 140 | -m --line-length max number of characters per lins (default: 60) 141 | -s --subquery-parens-new-line subquery parens on own line 142 | -k --keywords-new-line new line after keywords 143 | -j, --json output as JSON 144 | -h, --help output usage information 145 | ``` 146 | 147 | `soql-parser-js valid --help` 148 | 149 | ```bash 150 | Usage: valid [options] 151 | 152 | Options: 153 | -a, --allow-apex allow apex bind variables 154 | -p, --allow-partial allow partial queries 155 | -j, --json output as JSON 156 | -h, --help output usage information 157 | ``` 158 | -------------------------------------------------------------------------------- /docs/docs/examples.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 4 3 | --- 4 | 5 | # Other Examples 6 | 7 | ### Parsing Queries 8 | 9 | Parsing a SOQL query can be completed by calling `parseQuery(soqlQueryString)`. A `Query` data structure will be returned. 10 | 11 | ```typescript 12 | import { parseQuery } from '@jetstreamapp/soql-parser-js'; 13 | 14 | const soql = ` 15 | SELECT UserId, COUNT(Id) 16 | FROM LoginHistory 17 | WHERE LoginTime > 2010-09-20T22:16:30.000Z 18 | AND LoginTime < 2010-09-21T22:16:30.000Z 19 | GROUP BY UserId 20 | `; 21 | 22 | console.log(JSON.stringify(soqlQuery, null, 2)); 23 | ``` 24 | 25 |
26 | Results (click to show) 27 | 28 | ```json 29 | { 30 | "fields": [ 31 | { 32 | "type": "Field", 33 | "field": "UserId" 34 | }, 35 | { 36 | "type": "FieldFunctionExpression", 37 | "functionName": "COUNT", 38 | "parameters": ["Id"], 39 | "isAggregateFn": true, 40 | "rawValue": "COUNT(Id)" 41 | } 42 | ], 43 | "sObject": "LoginHistory", 44 | "where": { 45 | "left": { 46 | "field": "LoginTime", 47 | "operator": ">", 48 | "value": "2010-09-20T22:16:30.000Z", 49 | "literalType": "DATETIME" 50 | }, 51 | "operator": "AND", 52 | "right": { 53 | "left": { 54 | "field": "LoginTime", 55 | "operator": "<", 56 | "value": "2010-09-21T22:16:30.000Z", 57 | "literalType": "DATETIME" 58 | } 59 | } 60 | }, 61 | "groupBy": { 62 | "field": "UserId" 63 | } 64 | } 65 | ``` 66 | 67 |
68 | 69 | ### Parsing a partial query 70 | 71 | Added support for `allowPartialQuery` in version `4.4.0` 72 | 73 | ```typescript 74 | import { parseQuery } from '@jetstreamapp/soql-parser-js'; 75 | 76 | const soql = ` 77 | WHERE LoginTime > 2010-09-20T22:16:30.000Z 78 | AND LoginTime < 2010-09-21T22:16:30.000Z 79 | GROUP BY UserId 80 | `; 81 | 82 | const soqlQuery = parseQuery(soql, { allowPartialQuery: true }); 83 | 84 | console.log(JSON.stringify(soqlQuery, null, 2)); 85 | ``` 86 | 87 |
88 | Results (click to show) 89 | 90 | ```json 91 | { 92 | "where": { 93 | "left": { 94 | "field": "LoginTime", 95 | "operator": ">", 96 | "value": "2010-09-20T22:16:30.000Z", 97 | "literalType": "DATETIME" 98 | }, 99 | "operator": "AND", 100 | "right": { 101 | "left": { 102 | "field": "LoginTime", 103 | "operator": "<", 104 | "value": "2010-09-21T22:16:30.000Z", 105 | "literalType": "DATETIME" 106 | } 107 | } 108 | }, 109 | "groupBy": { 110 | "field": "UserId" 111 | } 112 | } 113 | ``` 114 | 115 |
116 | 117 | ### Validating Queries 118 | 119 | ```typescript 120 | import { isQueryValid } from '@jetstreamapp/soql-parser-js'; 121 | 122 | const invalidSoql = `SELECT UserId, COUNT(Id) Account`; 123 | const validSoql = `SELECT UserId, COUNT(Id) Account`; 124 | 125 | console.log(isQueryValid(soql)); 126 | console.log(isQueryValid(soql)); 127 | ``` 128 | 129 | ### Composing Queries 130 | 131 | Build a `Query` data structure to have it converted back into a SOQL query. 132 | 133 | 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, 134 | but depending on your use-case, you may need to build your own data structure to compose a query. 135 | These examples show building your own Query object with the minimum required fields. 136 | 137 | Some utility methods have been provided to make it easier to build the field data structures. 138 | 139 | **Note:** Some operators may be converted to uppercase (e.x. NOT, AND) 140 | 141 | **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. 142 | 143 | ```typescript 144 | import { composeQuery, getField, Query } from '@jetstreamapp/soql-parser-js'; 145 | 146 | // Build a subquery 147 | const oppLineItemsSubquery = { 148 | fields: [ 149 | getField('Quantity'), 150 | getField('ListPrice'), 151 | getField({ 152 | field: 'UnitPrice', 153 | relationships: ['PricebookEntry'], 154 | }), 155 | getField({ 156 | field: 'Name', 157 | relationships: ['PricebookEntry'], 158 | }), 159 | ], 160 | relationshipName: 'OpportunityLineItems', 161 | }; 162 | 163 | // build the main query and add the subquery as a field 164 | const soqlQuery: Query = { 165 | fields: [ 166 | getField('Id'), 167 | getField('Name'), 168 | getField({ 169 | functionName: 'FORMAT', 170 | parameters: 'Amount', 171 | alias: 'MyFormattedAmount', 172 | }), 173 | getField({ subquery: oppLineItemsSubquery }), 174 | ], 175 | sObject: 'Opportunity', 176 | where: { 177 | left: { 178 | field: 'CreatedDate', 179 | operator: '>', 180 | value: 'LAST_N_YEARS:1', 181 | }, 182 | operator: 'AND', 183 | right: { 184 | left: { 185 | field: 'StageName', 186 | operator: '=', 187 | value: 'Closed Won', 188 | // literalType is optional, but if set to STRING and our value is not already wrapped in "'", they will be added 189 | // All other literalType values are ignored when composing a query 190 | literalType: 'STRING', 191 | }, 192 | }, 193 | }, 194 | limit: 150, 195 | }; 196 | 197 | const composedQuery = composeQuery(soqlQuery, { format: true }); 198 | 199 | console.log(composedQuery); 200 | ``` 201 | 202 | **Results** 203 | 204 | ```sql 205 | SELECT Id, Name, FORMAT(Amount) MyFormattedAmount, 206 | ( 207 | SELECT Quantity, ListPrice, PricebookEntry.UnitPrice, 208 | PricebookEntry.Name 209 | FROM OpportunityLineItems 210 | ) 211 | FROM Opportunity 212 | WHERE CreatedDate > LAST_N_YEARS:1 213 | AND StageName = 'Closed Won' 214 | LIMIT 150 215 | ``` 216 | 217 | ### Composing a partial query 218 | 219 | Starting in version `4.4`, compose will not fail if there are missing `SELECT` and `FROM` clauses in your query. 220 | 221 | Partial compose support it supported without any additional steps. 222 | 223 | ```typescript 224 | import { Compose, parseQuery } from '@jetstreamapp/soql-parser-js'; 225 | 226 | const soql = `WHERE Name LIKE 'A%' AND MailingCity = 'California`; 227 | const parsedQuery = parseQuery(soql, { allowPartialQuery: true }); 228 | 229 | // Results of Parsed Query: 230 | /** 231 | { 232 | where: { 233 | left: { field: 'Name', operator: 'LIKE', value: "'A%'", literalType: 'STRING' }, 234 | operator: 'AND', 235 | right: { left: { field: 'MailingCity', operator: '=', value: "'California'", literalType: 'STRING' } }, 236 | }, 237 | } 238 | */ 239 | 240 | const composedQuery = composeQuery(soqlQuery, { format: true }); 241 | 242 | console.log(composedQuery); 243 | ``` 244 | 245 | **Results** 246 | 247 | ```sql 248 | WHERE Name LIKE 'A%' AND MailingCity = 'California 249 | ``` 250 | 251 |
252 | See the alternate way to compose partial queries by calling the Compose class directly 253 | 254 | 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. 255 | 256 | For example, if you just need the `WHERE` clause from a query as a string, you can do the following: 257 | 258 | ```typescript 259 | import { Compose, parseQuery } from '@jetstreamapp/soql-parser-js'; 260 | 261 | const soql = `SELECT Id FROM Account WHERE Name = 'Foo'`; 262 | const parsedQuery = parseQuery(soql); 263 | 264 | // Results of Parsed Query: 265 | // const parsedQuery = { 266 | // fields: [ 267 | // { 268 | // type: 'Field', 269 | // field: 'Id', 270 | // }, 271 | // ], 272 | // sObject: 'Account', 273 | // where: { 274 | // left: { 275 | // field: 'Name', 276 | // operator: '=', 277 | // value: "'Foo'", 278 | // literalType: 'STRING', 279 | // }, 280 | // }, 281 | // }; 282 | 283 | // Create a new instance of the compose class and set the autoCompose to false to avoid composing the entire query 284 | const composer = new Compose(parsedQuery, { autoCompose: false }); 285 | 286 | 287 | const whereClause = composer.parseWhereOrHavingClause(parsedQuery.where); 288 | 289 | console.log(whereClause); 290 | } 291 | ``` 292 | 293 | #### Available methods on the `Compose` class 294 | 295 | These are used internally, but are public and available for use. 296 | 297 | ```typescript 298 | parseQuery(query: Query | Subquery): string; 299 | parseFields(fields: FieldType[]): { text: string; typeOfClause?: string[] }[]; 300 | parseTypeOfField(typeOfField: FieldTypeOf): string[]; 301 | parseWhereOrHavingClause(whereOrHaving: WhereClause | HavingClause, tabOffset = 0, priorConditionIsNegation = false): string; 302 | parseGroupByClause(groupBy: GroupByClause | GroupByClause[]): string; 303 | parseOrderBy(orderBy: OrderByClause | OrderByClause[]): string; 304 | parseWithDataCategory(withDataCategory: WithDataCategoryClause): string; 305 | ``` 306 | 307 |
308 | 309 | ## Format Query 310 | 311 | This function is provided as a convenience and just calls parse and compose. 312 | [Check out the demo](https://paustint.github.io/soql-parser-js/) to see the outcome of the various format options. 313 | 314 | ```typescript 315 | import { formatQuery } from '@jetstreamapp/soql-parser-js'; 316 | 317 | const query = `SELECT Id, Name, AccountNumber, AccountSource, AnnualRevenue, BillingAddress, BillingCity, BillingCountry, BillingGeocodeAccuracy, ShippingStreet, Sic, SicDesc, Site, SystemModstamp, TickerSymbol, Type, Website, (SELECT Id, Name, AccountId, Amount, CampaignId, CloseDate, CreatedById, Type FROM Opportunities), (SELECT Id, Name, AccountNumber, AccountSource, AnnualRevenue, BillingAddress, Website FROM ChildAccounts) FROM Account WHERE Name LIKE 'a%' OR Name LIKE 'b%' OR Name LIKE 'c%'`; 318 | 319 | const formattedQuery1 = formatQuery(query); 320 | const formattedQuery2 = formatQuery(query, { 321 | fieldMaxLineLength: 20, 322 | fieldSubqueryParensOnOwnLine: false, 323 | whereClauseOperatorsIndented: true, 324 | }); 325 | const formattedQuery3 = formatQuery(query, { fieldSubqueryParensOnOwnLine: true, whereClauseOperatorsIndented: true }); 326 | ``` 327 | 328 | ```sql 329 | -- formattedQuery1 330 | SELECT Id, Name, AccountNumber, AccountSource, AnnualRevenue, 331 | BillingAddress, BillingCity, BillingCountry, BillingGeocodeAccuracy, 332 | ShippingStreet, Sic, SicDesc, Site, SystemModstamp, TickerSymbol, Type, 333 | Website, 334 | ( 335 | SELECT Id, Name, AccountId, Amount, CampaignId, CloseDate, 336 | CreatedById, Type 337 | FROM Opportunities 338 | ), 339 | ( 340 | SELECT Id, Name, AccountNumber, AccountSource, AnnualRevenue, 341 | BillingAddress, Website 342 | FROM ChildAccounts 343 | ) 344 | FROM Account 345 | WHERE Name LIKE 'a%' 346 | OR Name LIKE 'b%' 347 | OR Name LIKE 'c%' 348 | 349 | -- formattedQuery2 350 | SELECT Id, Name, 351 | AccountNumber, AccountSource, 352 | AnnualRevenue, BillingAddress, 353 | BillingCity, BillingCountry, 354 | BillingGeocodeAccuracy, ShippingStreet, 355 | Sic, SicDesc, Site, 356 | SystemModstamp, TickerSymbol, Type, 357 | Website, 358 | (SELECT Id, Name, 359 | AccountId, Amount, CampaignId, 360 | CloseDate, CreatedById, Type 361 | FROM Opportunities), 362 | (SELECT Id, Name, 363 | AccountNumber, AccountSource, 364 | AnnualRevenue, BillingAddress, 365 | Website 366 | FROM ChildAccounts) 367 | FROM Account 368 | WHERE Name LIKE 'a%' 369 | OR Name LIKE 'b%' 370 | OR Name LIKE 'c%' 371 | 372 | 373 | -- formattedQuery3 374 | SELECT Id, Name, AccountNumber, AccountSource, AnnualRevenue, 375 | BillingAddress, BillingCity, BillingCountry, BillingGeocodeAccuracy, 376 | ShippingStreet, Sic, SicDesc, Site, SystemModstamp, TickerSymbol, Type, 377 | Website, 378 | ( 379 | SELECT Id, Name, AccountId, Amount, CampaignId, CloseDate, 380 | CreatedById, Type 381 | FROM Opportunities 382 | ), 383 | ( 384 | SELECT Id, Name, AccountNumber, AccountSource, AnnualRevenue, 385 | BillingAddress, Website 386 | FROM ChildAccounts 387 | ) 388 | FROM Account 389 | WHERE Name LIKE 'a%' 390 | OR Name LIKE 'b%' 391 | OR Name LIKE 'c%' 392 | ``` 393 | -------------------------------------------------------------------------------- /docs/docusaurus.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Note: type annotations allow type checking and IDEs autocompletion 3 | 4 | const { themes } = require('prism-react-renderer'); 5 | 6 | const lightCodeTheme = themes.github; 7 | const darkCodeTheme = themes.shadesOfPurple; 8 | 9 | /** @type {import('@docusaurus/types').Config} */ 10 | const config = { 11 | title: 'SOQL Parser JS', 12 | tagline: 'Parse and generate SOQL queries', 13 | url: 'https://soql-parser-js.getjetstream.app', 14 | baseUrl: '/', 15 | onBrokenLinks: 'throw', 16 | onBrokenMarkdownLinks: 'warn', 17 | favicon: 'img/favicon.ico', 18 | 19 | // GitHub pages deployment config. 20 | // If you aren't using GitHub pages, you don't need these. 21 | organizationName: 'jetstreamapp', 22 | projectName: 'soql-parser-js', 23 | deploymentBranch: 'gh-pages', 24 | trailingSlash: false, 25 | 26 | // Even if you don't use internalization, you can use this field to set useful 27 | // metadata like html lang. For example, if your site is Chinese, you may want 28 | // to replace "en" with "zh-Hans". 29 | i18n: { 30 | defaultLocale: 'en', 31 | locales: ['en'], 32 | }, 33 | 34 | presets: [ 35 | [ 36 | 'classic', 37 | /** @type {import('@docusaurus/preset-classic').Options} */ 38 | ({ 39 | docs: { 40 | sidebarPath: require.resolve('./sidebars.js'), 41 | // Please change this to your repo. 42 | // Remove this to remove the "edit this page" links. 43 | editUrl: 'https://github.com/jetstreamapp/soql-parser-js/docs/docs/', 44 | }, 45 | theme: { 46 | customCss: require.resolve('./src/css/custom.css'), 47 | }, 48 | }), 49 | ], 50 | ], 51 | 52 | themeConfig: 53 | /** @type {import('@docusaurus/preset-classic').ThemeConfig} */ 54 | ({ 55 | colorMode: { 56 | defaultMode: 'dark', 57 | respectPrefersColorScheme: true, 58 | }, 59 | navbar: { 60 | title: '', 61 | logo: { 62 | alt: 'Logo', 63 | src: 'img/soql-parser-js-logo.svg', 64 | }, 65 | items: [ 66 | { 67 | type: 'doc', 68 | docId: 'overview', 69 | position: 'left', 70 | label: 'Documentation', 71 | }, 72 | { to: '/playground', label: 'Playground', position: 'left' }, 73 | { 74 | href: 'https://github.com/jetstreamapp/soql-parser-js', 75 | label: 'GitHub', 76 | position: 'right', 77 | }, 78 | ], 79 | }, 80 | footer: { 81 | style: 'dark', 82 | links: [ 83 | { 84 | title: 'Links', 85 | items: [ 86 | { 87 | label: 'Documentation', 88 | to: '/docs/overview', 89 | }, 90 | { 91 | label: 'GitHub', 92 | href: 'https://github.com/jetstreamapp/soql-parser-js', 93 | }, 94 | ], 95 | }, 96 | ], 97 | }, 98 | markdown: { 99 | mdx1Compat: { 100 | comments: false, 101 | admonitions: false, 102 | headingIds: false, 103 | }, 104 | }, 105 | prism: { 106 | theme: lightCodeTheme, 107 | darkTheme: darkCodeTheme, 108 | }, 109 | additionalLanguages: ['javascript', 'json', 'sql'], 110 | }), 111 | }; 112 | 113 | module.exports = config; 114 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "docusaurus": "docusaurus", 7 | "start": "docusaurus start", 8 | "build": "docusaurus build", 9 | "swizzle": "docusaurus swizzle", 10 | "deploy": "docusaurus deploy", 11 | "clear": "docusaurus clear", 12 | "serve": "docusaurus serve", 13 | "write-translations": "docusaurus write-translations", 14 | "write-heading-ids": "docusaurus write-heading-ids", 15 | "typecheck": "tsc" 16 | }, 17 | "dependencies": { 18 | "@docusaurus/core": "3.4.0", 19 | "@docusaurus/preset-classic": "3.4.0", 20 | "@jetstreamapp/soql-parser-js": "^6.2.0", 21 | "@mdx-js/react": "^3.0.1", 22 | "clsx": "^2.1.1", 23 | "prism-react-renderer": "^2.3.1", 24 | "react": "^18.3.1", 25 | "react-dom": "^18.3.1" 26 | }, 27 | "devDependencies": { 28 | "@docusaurus/module-type-aliases": "3.4.0", 29 | "@docusaurus/tsconfig": "3.4.0", 30 | "@docusaurus/types": "3.0.0", 31 | "@types/react": "^18.3.3", 32 | "typescript": "~5.4.5" 33 | }, 34 | "browserslist": { 35 | "production": [ 36 | ">0.5%", 37 | "not dead", 38 | "not op_mini all" 39 | ], 40 | "development": [ 41 | "last 1 chrome version", 42 | "last 1 firefox version", 43 | "last 1 safari version" 44 | ] 45 | }, 46 | "engines": { 47 | "node": ">=18.0" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /docs/sidebars.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Creating a sidebar enables you to: 3 | - create an ordered group of docs 4 | - render a sidebar for each doc of that group 5 | - provide next/previous navigation 6 | 7 | The sidebars can be generated from the filesystem, or explicitly defined here. 8 | 9 | Create as many sidebars as you want. 10 | */ 11 | 12 | // @ts-check 13 | 14 | /** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */ 15 | const sidebars = { 16 | // By default, Docusaurus generates a sidebar from the docs folder structure 17 | tutorialSidebar: [{ type: 'autogenerated', dirName: '.' }], 18 | }; 19 | 20 | module.exports = sidebars; 21 | -------------------------------------------------------------------------------- /docs/src/components/ComposeQueries/ParsedOutput/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Highlight } from '../../Utilities/Highlight'; 3 | 4 | export interface ParsedOutputProps { 5 | query: string; 6 | } 7 | 8 | export default function ParsedOutput({ query }: ParsedOutputProps): JSX.Element { 9 | return ( 10 |
11 | {query && ( 12 | <> 13 |
Output
14 | 15 | 16 | )} 17 |
18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /docs/src/components/ComposeQueries/ParsedOutput/styles.module.css: -------------------------------------------------------------------------------- 1 | textarea { 2 | width: 100%; 3 | height: 150px; 4 | padding: 12px 20px; 5 | box-sizing: border-box; 6 | border: 2px solid #ccc; 7 | border-radius: 4px; 8 | background-color: #f8f8f8; 9 | font-size: 16px; 10 | resize: vertical; 11 | } 12 | -------------------------------------------------------------------------------- /docs/src/components/ComposeQueries/QueryInput/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import clsx from 'clsx'; 3 | import styles from './styles.module.css'; 4 | 5 | export interface QueryInputProps { 6 | queryObj: string; 7 | onChange: (value: string) => void; 8 | } 9 | 10 | export default function QueryInput({ queryObj, onChange }: QueryInputProps): JSX.Element { 11 | return ( 12 |
13 |