├── .eslintignore ├── .eslintrc ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── help-wanted.md ├── pull_request_template.md └── workflows │ ├── nodejs.yml │ └── release.yml ├── .gitignore ├── .npmignore ├── .npmrc ├── .prettierignore ├── .prettierrc.json ├── .vscode ├── launch.json └── settings.json ├── .whitesource ├── CHANGELOG.md ├── LICENSE ├── README.md ├── jest.config.js ├── odata-v4-aggregation.abnf ├── odata-v4.abnf ├── package-lock.json ├── package.json ├── src ├── builder │ ├── batch.ts │ ├── filter.ts │ ├── index.ts │ ├── param.ts │ └── types.ts ├── constants │ ├── index.ts │ └── method.ts ├── expressions.ts ├── index.ts ├── json.ts ├── lexer.ts ├── nameOrIdentifier.ts ├── odataUri.ts ├── parser.ts ├── primitiveLiteral.ts ├── query.ts ├── resourcePath.ts ├── token.ts ├── types.ts ├── utils.ts └── visitor.ts ├── test ├── __snapshots__ │ └── filter.spec.ts.snap ├── builder │ ├── filter.test.ts │ └── params.test.ts ├── cases.ts ├── filter.spec.ts ├── issue.spec.ts ├── json.spec.ts ├── odata-abnf-testcases.xml ├── odata-uri.spec.ts ├── parser.spec.ts ├── primitive-cases.ts ├── primitiveLiteral.spec.ts ├── query.spec.ts ├── resource-path.spec.ts ├── resources │ └── school.edmx.json └── visitor.spec.ts └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | *.js 2 | node_modules 3 | lib 4 | coverage -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": [ 5 | "@typescript-eslint" 6 | ], 7 | "extends": [ 8 | "plugin:@typescript-eslint/eslint-recommended", 9 | "prettier" 10 | ], 11 | "env": { 12 | "es6": true, 13 | "node": true, 14 | "jest": true, 15 | "browser": true 16 | }, 17 | "parserOptions": { 18 | "ecmaVersion": 2018, 19 | "sourceType": "module", 20 | "ecmaFeatures": { 21 | "impliedStrict": true, 22 | "objectLiteralDuplicateProperties": false 23 | } 24 | }, 25 | "rules": { 26 | "array-bracket-spacing": [ 27 | "error", 28 | "never" 29 | ], 30 | "camelcase": [ 31 | "error", 32 | { 33 | "properties": "never" 34 | } 35 | ], 36 | "comma-dangle": [ 37 | "error", 38 | "never" 39 | ], 40 | "curly": [ 41 | "error", 42 | "all" 43 | ], 44 | "eol-last": [ 45 | "error" 46 | ], 47 | "indent": [ 48 | "error", 49 | 2, 50 | { 51 | "SwitchCase": 1 52 | } 53 | ], 54 | "no-tabs": "error", 55 | "keyword-spacing": [ 56 | "error" 57 | ], 58 | "no-else-return": [ 59 | "error" 60 | ], 61 | "no-mixed-spaces-and-tabs": [ 62 | "error" 63 | ], 64 | "no-multiple-empty-lines": [ 65 | "error" 66 | ], 67 | "no-spaced-func": [ 68 | "error" 69 | ], 70 | "no-trailing-spaces": [ 71 | "error" 72 | ], 73 | "no-undef": [ 74 | "error" 75 | ], 76 | "no-unexpected-multiline": [ 77 | "error" 78 | ], 79 | "quotes": [ 80 | "error", 81 | "single", 82 | { 83 | "allowTemplateLiterals": true, 84 | "avoidEscape": true 85 | } 86 | ], 87 | "max-len": [ 88 | 0 89 | ], 90 | "semi": [ 91 | "error" 92 | ], 93 | "space-before-blocks": [ 94 | "error", 95 | "always" 96 | ], 97 | "space-before-function-paren": [ 98 | "error", 99 | "never" 100 | ], 101 | "space-in-parens": [ 102 | "error", 103 | "never" 104 | ], 105 | "space-unary-ops": [ 106 | "error", 107 | { 108 | "nonwords": false, 109 | "overrides": {} 110 | } 111 | ], 112 | // "valid-jsdoc": ["error"] 113 | // ECMAScript 6 rules 114 | "arrow-body-style": [ 115 | "error", 116 | "as-needed", 117 | { 118 | "requireReturnForObjectLiteral": false 119 | } 120 | ], 121 | "arrow-parens": [ 122 | "error", 123 | "always" 124 | ], 125 | "arrow-spacing": [ 126 | "error", 127 | { 128 | "after": true, 129 | "before": true 130 | } 131 | ], 132 | "no-class-assign": [ 133 | "error" 134 | ], 135 | "no-const-assign": [ 136 | "error" 137 | ], 138 | "no-duplicate-imports": [ 139 | "error" 140 | ], 141 | "no-new-symbol": [ 142 | "error" 143 | ], 144 | "no-useless-rename": [ 145 | "error" 146 | ], 147 | "no-var": [ 148 | "error" 149 | ], 150 | "object-shorthand": [ 151 | "error", 152 | "always", 153 | { 154 | "avoidQuotes": true, 155 | "ignoreConstructors": false 156 | } 157 | ], 158 | "prefer-arrow-callback": [ 159 | "error", 160 | { 161 | "allowNamedFunctions": false, 162 | "allowUnboundThis": true 163 | } 164 | ], 165 | "prefer-const": [ 166 | "error" 167 | ], 168 | "prefer-rest-params": [ 169 | "error" 170 | ], 171 | "prefer-template": [ 172 | "error" 173 | ], 174 | "template-curly-spacing": [ 175 | "error", 176 | "never" 177 | ] 178 | } 179 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: Soontao 7 | 8 | --- 9 | 10 | ## Describe the bug 11 | 12 | A clear and concise description of what the bug is. 13 | 14 | ## System Environment 15 | 16 | * OS: 17 | * Node Version: 18 | * This Library Version: 19 | 20 | ## To Reproduce 21 | 22 | Steps to reproduce the behavior: 23 | 1. Go to '...' 24 | 2. Click on '....' 25 | 3. Scroll down to '....' 26 | 4. See error 27 | 28 | **Provide an example repo is better** 29 | 30 | ## Expected behavior 31 | A clear and concise description of what you expected to happen. 32 | 33 | ## Additional context 34 | Add any other context about the problem here. 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[FEATURE REQUEST]" 5 | labels: documentation, enhancement 6 | assignees: Soontao 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/help-wanted.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Help Wanted 3 | about: Help Wanted 4 | title: '[Question]' 5 | labels: help wanted 6 | assignees: Soontao 7 | 8 | --- 9 | 10 | ## System Environment 11 | 12 | * OS: 13 | * NodeJS Version: 14 | * This LIbrary Version: 15 | 16 | ## Question Description 17 | 18 | > add some question description here 19 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Describe your changes 2 | 3 | - xxx 4 | 5 | ## Issue ticket number and link (if applicable) 6 | 7 | [blabla]() 8 | 9 | ## Checklist before requesting a review 10 | 11 | - [ ] Project license confirmed 12 | - [ ] Unit-test added & passed at local development environment 13 | - [ ] All CI check passed 14 | - [ ] Design Document (if applicable) 15 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - "*" 7 | pull_request: 8 | branches: 9 | - "*" 10 | schedule: 11 | - cron: "15 3 * * *" 12 | 13 | jobs: 14 | fast: 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Use Node.js 20 | uses: actions/setup-node@v3 21 | with: 22 | node-version: 18.x 23 | 24 | - name: Cache node modules 25 | uses: actions/cache@v3 26 | env: 27 | cache-name: cache-node-modules 28 | with: 29 | path: ~/.npm 30 | key: ${{ runner.os }}-build-quick-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} 31 | 32 | - run: npm ci 33 | - run: npm run lint 34 | - run: npm run build --if-present 35 | - run: npm run coverage 36 | 37 | - name: Codecov 38 | uses: codecov/codecov-action@v1 39 | with: 40 | token: ${{ secrets.CODECOV_TOKEN }} 41 | 42 | build-all-platform: 43 | runs-on: ${{ matrix.os }} 44 | 45 | strategy: 46 | matrix: 47 | node-version: [16.x, 18.x] 48 | os: [ubuntu-latest] 49 | 50 | steps: 51 | - uses: actions/checkout@v3 52 | - name: Use Node.js ${{ matrix.node-version }} 53 | uses: actions/setup-node@v3 54 | with: 55 | node-version: ${{ matrix.node-version }} 56 | 57 | - name: Cache node modules 58 | uses: actions/cache@v3 59 | env: 60 | cache-name: cache-node-modules 61 | with: 62 | path: ~/.npm 63 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ matrix.node-version }}-${{ hashFiles('**/package-lock.json') }} 64 | 65 | - run: npm ci 66 | - run: npm run lint 67 | - run: npm run build --if-present 68 | - run: npm run test 69 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Publish NPM Module 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*" 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | - uses: actions/setup-node@v3 14 | with: 15 | node-version: 18.x 16 | registry-url: https://registry.npmjs.org/ 17 | - run: npm ci 18 | - run: npm run test 19 | - run: npm publish --access public 20 | env: 21 | NODE_AUTH_TOKEN: ${{secrets.Z_NPM_TOKEN}} 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.log* 3 | doc/*.html 4 | node_modules 5 | *.code-workspace 6 | *.sublime-project 7 | *.sublime-workspace 8 | lib 9 | coverage 10 | dist 11 | docs 12 | es5 13 | es6 14 | node 15 | report 16 | .antlr 17 | *.tgz -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | coverage 2 | docs 3 | report 4 | *.log* 5 | test 6 | .vscode 7 | .github 8 | jest.config.js 9 | *.abnf 10 | src 11 | *.tgz -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | audit = false 2 | omit-lockfile-registry-resolved = true 3 | fund = false 4 | lockfile-version = 2 -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | coverage 2 | .vscode 3 | node_modules 4 | report 5 | lib 6 | test -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "node", 6 | "name": "vscode-jest-tests", 7 | "request": "launch", 8 | "args": [ 9 | "--runInBand", 10 | "--watchAll=false" 11 | ], 12 | "cwd": "${workspaceFolder}", 13 | "console": "integratedTerminal", 14 | "internalConsoleOptions": "neverOpen", 15 | "disableOptimisticBPs": true, 16 | "program": "${workspaceFolder}/node_modules/.bin/jest" 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.fixAll": true, 4 | "source.organizeImports": true 5 | }, 6 | "jest.autoEnable": false, 7 | "cSpell.words": [ 8 | "Theo", 9 | "allpages", 10 | "datetime", 11 | "datetimeoffset", 12 | "inlinecount", 13 | "odata" 14 | ] 15 | } -------------------------------------------------------------------------------- /.whitesource: -------------------------------------------------------------------------------- 1 | { 2 | "scanSettings": { 3 | "baseBranches": [] 4 | }, 5 | "checkRunSettings": { 6 | "vulnerableCheckRunConclusionLevel": "failure", 7 | "displayMode": "diff" 8 | }, 9 | "issueSettings": { 10 | "minSeverityLevel": "LOW", 11 | "issueType": "DEPENDENCY" 12 | } 13 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ### [0.2.14](https://github.com/Soontao/odata-v4-parser/compare/v0.2.13...v0.2.14) (2023-03-23) 6 | 7 | ### [0.2.13](https://github.com/Soontao/odata-v4-parser/compare/v0.2.12...v0.2.13) (2023-03-23) 8 | 9 | 10 | ### Bug Fixes 11 | 12 | * double quote in filter ([6f5ec49](https://github.com/Soontao/odata-v4-parser/commit/6f5ec4906c1fefc398e462c703e858405bdd6146)) 13 | * general values in string literal ([653db44](https://github.com/Soontao/odata-v4-parser/commit/653db445cdee572277d5aa1734d14d8042c60c0c)) 14 | 15 | ### [0.2.12](https://github.com/Soontao/odata-v4-parser/compare/v0.2.11...v0.2.12) (2022-11-15) 16 | 17 | 18 | ### Bug Fixes 19 | 20 | * point ([723e095](https://github.com/Soontao/odata-v4-parser/commit/723e0955cbabee81cd4d36abecf9bd75a0a0660e)) 21 | 22 | ### [0.2.11](https://github.com/Soontao/odata-v4-parser/compare/v0.2.10...v0.2.11) (2022-04-05) 23 | 24 | 25 | ### Features 26 | 27 | * with type ([6aaa2f6](https://github.com/Soontao/odata-v4-parser/commit/6aaa2f61dc0f410c2f5341b0f88f63db727580fd)) 28 | 29 | 30 | ### Bug Fixes 31 | 32 | * **ci:** pipeline ([9a3fe11](https://github.com/Soontao/odata-v4-parser/commit/9a3fe11f2f1a22efbc8e0e645ec9fe7307e2bad1)) 33 | 34 | ### [0.2.10](https://github.com/Soontao/odata-v4-parser/compare/v0.2.9...v0.2.10) (2021-08-16) 35 | 36 | 37 | ### Features 38 | 39 | * support odata v2 ([91c694d](https://github.com/Soontao/odata-v4-parser/commit/91c694d4a48fbeba3a099b58477b7406ddcb2a4d)) 40 | 41 | ### [0.2.9](https://github.com/Soontao/odata-v4-parser/compare/v0.2.8...v0.2.9) (2021-07-07) 42 | 43 | ### [0.2.8](https://github.com/Soontao/odata-v4-parser/compare/v0.2.7...v0.2.8) (2021-07-02) 44 | 45 | ### [0.2.7](https://github.com/Soontao/odata-v4-parser/compare/v0.2.6...v0.2.7) (2021-06-23) 46 | 47 | ### [0.2.6](https://github.com/Soontao/odata-v4-parser/compare/v0.2.5...v0.2.6) (2021-06-20) 48 | 49 | 50 | ### Features 51 | 52 | * support filter with type ([81d6cc7](https://github.com/Soontao/odata-v4-parser/commit/81d6cc72c297c2be561da662e66c9381a9c62f8e)) 53 | 54 | 55 | ### Bug Fixes 56 | 57 | * primitive value issue ([97b432c](https://github.com/Soontao/odata-v4-parser/commit/97b432cb77be305ef822161951d9f1f46fdfc1cc)) 58 | * type issue ([b3fb06c](https://github.com/Soontao/odata-v4-parser/commit/b3fb06cf59928f0a3c51ee7aeb1de5ddd68487d4)) 59 | 60 | ### [0.2.5](https://github.com/Soontao/odata-v4-parser/compare/v0.2.4...v0.2.5) (2021-04-19) 61 | 62 | 63 | ### Bug Fixes 64 | 65 | * **ci:** issue ([90c616a](https://github.com/Soontao/odata-v4-parser/commit/90c616a6b4872ad97700b487cd2fdd9b8fa7547d)) 66 | 67 | ### [0.2.4](https://github.com/Soontao/odata-v4-parser/compare/v0.2.3...v0.2.4) (2021-03-24) 68 | 69 | 70 | ### Features 71 | 72 | * support uppper case 'POLYGON' ([752c6e1](https://github.com/Soontao/odata-v4-parser/commit/752c6e1463c21b19b45dec1aacd94b3af525fcbd)) 73 | 74 | ### [0.2.3](https://github.com/Soontao/odata-v4-parser/compare/v0.2.2...v0.2.3) (2020-09-23) 75 | 76 | ### [0.2.2](https://github.com/Soontao/odata-v4-parser/compare/v0.2.1...v0.2.2) (2020-09-23) 77 | 78 | 79 | ### Features 80 | 81 | * add odata method constants ([aaf8ce6](https://github.com/Soontao/odata-v4-parser/commit/aaf8ce6d61ede52226f2593050bb02cacf27fc7c)) 82 | 83 | ### [0.2.1](https://github.com/Soontao/odata-v4-parser/compare/v0.1.46...v0.2.1) (2020-08-26) 84 | 85 | ### [0.1.46](https://github.com/Soontao/odata-v4-parser/compare/v0.1.45...v0.1.46) (2020-08-26) 86 | 87 | 88 | ### Bug Fixes 89 | 90 | * build script ([f5600cf](https://github.com/Soontao/odata-v4-parser/commit/f5600cf418c832572e6b6cf5181cffd35908b7c5)) 91 | 92 | ### [0.1.45](https://github.com/Soontao/odata-v4-parser/compare/v0.1.44...v0.1.45) (2020-08-26) 93 | 94 | 95 | ### Bug Fixes 96 | 97 | * update ([4d3cc04](https://github.com/Soontao/odata-v4-parser/commit/4d3cc040d7d5ffd67aff0d964483099a41cddc6f)) 98 | 99 | ### [0.1.44](https://github.com/Soontao/odata-v4-parser/compare/v0.1.43...v0.1.44) (2020-08-24) 100 | 101 | ### [0.1.43](https://github.com/Soontao/odata-v4-parser/compare/v0.1.42...v0.1.43) (2020-08-10) 102 | 103 | 104 | ### Bug Fixes 105 | 106 | * batch type ([9b2d981](https://github.com/Soontao/odata-v4-parser/commit/9b2d9819ab9b8e56ec7fe7aa2b7d85f85154c5ec)) 107 | 108 | ### [0.1.42](https://github.com/Soontao/odata-v4-parser/compare/v0.1.41...v0.1.42) (2020-08-10) 109 | 110 | 111 | ### Features 112 | 113 | * test for $batch ([517bb6e](https://github.com/Soontao/odata-v4-parser/commit/517bb6ebab4d5bdaba825c6c1d685643a6a00183)) 114 | 115 | ### [0.1.41](https://github.com/Soontao/odata-v4-parser/compare/v0.1.40...v0.1.41) (2020-08-09) 116 | 117 | 118 | ### Features 119 | 120 | * type for batch request for odata 4.01 ([88e5bf2](https://github.com/Soontao/odata-v4-parser/commit/88e5bf25a084fb389879088f468727befe6c8e70)) 121 | 122 | ### [0.1.40](https://github.com/Soontao/odata-v4-parser/compare/v0.1.39...v0.1.40) (2020-08-01) 123 | 124 | ### [0.1.39](https://github.com/Soontao/odata-v4-parser/compare/v0.1.38...v0.1.39) (2020-07-30) 125 | 126 | 127 | ### Features 128 | 129 | * support filter & param ([83f62c7](https://github.com/Soontao/odata-v4-parser/commit/83f62c78c73433af6c4575dd00aa01d57dd4dc43)) 130 | 131 | ### [0.1.38](https://github.com/Soontao/odata-v4-parser/compare/v0.1.37...v0.1.38) (2020-07-10) 132 | 133 | 134 | ### Bug Fixes 135 | 136 | * visit undefine object will throw error ([8c37045](https://github.com/Soontao/odata-v4-parser/commit/8c37045f3734950fad052369b77e83d0571e240e)) 137 | 138 | ### [0.1.37](https://github.com/Soontao/odata-v4-parser/compare/v0.1.36...v0.1.37) (2020-07-10) 139 | 140 | 141 | ### Features 142 | 143 | * **visitor:** support parent ([2824438](https://github.com/Soontao/odata-v4-parser/commit/2824438824b26d5efd13dfc82964f6d66d6155fd)) 144 | 145 | ### [0.1.36](https://github.com/Soontao/odata-v4-parser/compare/v0.1.35...v0.1.36) (2020-07-09) 146 | 147 | 148 | ### Features 149 | 150 | * **visitor:** deep first traverse ([8bd49bf](https://github.com/Soontao/odata-v4-parser/commit/8bd49bfa37ec419419c6336ae36bb216abdca79d)) 151 | 152 | ### [0.1.35](https://github.com/Soontao/odata-v4-parser/compare/v0.1.34...v0.1.35) (2020-07-09) 153 | 154 | 155 | ### Features 156 | 157 | * **visitor:** add ast visitor ([0ff94c3](https://github.com/Soontao/odata-v4-parser/commit/0ff94c3be04d25b898f09aa4f2abf73ac49f8c2c)) 158 | 159 | ### [0.1.34](https://github.com/Soontao/odata-v4-parser/compare/v0.1.33...v0.1.34) (2020-07-09) 160 | 161 | ### [0.1.33](https://github.com/Soontao/odata-v4-parser/compare/v0.1.32...v0.1.33) (2020-07-09) 162 | 163 | ### [0.1.32](https://github.com/Soontao/odata-v4-parser/compare/v0.1.31...v0.1.32) (2020-07-06) 164 | 165 | ### 0.1.31 (2020-07-06) 166 | 167 | 168 | ### Features 169 | 170 | * [#1](https://github.com/Soontao/odata-v4-parser/issues/1), find node by name ([ae40496](https://github.com/Soontao/odata-v4-parser/commit/ae40496c9acc40abccc9115848819a011ac1a6b3)) 171 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020 Theo Sun 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OData(V4) URI Parser 2 | 3 | [![npm (scoped)](https://img.shields.io/npm/v/@odata/parser)](https://www.npmjs.com/package/@odata/parser) 4 | [![Node CI](https://github.com/Soontao/odata-v4-parser/actions/workflows/nodejs.yml/badge.svg)](https://github.com/Soontao/odata-v4-parser/actions/workflows/nodejs.yml) 5 | [![Codecov](https://codecov.io/gh/Soontao/odata-v4-parser/branch/master/graph/badge.svg)](https://codecov.io/gh/Soontao/odata-v4-parser) 6 | 7 | 8 | OData v4 parser based on OASIS Standard OData v4 ABNF grammar 9 | 10 | ## Usage - URI Parser 11 | 12 | ```ts 13 | import { defaultParser } from "@odata/parser"; 14 | const ast = defaultParser.odataUri("/Categories(10)?$expand=A,C&$select=D,E") 15 | // process it 16 | ``` 17 | 18 | 19 | 20 | ## Usage - OData QueryParam/Filter Builder 21 | 22 | ```ts 23 | import { param, filter } from "@odata/parser"; 24 | param().top(1).filter(filter({ A: 1 })) 25 | // => $top=1&$filter=A eq 1 26 | ``` 27 | 28 | ### filter with type 29 | 30 | ```ts 31 | import { filter, literalValues } from "@odata/parser"; 32 | 33 | expect(filter({ A: 1 }).build()) 34 | .toBe("A eq 1") 35 | expect(filter({ A: literalValues.String(1) }).build()) 36 | .toBe("A eq '1'") 37 | expect(filter({ A: literalValues.Guid("253f842d-d739-41b8-ac8c-139ac7a9dd14") }).build()) 38 | .toBe("A eq 253f842d-d739-41b8-ac8c-139ac7a9dd14") 39 | 40 | ``` 41 | 42 | 43 | ## [CHANGELOG](./CHANGELOG.md) 44 | 45 | ## [LICENSE](./LICENSE) 46 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'transform': { 3 | '.(ts|tsx)': 'ts-jest' 4 | }, 5 | 'testTimeout': 30 * 1000, 6 | 'collectCoverageFrom': [ 7 | 'src/**/*', 8 | '!**/node_modules/**' 9 | ], 10 | 'coveragePathIgnorePatterns': [ 11 | 'node_modules/' 12 | ], 13 | 'testPathIgnorePatterns': [ 14 | '/node_modules/', 15 | '/lib/' 16 | ], 17 | 'modulePathIgnorePatterns': [ 18 | '/lib' 19 | ], 20 | "testEnvironment": "node" 21 | }; 22 | -------------------------------------------------------------------------------- /odata-v4-aggregation.abnf: -------------------------------------------------------------------------------- 1 | ;------------------------------------------------------------------------------ 2 | ; odata-aggregation-abnf 3 | ;------------------------------------------------------------------------------ 4 | ; 5 | ; OData Extension for Data Aggregation Version 4.0 6 | ; Committee Specification 01 7 | ; 26 February 2014 8 | ; Copyright (c) OASIS Open 2014. All Rights Reserved. 9 | ; Source: http://docs.oasis-open.org/odata/odata-data-aggregation-ext/v4.0/cs01/abnf/ 10 | ; 11 | ; Technical Committee: 12 | ; OASIS Open Data Protocol (OData) TC 13 | ; https://www.oasis-open.org/committees/odata 14 | ; 15 | ; Chairs: 16 | ; - Barbara Hartel (barbara.hartel@sap.com), SAP AG 17 | ; - Ram Jeyaraman (Ram.Jeyaraman@microsoft.com), Microsoft 18 | ; 19 | ; Editors: 20 | ; - Ralf Handl (ralf.handl@sap.com), SAP AG 21 | ; - Hubert Heijkers (hubert.heijkers@nl.ibm.com), IBM 22 | ; - Gerald Krause (gerald.krause@sap.com), SAP AG 23 | ; - Michael Pizzo (mikep@microsoft.com), Microsoft 24 | ; - Martin Zurmuehl (martin.zurmuehl@sap.com), SAP AG 25 | ; 26 | ; Additional artifacts: 27 | ; This grammar is one component of a Work Product which consists of: 28 | ; - OData Extension for Data Aggregation Version 4.0 29 | ; - OData Aggregation ABNF Construction Rules Version 4.0 30 | ; - OData Aggregation ABNF Test Cases 31 | ; - OData Aggregation Vocabulary 32 | ; 33 | ; Related work: 34 | ; This specification is related to: 35 | ; - OData Version 4.0 Part 1: Protocol 36 | ; - OData Version 4.0 Part 2: URL Conventions 37 | ; - OData Version 4.0 Part 3: CSDL 38 | ; - OData ABNF Construction Rules Version 4.0 39 | ; - OData Core Vocabulary 40 | ; - OData Measures Vocabulary 41 | ; - OData JSON Format Version 4.0 42 | ; This specification replaces or supersedes: 43 | ; - None 44 | ; 45 | ; Declared XML namespaces: 46 | ; - None 47 | ; 48 | ; Abstract: 49 | ; This specification adds basic grouping and aggregation functionality (e.g. 50 | ; sum, min, and max) to the Open Data Protocol (OData) without changing any 51 | ; of the base principles of OData. 52 | ; 53 | ; Overview: 54 | ; This grammar uses the ABNF defined in RFC5234 with one extension: literals 55 | ; enclosed in single quotes (e.g. '$metadata') are treated case-sensitive. 56 | ; 57 | ; It extends the OData ABNF Construction Rules Version 4.0 58 | ; 59 | ; Contents: 60 | ; 1. New alternatives for OData ABNF Construction Rules 61 | ; 2. System Query Option $apply 62 | ; 3. Extensions to $filter 63 | ; 64 | ;------------------------------------------------------------------------------ 65 | 66 | ;------------------------------------------------------------------------------ 67 | ; 1. New alternatives for OData ABNF Construction Rules 68 | ;------------------------------------------------------------------------------ 69 | 70 | systemQueryOption =/ apply 71 | 72 | boolMethodCallExpr =/ isdefinedExpr 73 | 74 | primitiveProperty =/ aggregateAlias / customAggregate 75 | 76 | 77 | ;------------------------------------------------------------------------------ 78 | ; 2. System Query Option $apply 79 | ;------------------------------------------------------------------------------ 80 | 81 | apply = '$apply' EQ applyExpr 82 | applyExpr = applyTrafo *( "/" applyTrafo ) 83 | applyTrafo = aggregateTrafo 84 | / bottomcountTrafo 85 | / bottompercentTrafo 86 | / bottomsumTrafo 87 | / concatTrafo 88 | / expandTrafo 89 | / filterTrafo 90 | / groupbyTrafo 91 | / identityTrafo 92 | / searchTrafo 93 | / topcountTrafo 94 | / toppercentTrafo 95 | / topsumTrafo 96 | / customFunction 97 | 98 | aggregateTrafo = 'aggregate' OPEN BWS aggregateExpr *( BWS COMMA BWS aggregateExpr ) BWS CLOSE 99 | aggregateExpr = customAggregate [ aggregateAs aggregateFrom ] 100 | / commonExpr aggregateWith aggregateAs [ aggregateFrom ] 101 | / pathPrefix '$count' aggregateAs 102 | / pathPrefix customAggregate 103 | / pathPrefix pathSegment OPEN aggregateExpr CLOSE 104 | aggregateAs = RWS 'as' RWS aggregateAlias 105 | aggregateWith = RWS 'with' RWS aggregateMethod 106 | aggregateFrom = RWS 'from' RWS groupingProperty [ aggregateWith ] [ aggregateFrom ] 107 | aggregateMethod = 'sum' 108 | / 'min' 109 | / 'max' 110 | / 'average' 111 | / 'countdistinct' 112 | / namespace "." odataIdentifier 113 | 114 | customAggregate = odataIdentifier 115 | aggregateAlias = odataIdentifier 116 | 117 | groupingProperty = pathPrefix 118 | ( entityNavigationProperty [ "/" qualifiedEntityTypeName ] 119 | / primitiveProperty 120 | / complexProperty 121 | ) 122 | pathPrefix = [ qualifiedEntityTypeName "/" ] *( pathSegment "/" ) 123 | pathSegment = ( complexProperty / complexColProperty ) [ "/" qualifiedComplexTypeName ] 124 | / navigationProperty [ "/" qualifiedEntityTypeName ] 125 | 126 | bottomcountTrafo = 'bottomcount' OPEN BWS commonExpr BWS COMMA BWS commonExpr BWS CLOSE 127 | bottompercentTrafo = 'bottompercent' OPEN BWS commonExpr BWS COMMA BWS commonExpr BWS CLOSE 128 | bottomsumTrafo = 'bottomsum' OPEN BWS commonExpr BWS COMMA BWS commonExpr BWS CLOSE 129 | 130 | concatTrafo = 'concat' OPEN BWS applyExpr 1*( BWS COMMA BWS applyExpr ) BWS CLOSE 131 | expandTrafo = 'expand' OPEN BWS expandPath [ BWS COMMA BWS filterTrafo ] *( BWS COMMA BWS expandTrafo ) BWS CLOSE 132 | filterTrafo = 'filter' OPEN BWS boolCommonExpr BWS CLOSE 133 | searchTrafo = 'search' OPEN BWS searchExpr BWS CLOSE 134 | 135 | groupbyTrafo = 'groupby' OPEN BWS groupbyList *( BWS COMMA BWS applyExpr) BWS CLOSE 136 | groupbyList = OPEN BWS groupbyElement *( BWS COMMA BWS groupbyElement ) BWS CLOSE 137 | groupbyElement = groupingProperty / rollupSpec 138 | rollupSpec = 'rollup' OPEN BWS 139 | ( '$all' / groupingProperty ) 140 | 1*( BWS COMMA BWS groupingProperty ) 141 | BWS CLOSE 142 | 143 | identityTrafo = 'identity' 144 | 145 | topcountTrafo = 'topcount' OPEN BWS commonExpr BWS COMMA BWS commonExpr BWS CLOSE 146 | toppercentTrafo = 'toppercent' OPEN BWS commonExpr BWS COMMA BWS commonExpr BWS CLOSE 147 | topsumTrafo = 'topsum' OPEN BWS commonExpr BWS COMMA BWS commonExpr BWS CLOSE 148 | 149 | customFunction = namespace "." entityColFunction functionExprParameters 150 | 151 | 152 | ;------------------------------------------------------------------------------ 153 | ; 3. Extensions to $filter 154 | ;------------------------------------------------------------------------------ 155 | 156 | isdefinedExpr = 'isdefined' OPEN BWS ( firstMemberExpr ) BWS CLOSE 157 | 158 | 159 | ;------------------------------------------------------------------------------ 160 | ; End of odata-aggregation-abnf 161 | ;------------------------------------------------------------------------------ -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@odata/parser", 3 | "version": "0.2.14", 4 | "description": "OData(V4) Parser", 5 | "main": "lib/index", 6 | "typings": "lib/index.d.ts", 7 | "engines": { 8 | "node": ">=14", 9 | "npm": ">=6" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/Soontao/odata-v4-parser" 14 | }, 15 | "contributors": [ 16 | { 17 | "name": "Theo Sun", 18 | "email": "theo.sun@outlook.com", 19 | "url": "https://github.com/Soontao" 20 | }, 21 | { 22 | "name": "JayStack Enterprises", 23 | "url": "http://jaystack.com" 24 | } 25 | ], 26 | "license": "MIT", 27 | "scripts": { 28 | "prebuild": "npm run lint", 29 | "build": "tsc", 30 | "lint": "eslint src/** --fix", 31 | "coverage": "npm test -- --coverage --no-cache", 32 | "typedoc": "typedoc --name \"OData v4 Filter Parser\" --excludeExternals --excludeNotExported --hideGenerator --excludeExternals --excludePrivate --out docs src", 33 | "test": "jest", 34 | "prepublishOnly": "npm run test && npm run build", 35 | "release": "npx standard-version --no-verify && git push --follow-tags origin main" 36 | }, 37 | "devDependencies": { 38 | "@types/jest": "^29.5.0", 39 | "@types/node": "^14.18.40", 40 | "@typescript-eslint/eslint-plugin": "^5.56.0", 41 | "@typescript-eslint/parser": "^5.56.0", 42 | "eslint": "^7.32.0", 43 | "eslint-config-prettier": "^8.8.0", 44 | "jest": "^29.5.0", 45 | "prettier": "2.3.2", 46 | "ts-jest": "^29.0.5", 47 | "typescript": "^5.0.2" 48 | }, 49 | "dependencies": { 50 | "@newdash/newdash": "^5.21.4", 51 | "@odata/metadata": "^0.2.8" 52 | }, 53 | "keywords": [ 54 | "odata", 55 | "odata(v4)", 56 | "uri parser" 57 | ] 58 | } -------------------------------------------------------------------------------- /src/builder/batch.ts: -------------------------------------------------------------------------------- 1 | // https://docs.oasis-open.org/odata/odata-json-format/v4.01/odata-json-format-v4.01.html#_Toc38457781 2 | // support json format for batch request 3 | 4 | import { ODataMethod } from '../constants'; 5 | 6 | export interface JsonBatchRequestBundle { 7 | requests: JsonBatchRequest[]; 8 | } 9 | 10 | export type JsonBatchMethod = ODataMethod; 11 | 12 | export type JsonBatchHeaders = Record; 13 | 14 | export interface JsonBatchRequest { 15 | id: string; 16 | method: JsonBatchMethod; 17 | url: string; 18 | atomicityGroup?: string; 19 | dependsOn?: string[]; 20 | headers?: JsonBatchHeaders; 21 | body?: T; 22 | } 23 | 24 | export interface JsonBatchResponseBundle { 25 | responses: JsonBatchResponse[]; 26 | } 27 | 28 | export interface JsonBatchResponse { 29 | id: string; 30 | status: number; 31 | body?: any; 32 | headers?: JsonBatchHeaders; 33 | } 34 | -------------------------------------------------------------------------------- /src/builder/filter.ts: -------------------------------------------------------------------------------- 1 | import join from '@newdash/newdash/join'; 2 | import { Edm } from '@odata/metadata'; 3 | import { convertPrimitiveValueToString, ODataVersion } from './types'; 4 | 5 | export enum ExprOperator { 6 | eq = 'eq', 7 | ne = 'ne', 8 | gt = 'gt', 9 | lt = 'lt', 10 | ge = 'ge', 11 | le = 'le' 12 | } 13 | 14 | type FieldExpr = { 15 | op: ExprOperator; 16 | value: any; 17 | }; 18 | 19 | type FieldExprMappings = { 20 | [key: string]: FieldExpr[]; 21 | }; 22 | 23 | /** 24 | * @private 25 | * @internal 26 | */ 27 | class ODataFieldExpr { 28 | constructor( 29 | filter: ODataFilter, 30 | fieldName: string, 31 | mapping: FieldExprMappings 32 | ) { 33 | this._exprMappings = mapping; 34 | this._fieldName = fieldName; 35 | this._filter = filter; 36 | // initialize 37 | if (this._getFieldExprs() == undefined) { 38 | this._exprMappings[this._fieldName] = []; 39 | } 40 | } 41 | 42 | private _filter: ODataFilter; 43 | 44 | private _fieldName: string; 45 | 46 | private _exprMappings: FieldExprMappings; 47 | 48 | private _getFieldExprs() { 49 | return this._exprMappings[this._fieldName]; 50 | } 51 | 52 | private _addExpr(op: ExprOperator, value: any) { 53 | if (value === null) { 54 | this._getFieldExprs().push({ op, value: 'null' }); 55 | return; 56 | } 57 | 58 | switch (typeof value) { 59 | case 'number': 60 | case 'boolean': 61 | case 'string': 62 | case 'object': 63 | this._getFieldExprs().push({ op, value }); 64 | break; 65 | case 'undefined': 66 | throw new Error( 67 | `You must set value in odata filter eq/ne/gt/ge/ne/nt ...` 68 | ); 69 | default: 70 | throw new Error( 71 | `Not support typeof ${typeof value}: ${value} in odata filter eq/ne/gt/ge/ne/nt ...` 72 | ); 73 | } 74 | } 75 | 76 | /** 77 | * equal 78 | * @param value 79 | */ 80 | eq(value: number | string | Edm.PrimitiveTypeValue): ODataFilter { 81 | this._addExpr(ExprOperator.eq, value); 82 | return this._filter; 83 | } 84 | 85 | /** 86 | * not equal 87 | * @param value 88 | */ 89 | ne(value: number | string | Edm.PrimitiveTypeValue): ODataFilter { 90 | this._addExpr(ExprOperator.ne, value); 91 | return this._filter; 92 | } 93 | 94 | eqString(value: string): ODataFilter { 95 | this._addExpr(ExprOperator.eq, `'${value}'`); 96 | return this._filter; 97 | } 98 | 99 | neString(value: string): ODataFilter { 100 | this._addExpr(ExprOperator.ne, `'${value}'`); 101 | return this._filter; 102 | } 103 | 104 | /** 105 | * greater or equal 106 | * @param value 107 | */ 108 | ge(value: number | string | Edm.PrimitiveTypeValue): ODataFilter { 109 | this._addExpr(ExprOperator.ge, value); 110 | return this._filter; 111 | } 112 | 113 | /** 114 | * greater than 115 | * @param value 116 | */ 117 | gt(value: number | string | Edm.PrimitiveTypeValue): ODataFilter { 118 | this._addExpr(ExprOperator.gt, value); 119 | return this._filter; 120 | } 121 | 122 | /** 123 | * less or equal 124 | * @param value 125 | */ 126 | le(value: number | string | Edm.PrimitiveTypeValue): ODataFilter { 127 | this._addExpr(ExprOperator.le, value); 128 | return this._filter; 129 | } 130 | 131 | /** 132 | * less than 133 | * @param value 134 | */ 135 | lt(value: number | string | Edm.PrimitiveTypeValue): ODataFilter { 136 | this._addExpr(ExprOperator.lt, value); 137 | return this._filter; 138 | } 139 | 140 | /** 141 | * match any value in an array 142 | * 143 | * @param values 144 | */ 145 | in( 146 | values: Array = [] 147 | ): ODataFilter { 148 | if (values.length > 0) { 149 | values.forEach((value) => { 150 | this.eq(value); 151 | }); 152 | } 153 | return this._filter; 154 | } 155 | 156 | /** 157 | * filter by value range 158 | * 159 | * @param low 160 | * @param max 161 | * @param includeBoundary 162 | */ 163 | between(low: any, max: any, includeBoundary = true): ODataFilter { 164 | if (low == undefined || max == undefined) { 165 | throw new Error('You must give out the start and end value'); 166 | } 167 | if (includeBoundary) { 168 | this.ge(low); 169 | this.le(max); 170 | } else { 171 | this.gt(low); 172 | this.lt(max); 173 | } 174 | return this._filter; 175 | } 176 | } 177 | 178 | /** 179 | * OData filter builder 180 | */ 181 | export class ODataFilter { 182 | static New(obj?: Partial): ODataFilter { 183 | return new ODataFilter(obj as E); 184 | } 185 | 186 | constructor(obj?: T) { 187 | if (obj != undefined && typeof obj == 'object') { 188 | Object.entries(obj).forEach(([prop, value]) => { 189 | this.field(prop as any).eq(value); 190 | }); 191 | } 192 | } 193 | 194 | private _fieldExprMappings: FieldExprMappings = {}; 195 | 196 | /** 197 | * getExprMapping 198 | * 199 | * @internal 200 | * @private 201 | */ 202 | private getExprMapping(): FieldExprMappings { 203 | return this._fieldExprMappings; 204 | } 205 | 206 | /** 207 | * @param name filed name 208 | */ 209 | field(name: keyof T): ODataFieldExpr { 210 | return new ODataFieldExpr(this, name as string, this.getExprMapping()); 211 | } 212 | 213 | public toString(version: ODataVersion = 'v4'): string { 214 | return this.build(version); 215 | } 216 | 217 | private _buildExprLit(value: any, version: ODataVersion = 'v4') { 218 | if (value === null) { 219 | return 'null'; 220 | } 221 | 222 | switch (typeof value) { 223 | case 'number': 224 | case 'boolean': 225 | return `${value}`; 226 | case 'string': 227 | if (value.startsWith("'") || value.startsWith('datetime')) { 228 | return value; 229 | } 230 | return `'${value}'`; 231 | case 'object': 232 | if (value instanceof Edm.PrimitiveTypeValue) { 233 | return convertPrimitiveValueToString(value, version); 234 | } 235 | throw new Error( 236 | `Not support object ${ 237 | value?.constructor?.name || typeof value 238 | } in odata filter eq/ne/gt/ge/ne/nt ...` 239 | ); 240 | 241 | case 'undefined': 242 | throw new Error( 243 | `You must set value in odata filter eq/ne/gt/ge/ne/nt ...` 244 | ); 245 | default: 246 | throw new Error( 247 | `Not support typeof ${typeof value}: ${value} in odata filter eq/ne/gt/ge/ne/nt ...` 248 | ); 249 | } 250 | } 251 | 252 | protected _buildFieldExprString( 253 | field: string, 254 | version: ODataVersion = 'v4' 255 | ): string { 256 | const exprs = this.getExprMapping()[field]; 257 | if (exprs.length > 0) { 258 | if (exprs.filter((expr) => expr.op == ExprOperator.eq).length == 0) { 259 | return `(${join( 260 | exprs.map( 261 | ({ op, value }) => 262 | `${field} ${op} ${this._buildExprLit(value, version)}` 263 | ), 264 | ' and ' 265 | )})`; 266 | } 267 | return `(${join( 268 | exprs.map( 269 | ({ op, value }) => 270 | `${field} ${op} ${this._buildExprLit(value, version)}` 271 | ), 272 | ' or ' 273 | )})`; 274 | } 275 | return ''; 276 | } 277 | 278 | public build(version: ODataVersion = 'v4'): string { 279 | let _rt = ''; 280 | _rt = join( 281 | // join all fields exprs string 282 | Object.entries(this.getExprMapping()).map(([fieldName, exprs]) => { 283 | switch (exprs.length) { 284 | // if one field expr mapping array is empty 285 | case 0: 286 | return ''; 287 | // only have one expr 288 | case 1: 289 | const { op, value } = exprs[0]; 290 | return `${fieldName} ${op} ${this._buildExprLit(value, version)}`; 291 | default: 292 | // multi exprs 293 | return this._buildFieldExprString(fieldName, version); 294 | } 295 | }), 296 | ' and ' 297 | ); 298 | return _rt; 299 | } 300 | } 301 | -------------------------------------------------------------------------------- /src/builder/index.ts: -------------------------------------------------------------------------------- 1 | import { Edm } from '@odata/metadata'; 2 | import { ODataFilter } from './filter'; 3 | import { ODataParam } from './param'; 4 | 5 | export * from './batch'; 6 | export * from './filter'; 7 | export * from './param'; 8 | export * from './types'; 9 | 10 | export function param() { 11 | return ODataParam.New(); 12 | } 13 | 14 | export function filter(obj?: Record) { 15 | return ODataFilter.New(obj); 16 | } 17 | 18 | /** 19 | * edm primitive literal value creators 20 | */ 21 | export const literalValues = { 22 | Binary: (value: Buffer | string) => Edm.Binary.createValue(value), 23 | Boolean: (value: Boolean) => Edm.Boolean.createValue(value), 24 | Byte: (value: number) => Edm.Byte.createValue(value), 25 | Date: (value: Date | string) => Edm.Date.createValue(value), 26 | DateTimeOffset: (value: Date | String) => 27 | Edm.DateTimeOffset.createValue(value), 28 | Decimal: (value: number) => Edm.Decimal.createValue(value), 29 | Double: (value: number) => Edm.Double.createValue(value), 30 | Duration: (value: string) => Edm.Duration.createValue(value), 31 | Guid: (value: string) => Edm.Guid.createValue(value), 32 | Int16: (value: number) => Edm.Int16.createValue(value), 33 | Int32: (value: number) => Edm.Int32.createValue(value), 34 | Int64: (value: number) => Edm.Int64.createValue(value), 35 | SByte: (value: number) => Edm.SByte.createValue(value), 36 | Single: (value: number) => Edm.Single.createValue(value), 37 | Stream: (value: any) => Edm.Stream.createValue(value), 38 | String: (value: string) => Edm.String.createValue(value), 39 | TimeOfDay: (value: any) => Edm.TimeOfDay.createValue(value), 40 | Geography: (value: any) => Edm.Geography.createValue(value), 41 | GeographyPoint: (value: any) => Edm.GeographyPoint.createValue(value), 42 | GeographyLineString: (value: any) => 43 | Edm.GeographyLineString.createValue(value), 44 | GeographyPolygon: (value: any) => Edm.GeographyPolygon.createValue(value), 45 | GeographyMultiPoint: (value: any) => 46 | Edm.GeographyMultiPoint.createValue(value), 47 | GeographyMultiLineString: (value: any) => 48 | Edm.GeographyMultiLineString.createValue(value), 49 | GeographyMultiPolygon: (value: any) => 50 | Edm.GeographyMultiPolygon.createValue(value), 51 | GeographyCollection: (value: any) => 52 | Edm.GeographyCollection.createValue(value), 53 | Geometry: (value: any) => Edm.Geometry.createValue(value), 54 | GeometryPoint: (value: any) => Edm.GeometryPoint.createValue(value), 55 | GeometryLineString: (value: any) => Edm.GeometryLineString.createValue(value), 56 | GeometryPolygon: (value: any) => Edm.GeometryPolygon.createValue(value), 57 | GeometryMultiPoint: (value: any) => Edm.GeometryMultiPoint.createValue(value), 58 | GeometryMultiLineString: (value: any) => 59 | Edm.GeometryMultiLineString.createValue(value), 60 | GeometryMultiPolygon: (value: any) => 61 | Edm.GeometryMultiPolygon.createValue(value), 62 | GeometryCollection: (value: any) => Edm.GeometryCollection.createValue(value) 63 | }; 64 | -------------------------------------------------------------------------------- /src/builder/param.ts: -------------------------------------------------------------------------------- 1 | import concat from '@newdash/newdash/concat'; 2 | import isArray from '@newdash/newdash/isArray'; 3 | import join from '@newdash/newdash/join'; 4 | import uniq from '@newdash/newdash/uniq'; 5 | import { ODataFilter } from './filter'; 6 | import { ODataVersion } from './types'; 7 | 8 | class SearchParams { 9 | _store = new Map(); 10 | 11 | append(key: string, value: string): void { 12 | if (this._store.has(key)) { 13 | throw new Error( 14 | `key ${key} has been appended before, and can not be overwritten!` 15 | ); 16 | } 17 | this._store.set(key, value); 18 | } 19 | 20 | toString(): string { 21 | const coll = []; 22 | this._store.forEach((value, key) => { 23 | coll.push(`${key}=${value}`); 24 | }); 25 | return coll.join('&'); 26 | } 27 | 28 | destroy() { 29 | delete this._store; 30 | } 31 | } 32 | 33 | export interface ODataParamOrderField { 34 | /** 35 | * field name 36 | */ 37 | field: string; 38 | 39 | /** 40 | * order asc or desc 41 | */ 42 | order?: 'asc' | 'desc'; 43 | } 44 | 45 | /** 46 | * OData Query Param 47 | * 48 | * OData V4 support 49 | */ 50 | export class ODataQueryParam { 51 | static New(): ODataQueryParam { 52 | return new ODataQueryParam(); 53 | } 54 | 55 | private $skip = 0; 56 | private $filter: string | ODataFilter; 57 | private $top = 0; 58 | private $select: string[] = []; 59 | private $orderby: string; 60 | private $format: 'json' | 'xml'; 61 | private $search: string; 62 | private $expand: string[] = []; 63 | private $count = false; 64 | 65 | /** 66 | * 67 | * count items in odata v4 68 | * 69 | * @param count 70 | * 71 | * @version 4.0.0 72 | */ 73 | count(count = true): ODataQueryParam { 74 | this.$count = count; 75 | return this; 76 | } 77 | 78 | /** 79 | * apply filter for query 80 | * 81 | * @param filter 82 | */ 83 | filter(filter?: any) { 84 | if (filter instanceof ODataFilter) { 85 | this.$filter = filter.build(); 86 | return this; 87 | } else if (typeof filter === 'object') { 88 | this.$filter = ODataFilter.New(filter).build(); 89 | return this; 90 | } else if (typeof filter === 'string') { 91 | this.$filter = filter; 92 | return this; 93 | } 94 | throw new Error( 95 | 'ODataQueryParam.filter only accept string or ODataFilter type parameter' 96 | ); 97 | } 98 | 99 | /** 100 | * skip first records 101 | * 102 | * @param skip 103 | */ 104 | skip(skip: number) { 105 | this.$skip = skip; 106 | return this; 107 | } 108 | 109 | /** 110 | * limit result max records 111 | * 112 | * @param top 113 | */ 114 | top(top: number) { 115 | this.$top = top; 116 | return this; 117 | } 118 | 119 | /** 120 | * select viewed fields 121 | * 122 | * @param selects 123 | */ 124 | select(selects: string | string[]) { 125 | this.$select = concat(this.$select, selects as any); 126 | return this; 127 | } 128 | 129 | /** 130 | * set order sequence 131 | * 132 | * @param fieldOrOrders 133 | * @param order default desc, disabled when first params is array 134 | */ 135 | orderby( 136 | fieldOrOrders: string | ODataParamOrderField[], 137 | order: 'asc' | 'desc' = 'desc' 138 | ) { 139 | if (isArray(fieldOrOrders)) { 140 | return this.orderbyMulti(fieldOrOrders); 141 | } 142 | this.$orderby = `${fieldOrOrders} ${order}`; 143 | return this; 144 | } 145 | 146 | /** 147 | * set order by multi field 148 | * 149 | * @param fields 150 | */ 151 | orderbyMulti(fields: ODataParamOrderField[] = []) { 152 | this.$orderby = join( 153 | fields.map((f) => `${f.field} ${f.order || 'desc'}`), 154 | ',' 155 | ); 156 | return this; 157 | } 158 | 159 | /** 160 | * result format, please keep it as json 161 | * 162 | * @param format default json 163 | */ 164 | format(format: 'json' | 'xml') { 165 | this.$format = format; 166 | return this; 167 | } 168 | 169 | /** 170 | * full text search 171 | * 172 | * default with fuzzy search, SAP system or OData V4 only 173 | * 174 | * @param value 175 | * @version 4.0.0 176 | */ 177 | search(value: string): this { 178 | this.$search = value; 179 | return this; 180 | } 181 | 182 | /** 183 | * expand navigation props 184 | * 185 | * @param fields 186 | * @param replace 187 | */ 188 | expand(fields: string | string[], replace = false): this { 189 | if (replace) { 190 | if (typeof fields == 'string') { 191 | this.$expand = [fields]; 192 | } else if (isArray(fields)) { 193 | this.$expand = fields; 194 | } 195 | } else { 196 | this.$expand = concat(this.$expand, fields as any); 197 | } 198 | return this; 199 | } 200 | 201 | toString(version: ODataVersion = 'v4'): string { 202 | const rt = new SearchParams(); 203 | if (this.$format) { 204 | rt.append('$format', this.$format); 205 | } 206 | if (this.$filter) { 207 | rt.append('$filter', this.$filter.toString()); 208 | } 209 | if (this.$orderby) { 210 | rt.append('$orderby', this.$orderby); 211 | } 212 | if (this.$search) { 213 | rt.append('$search', this.$search); 214 | } 215 | if (this.$select && this.$select.length > 0) { 216 | rt.append('$select', join(uniq(this.$select), ',')); 217 | } 218 | if (this.$skip) { 219 | rt.append('$skip', this.$skip.toString()); 220 | } 221 | if (this.$top && this.$top > 0) { 222 | rt.append('$top', this.$top.toString()); 223 | } 224 | if (this.$expand && this.$expand.length > 0) { 225 | rt.append('$expand', this.$expand.join(',')); 226 | } 227 | switch (version) { 228 | case 'v2': 229 | if (this.$count) { 230 | rt.append('$inlinecount', 'allpages'); 231 | } 232 | break; 233 | case 'v4': 234 | if (this.$count) { 235 | rt.append('$count', 'true'); 236 | } 237 | break; 238 | default: 239 | break; 240 | } 241 | return rt.toString(); 242 | } 243 | } 244 | 245 | export const ODataParam = ODataQueryParam; 246 | -------------------------------------------------------------------------------- /src/builder/types.ts: -------------------------------------------------------------------------------- 1 | import { Edm } from '@odata/metadata'; 2 | 3 | export type ODataVersion = 'v2' | 'v4'; 4 | 5 | /** 6 | * 7 | * @param value primitive literal value 8 | * @returns the string representation 9 | */ 10 | export function convertPrimitiveValueToString( 11 | value: Edm.PrimitiveTypeValue, 12 | version: ODataVersion = 'v4' 13 | ) { 14 | if (value?.getValue?.() === null) { 15 | return 'null'; 16 | } 17 | 18 | if (value?.getValue?.() !== undefined) { 19 | switch (value?.getType?.()) { 20 | case Edm.Int16: 21 | case Edm.Int32: 22 | case Edm.Int64: 23 | case Edm.Guid: 24 | case Edm.Double: 25 | case Edm.Decimal: 26 | case Edm.Byte: 27 | case Edm.SByte: 28 | case Edm.Single: 29 | return String(value.getValue()); 30 | case Edm.Boolean: 31 | return String(value.getValue()); 32 | case Edm.Binary: 33 | const vB = value.getValue(); 34 | if (vB instanceof Buffer) { 35 | return `binary'${vB.toString('base64')}'`; 36 | } 37 | return String(vB); 38 | case Edm.String: 39 | return `'${value.getValue()}'`; 40 | case Edm.Duration: 41 | // TODO integrate with some other duration lib 42 | return value.getValue(); 43 | case Edm.DateTime: 44 | let vd = value.getValue(); 45 | if (typeof vd === 'string') { 46 | vd = new Date(vd); 47 | } 48 | if (version === 'v2') { 49 | return `datetime'${vd.toISOString()}'`; 50 | } 51 | throw new Error("OData V4 is not support 'Edm.DateTime' values"); 52 | case Edm.DateTimeOffset: 53 | let v1 = value.getValue(); 54 | if (typeof v1 === 'string') { 55 | v1 = new Date(v1); 56 | } 57 | if (version === 'v2') { 58 | return `datetimeoffset'${v1.toISOString()}'`; 59 | } 60 | return v1.toISOString(); 61 | case Edm.Date: 62 | const v2 = value.getValue(); 63 | if (v2 instanceof Date) { 64 | return `${v2.getFullYear()}-${v2.getMonth() + 1}-${v2.getDate()}`; 65 | } 66 | return v2; 67 | case Edm.Geography: 68 | case Edm.GeographyPoint: 69 | case Edm.GeographyLineString: 70 | case Edm.GeographyPolygon: 71 | case Edm.GeographyMultiPoint: 72 | case Edm.GeographyMultiLineString: 73 | case Edm.GeographyMultiPolygon: 74 | case Edm.GeographyCollection: 75 | case Edm.Geometry: 76 | case Edm.GeometryPoint: 77 | case Edm.GeometryLineString: 78 | case Edm.GeometryPolygon: 79 | case Edm.GeometryMultiPoint: 80 | case Edm.GeometryMultiLineString: 81 | case Edm.GeometryMultiPolygon: 82 | case Edm.GeometryCollection: 83 | return String(value.getValue()); 84 | default: 85 | throw new TypeError(`not support type '${value.getType()}'`); 86 | } 87 | } 88 | 89 | throw new Error("'undefined' value provided"); 90 | } 91 | -------------------------------------------------------------------------------- /src/constants/index.ts: -------------------------------------------------------------------------------- 1 | export * from './method'; 2 | -------------------------------------------------------------------------------- /src/constants/method.ts: -------------------------------------------------------------------------------- 1 | export enum ODataMethod { 2 | GET = 'GET', 3 | POST = 'POST', 4 | PATCH = 'PATCH', 5 | PUT = 'PUT', 6 | DELETE = 'DELETE', 7 | } 8 | 9 | export const ODataMethods = [ 10 | ODataMethod.GET, 11 | ODataMethod.POST, 12 | ODataMethod.PATCH, 13 | ODataMethod.PUT, 14 | ODataMethod.DELETE 15 | ]; 16 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { Parser } from './parser'; 2 | 3 | export * from './builder'; 4 | export * from './constants'; 5 | export * from './lexer'; 6 | export * from './token'; 7 | export * from './types'; 8 | export * from './visitor'; 9 | 10 | export const defaultParser = new Parser(); 11 | -------------------------------------------------------------------------------- /src/json.ts: -------------------------------------------------------------------------------- 1 | import * as Expressions from './expressions'; 2 | import * as Lexer from './lexer'; 3 | import * as NameOrIdentifier from './nameOrIdentifier'; 4 | import * as PrimitiveLiteral from './primitiveLiteral'; 5 | import Utils, { SourceArray } from './utils'; 6 | 7 | export function complexColInUri( 8 | value: SourceArray, 9 | index: number 10 | ): Lexer.Token { 11 | const begin = Lexer.beginArray(value, index); 12 | if (begin === index) { 13 | return; 14 | } 15 | const start = index; 16 | index = begin; 17 | 18 | const items = []; 19 | let token = complexInUri(value, index); 20 | if (token) { 21 | while (token) { 22 | items.push(token); 23 | index = token.next; 24 | 25 | const end = Lexer.endArray(value, index); 26 | if (end > index) { 27 | index = end; 28 | break; 29 | } else { 30 | const separator = Lexer.valueSeparator(value, index); 31 | if (separator === index) { 32 | return; 33 | } 34 | index = separator; 35 | 36 | token = complexInUri(value, index); 37 | if (!token) { 38 | return; 39 | } 40 | } 41 | } 42 | } else { 43 | const end = Lexer.endArray(value, index); 44 | if (end === index) { 45 | return; 46 | } 47 | index = end; 48 | } 49 | 50 | return Lexer.tokenize(value, start, index, { items }, Lexer.TokenType.Array); 51 | } 52 | 53 | export function complexInUri(value: SourceArray, index: number): Lexer.Token { 54 | const begin = Lexer.beginObject(value, index); 55 | if (begin === index) { 56 | return; 57 | } 58 | const start = index; 59 | index = begin; 60 | 61 | const items = []; 62 | let token = 63 | annotationInUri(value, index) || 64 | primitivePropertyInUri(value, index) || 65 | complexPropertyInUri(value, index) || 66 | collectionPropertyInUri(value, index) || 67 | navigationPropertyInUri(value, index); 68 | if (token) { 69 | while (token) { 70 | items.push(token); 71 | index = token.next; 72 | 73 | const end = Lexer.endObject(value, index); 74 | if (end > index) { 75 | index = end; 76 | break; 77 | } else { 78 | const separator = Lexer.valueSeparator(value, index); 79 | if (separator === index) { 80 | return; 81 | } 82 | index = separator; 83 | 84 | token = 85 | annotationInUri(value, index) || 86 | primitivePropertyInUri(value, index) || 87 | complexPropertyInUri(value, index) || 88 | collectionPropertyInUri(value, index) || 89 | navigationPropertyInUri(value, index); 90 | if (!token) { 91 | return; 92 | } 93 | } 94 | } 95 | } else { 96 | const end = Lexer.endObject(value, index); 97 | if (end === index) { 98 | return; 99 | } 100 | index = end; 101 | } 102 | 103 | return Lexer.tokenize(value, start, index, { items }, Lexer.TokenType.Object); 104 | } 105 | 106 | export function collectionPropertyInUri( 107 | value: SourceArray, 108 | index: number 109 | ): Lexer.Token { 110 | let mark = Lexer.quotationMark(value, index); 111 | if (mark === index) { 112 | return; 113 | } 114 | const start = index; 115 | index = mark; 116 | 117 | const prop = 118 | NameOrIdentifier.primitiveColProperty(value, index) || 119 | NameOrIdentifier.complexColProperty(value, index); 120 | 121 | if (!prop) { 122 | return; 123 | } 124 | index = prop.next; 125 | 126 | mark = Lexer.quotationMark(value, index); 127 | if (mark === index) { 128 | return; 129 | } 130 | index = mark; 131 | 132 | const separator = Lexer.nameSeparator(value, index); 133 | if (separator === index) { 134 | return; 135 | } 136 | index = separator; 137 | 138 | const propValue = 139 | prop.type === Lexer.TokenType.PrimitiveCollectionProperty 140 | ? primitiveColInUri(value, index) 141 | : complexColInUri(value, index); 142 | 143 | if (!propValue) { 144 | return; 145 | } 146 | index = propValue.next; 147 | 148 | return Lexer.tokenize( 149 | value, 150 | start, 151 | index, 152 | { key: prop, value: propValue }, 153 | Lexer.TokenType.Property 154 | ); 155 | } 156 | 157 | export function primitiveColInUri( 158 | value: SourceArray, 159 | index: number 160 | ): Lexer.Token { 161 | const begin = Lexer.beginArray(value, index); 162 | if (begin === index) { 163 | return; 164 | } 165 | const start = index; 166 | index = begin; 167 | 168 | const items = []; 169 | let token = primitiveLiteralInJSON(value, index); 170 | if (token) { 171 | while (token) { 172 | items.push(token); 173 | index = token.next; 174 | 175 | const end = Lexer.endArray(value, index); 176 | if (end > index) { 177 | index = end; 178 | break; 179 | } else { 180 | const separator = Lexer.valueSeparator(value, index); 181 | if (separator === index) { 182 | return; 183 | } 184 | index = separator; 185 | 186 | token = primitiveLiteralInJSON(value, index); 187 | if (!token) { 188 | return; 189 | } 190 | } 191 | } 192 | } else { 193 | const end = Lexer.endArray(value, index); 194 | if (end === index) { 195 | return; 196 | } 197 | index = end; 198 | } 199 | 200 | return Lexer.tokenize(value, start, index, { items }, Lexer.TokenType.Array); 201 | } 202 | 203 | export function complexPropertyInUri( 204 | value: SourceArray, 205 | index: number 206 | ): Lexer.Token { 207 | let mark = Lexer.quotationMark(value, index); 208 | if (mark === index) { 209 | return; 210 | } 211 | const start = index; 212 | index = mark; 213 | 214 | const prop = NameOrIdentifier.complexProperty(value, index); 215 | if (!prop) { 216 | return; 217 | } 218 | index = prop.next; 219 | 220 | mark = Lexer.quotationMark(value, index); 221 | if (mark === index) { 222 | return; 223 | } 224 | index = mark; 225 | 226 | const separator = Lexer.nameSeparator(value, index); 227 | if (separator === index) { 228 | return; 229 | } 230 | index = separator; 231 | 232 | const propValue = complexInUri(value, index); 233 | if (!propValue) { 234 | return; 235 | } 236 | index = propValue.next; 237 | 238 | return Lexer.tokenize( 239 | value, 240 | start, 241 | index, 242 | { key: prop, value: propValue }, 243 | Lexer.TokenType.Property 244 | ); 245 | } 246 | 247 | export function annotationInUri( 248 | value: SourceArray, 249 | index: number 250 | ): Lexer.Token { 251 | let mark = Lexer.quotationMark(value, index); 252 | if (mark === index) { 253 | return; 254 | } 255 | const start = index; 256 | index = mark; 257 | 258 | const at = Lexer.AT(value, index); 259 | if (!at) { 260 | return; 261 | } 262 | index = at; 263 | 264 | const namespaceNext = NameOrIdentifier.namespace(value, index); 265 | if (namespaceNext === index) { 266 | return; 267 | } 268 | const namespaceStart = index; 269 | index = namespaceNext; 270 | 271 | if (value[index] !== 0x2e) { 272 | return; 273 | } 274 | index++; 275 | 276 | const term = NameOrIdentifier.termName(value, index); 277 | if (!term) { 278 | return; 279 | } 280 | index = term.next; 281 | 282 | mark = Lexer.quotationMark(value, index); 283 | if (mark === index) { 284 | return; 285 | } 286 | index = mark; 287 | 288 | const separator = Lexer.nameSeparator(value, index); 289 | if (separator === index) { 290 | return; 291 | } 292 | index = separator; 293 | 294 | const token = 295 | complexInUri(value, index) || 296 | complexColInUri(value, index) || 297 | primitiveLiteralInJSON(value, index) || 298 | primitiveColInUri(value, index); 299 | if (!token) { 300 | return; 301 | } 302 | index = token.next; 303 | 304 | return Lexer.tokenize( 305 | value, 306 | start, 307 | index, 308 | { 309 | key: `@${Utils.stringify(value, namespaceStart, namespaceNext)}.${ 310 | term.raw 311 | }`, 312 | value: token 313 | }, 314 | Lexer.TokenType.Annotation 315 | ); 316 | } 317 | 318 | export function keyValuePairInUri( 319 | value: SourceArray, 320 | index: number, 321 | keyFn: Function, 322 | valueFn: Function 323 | ): Lexer.Token { 324 | let mark = Lexer.quotationMark(value, index); 325 | if (mark === index) { 326 | return; 327 | } 328 | const start = index; 329 | index = mark; 330 | 331 | const prop = keyFn(value, index); 332 | if (!prop) { 333 | return; 334 | } 335 | index = prop.next; 336 | 337 | mark = Lexer.quotationMark(value, index); 338 | if (mark === index) { 339 | return; 340 | } 341 | index = mark; 342 | 343 | const separator = Lexer.nameSeparator(value, index); 344 | if (separator === index) { 345 | return; 346 | } 347 | index = separator; 348 | 349 | const propValue = valueFn(value, index); 350 | if (!propValue) { 351 | return; 352 | } 353 | index = propValue.next; 354 | 355 | return Lexer.tokenize( 356 | value, 357 | start, 358 | index, 359 | { key: prop, value: propValue }, 360 | Lexer.TokenType.Property 361 | ); 362 | } 363 | 364 | export function primitivePropertyInUri( 365 | value: SourceArray, 366 | index: number 367 | ): Lexer.Token { 368 | return keyValuePairInUri( 369 | value, 370 | index, 371 | NameOrIdentifier.primitiveProperty, 372 | primitiveLiteralInJSON 373 | ); 374 | } 375 | 376 | export function navigationPropertyInUri( 377 | value: SourceArray, 378 | index: number 379 | ): Lexer.Token { 380 | return ( 381 | singleNavPropInJSON(value, index) || collectionNavPropInJSON(value, index) 382 | ); 383 | } 384 | 385 | export function singleNavPropInJSON( 386 | value: SourceArray, 387 | index: number 388 | ): Lexer.Token { 389 | return keyValuePairInUri( 390 | value, 391 | index, 392 | NameOrIdentifier.entityNavigationProperty, 393 | Expressions.rootExpr 394 | ); 395 | } 396 | 397 | export function collectionNavPropInJSON( 398 | value: SourceArray, 399 | index: number 400 | ): Lexer.Token { 401 | return keyValuePairInUri( 402 | value, 403 | index, 404 | NameOrIdentifier.entityColNavigationProperty, 405 | rootExprCol 406 | ); 407 | } 408 | 409 | export function rootExprCol(value: SourceArray, index: number): Lexer.Token { 410 | const begin = Lexer.beginArray(value, index); 411 | if (begin === index) { 412 | return; 413 | } 414 | const start = index; 415 | index = begin; 416 | 417 | const items = []; 418 | let token = Expressions.rootExpr(value, index); 419 | if (token) { 420 | while (token) { 421 | items.push(token); 422 | index = token.next; 423 | 424 | const end = Lexer.endArray(value, index); 425 | if (end > index) { 426 | index = end; 427 | break; 428 | } else { 429 | const separator = Lexer.valueSeparator(value, index); 430 | if (separator === index) { 431 | return; 432 | } 433 | index = separator; 434 | 435 | token = Expressions.rootExpr(value, index); 436 | if (!token) { 437 | return; 438 | } 439 | } 440 | } 441 | } else { 442 | const end = Lexer.endArray(value, index); 443 | if (end === index) { 444 | return; 445 | } 446 | index = end; 447 | } 448 | 449 | return Lexer.tokenize(value, start, index, { items }, Lexer.TokenType.Array); 450 | } 451 | 452 | export function primitiveLiteralInJSON( 453 | value: SourceArray, 454 | index: number 455 | ): Lexer.Token { 456 | return ( 457 | stringInJSON(value, index) || 458 | numberInJSON(value, index) || 459 | booleanInJSON(value, index) || 460 | nullInJSON(value, index) 461 | ); 462 | } 463 | 464 | export function stringInJSON(value: SourceArray, index: number): Lexer.Token { 465 | let mark = Lexer.quotationMark(value, index); 466 | if (mark === index) { 467 | return; 468 | } 469 | const start = index; 470 | index = mark; 471 | 472 | let char = charInJSON(value, index); 473 | while (char > index) { 474 | index = char; 475 | char = charInJSON(value, index); 476 | } 477 | 478 | mark = Lexer.quotationMark(value, index); 479 | if (mark === index) { 480 | return; 481 | } 482 | index = mark; 483 | 484 | return Lexer.tokenize(value, start, index, 'string', Lexer.TokenType.Literal); 485 | } 486 | 487 | export function charInJSON(value: SourceArray, index: number): number { 488 | const escape = Lexer.escape(value, index); 489 | if (escape > index) { 490 | if (Utils.equals(value, escape, '%2F')) { 491 | return escape + 3; 492 | } 493 | if ( 494 | Utils.equals(value, escape, '/') || 495 | Utils.equals(value, escape, 'b') || 496 | Utils.equals(value, escape, 'f') || 497 | Utils.equals(value, escape, 'n') || 498 | Utils.equals(value, escape, 'r') || 499 | Utils.equals(value, escape, 't') 500 | ) { 501 | return escape + 1; 502 | } 503 | if ( 504 | Utils.equals(value, escape, 'u') && 505 | Utils.required(value, escape + 1, Lexer.HEXDIG, 4, 4) 506 | ) { 507 | return escape + 5; 508 | } 509 | const escapeNext = Lexer.escape(value, escape); 510 | if (escapeNext > escape) { 511 | return escapeNext; 512 | } 513 | const mark = Lexer.quotationMark(value, escape); 514 | if (mark > escape) { 515 | return mark; 516 | } 517 | } else { 518 | const mark = Lexer.quotationMark(value, index); 519 | if (mark === index) { 520 | return index + 1; 521 | } 522 | } 523 | } 524 | 525 | export function numberInJSON(value: SourceArray, index: number): Lexer.Token { 526 | const token = 527 | PrimitiveLiteral.doubleValue(value, index) || 528 | PrimitiveLiteral.int64Value(value, index); 529 | if (token) { 530 | token.value = 'number'; 531 | return token; 532 | } 533 | } 534 | 535 | export function booleanInJSON(value: SourceArray, index: number): Lexer.Token { 536 | if (Utils.equals(value, index, 'true')) { 537 | return Lexer.tokenize( 538 | value, 539 | index, 540 | index + 4, 541 | 'boolean', 542 | Lexer.TokenType.Literal 543 | ); 544 | } 545 | if (Utils.equals(value, index, 'false')) { 546 | return Lexer.tokenize( 547 | value, 548 | index, 549 | index + 5, 550 | 'boolean', 551 | Lexer.TokenType.Literal 552 | ); 553 | } 554 | } 555 | 556 | export function nullInJSON(value: SourceArray, index: number): Lexer.Token { 557 | if (Utils.equals(value, index, 'null')) { 558 | return Lexer.tokenize( 559 | value, 560 | index, 561 | index + 4, 562 | 'null', 563 | Lexer.TokenType.Literal 564 | ); 565 | } 566 | } 567 | 568 | export function arrayOrObject(value: SourceArray, index: number): Lexer.Token { 569 | const token = 570 | complexColInUri(value, index) || 571 | complexInUri(value, index) || 572 | rootExprCol(value, index) || 573 | primitiveColInUri(value, index); 574 | 575 | if (token) { 576 | return Lexer.tokenize( 577 | value, 578 | index, 579 | token.next, 580 | token, 581 | Lexer.TokenType.ArrayOrObject 582 | ); 583 | } 584 | } 585 | -------------------------------------------------------------------------------- /src/lexer.ts: -------------------------------------------------------------------------------- 1 | import Utils, { SourceArray } from './utils'; 2 | 3 | export enum TokenType { 4 | Literal = 'Literal', 5 | ArrayOrObject = 'ArrayOrObject', 6 | Array = 'Array', 7 | Object = 'Object', 8 | Property = 'Property', 9 | Annotation = 'Annotation', 10 | Enum = 'Enum', 11 | EnumValue = 'EnumValue', 12 | EnumMemberValue = 'EnumMemberValue', 13 | Identifier = 'Identifier', 14 | QualifiedEntityTypeName = 'QualifiedEntityTypeName', 15 | QualifiedComplexTypeName = 'QualifiedComplexTypeName', 16 | ODataIdentifier = 'ODataIdentifier', 17 | Collection = 'Collection', 18 | NamespacePart = 'NamespacePart', 19 | EntitySetName = 'EntitySetName', 20 | SingletonEntity = 'SingletonEntity', 21 | EntityTypeName = 'EntityTypeName', 22 | ComplexTypeName = 'ComplexTypeName', 23 | TypeDefinitionName = 'TypeDefinitionName', 24 | EnumerationTypeName = 'EnumerationTypeName', 25 | EnumerationMember = 'EnumerationMember', 26 | TermName = 'TermName', 27 | PrimitiveProperty = 'PrimitiveProperty', 28 | PrimitiveKeyProperty = 'PrimitiveKeyProperty', 29 | PrimitiveNonKeyProperty = 'PrimitiveNonKeyProperty', 30 | PrimitiveCollectionProperty = 'PrimitiveCollectionProperty', 31 | ComplexProperty = 'ComplexProperty', 32 | ComplexCollectionProperty = 'ComplexCollectionProperty', 33 | StreamProperty = 'StreamProperty', 34 | NavigationProperty = 'NavigationProperty', 35 | EntityNavigationProperty = 'EntityNavigationProperty', 36 | EntityCollectionNavigationProperty = 'EntityCollectionNavigationProperty', 37 | Action = 'Action', 38 | ActionImport = 'ActionImport', 39 | Function = 'Function', 40 | EntityFunction = 'EntityFunction', 41 | EntityCollectionFunction = 'EntityCollectionFunction', 42 | ComplexFunction = 'ComplexFunction', 43 | ComplexCollectionFunction = 'ComplexCollectionFunction', 44 | PrimitiveFunction = 'PrimitiveFunction', 45 | PrimitiveCollectionFunction = 'PrimitiveCollectionFunction', 46 | EntityFunctionImport = 'EntityFunctionImport', 47 | EntityCollectionFunctionImport = 'EntityCollectionFunctionImport', 48 | ComplexFunctionImport = 'ComplexFunctionImport', 49 | ComplexCollectionFunctionImport = 'ComplexCollectionFunctionImport', 50 | PrimitiveFunctionImport = 'PrimitiveFunctionImport', 51 | PrimitiveCollectionFunctionImport = 'PrimitiveCollectionFunctionImport', 52 | CommonExpression = 'CommonExpression', 53 | AndExpression = 'AndExpression', 54 | OrExpression = 'OrExpression', 55 | EqualsExpression = 'EqualsExpression', 56 | NotEqualsExpression = 'NotEqualsExpression', 57 | LesserThanExpression = 'LesserThanExpression', 58 | LesserOrEqualsExpression = 'LesserOrEqualsExpression', 59 | GreaterThanExpression = 'GreaterThanExpression', 60 | GreaterOrEqualsExpression = 'GreaterOrEqualsExpression', 61 | HasExpression = 'HasExpression', 62 | AddExpression = 'AddExpression', 63 | SubExpression = 'SubExpression', 64 | MulExpression = 'MulExpression', 65 | DivExpression = 'DivExpression', 66 | ModExpression = 'ModExpression', 67 | NotExpression = 'NotExpression', 68 | BoolParenExpression = 'BoolParenExpression', 69 | ParenExpression = 'ParenExpression', 70 | MethodCallExpression = 'MethodCallExpression', 71 | IsOfExpression = 'IsOfExpression', 72 | CastExpression = 'CastExpression', 73 | NegateExpression = 'NegateExpression', 74 | FirstMemberExpression = 'FirstMemberExpression', 75 | MemberExpression = 'MemberExpression', 76 | PropertyPathExpression = 'PropertyPathExpression', 77 | ImplicitVariableExpression = 'ImplicitVariableExpression', 78 | LambdaVariable = 'LambdaVariable', 79 | LambdaVariableExpression = 'LambdaVariableExpression', 80 | LambdaPredicateExpression = 'LambdaPredicateExpression', 81 | AnyExpression = 'AnyExpression', 82 | AllExpression = 'AllExpression', 83 | CollectionNavigationExpression = 'CollectionNavigationExpression', 84 | SimpleKey = 'SimpleKey', 85 | CompoundKey = 'CompoundKey', 86 | KeyValuePair = 'KeyValuePair', 87 | KeyPropertyValue = 'KeyPropertyValue', 88 | KeyPropertyAlias = 'KeyPropertyAlias', 89 | SingleNavigationExpression = 'SingleNavigationExpression', 90 | CollectionPathExpression = 'CollectionPathExpression', 91 | ComplexPathExpression = 'ComplexPathExpression', 92 | SinglePathExpression = 'SinglePathExpression', 93 | FunctionExpression = 'FunctionExpression', 94 | FunctionExpressionParameters = 'FunctionExpressionParameters', 95 | FunctionExpressionParameter = 'FunctionExpressionParameter', 96 | ParameterName = 'ParameterName', 97 | ParameterAlias = 'ParameterAlias', 98 | ParameterValue = 'ParameterValue', 99 | CountExpression = 'CountExpression', 100 | RefExpression = 'RefExpression', 101 | ValueExpression = 'ValueExpression', 102 | RootExpression = 'RootExpression', 103 | QueryOptions = 'QueryOptions', 104 | CustomQueryOption = 'CustomQueryOption', 105 | Expand = 'Expand', 106 | ExpandItem = 'ExpandItem', 107 | ExpandPath = 'ExpandPath', 108 | ExpandCountOption = 'ExpandCountOption', 109 | ExpandRefOption = 'ExpandRefOption', 110 | ExpandOption = 'ExpandOption', 111 | Levels = 'Levels', 112 | Search = 'Search', 113 | SearchExpression = 'SearchExpression', 114 | SearchParenExpression = 'SearchParenExpression', 115 | SearchNotExpression = 'SearchNotExpression', 116 | SearchOrExpression = 'SearchOrExpression', 117 | SearchAndExpression = 'SearchAndExpression', 118 | SearchTerm = 'SearchTerm', 119 | SearchPhrase = 'SearchPhrase', 120 | SearchWord = 'SearchWord', 121 | Filter = 'Filter', 122 | OrderBy = 'OrderBy', 123 | OrderByItem = 'OrderByItem', 124 | Skip = 'Skip', 125 | Top = 'Top', 126 | Format = 'Format', 127 | InlineCount = 'InlineCount', 128 | Select = 'Select', 129 | SelectItem = 'SelectItem', 130 | SelectPath = 'SelectPath', 131 | AliasAndValue = 'AliasAndValue', 132 | SkipToken = 'SkipToken', 133 | Id = 'Id', 134 | Crossjoin = 'Crossjoin', 135 | AllResource = 'AllResource', 136 | ActionImportCall = 'ActionImportCall', 137 | FunctionImportCall = 'FunctionImportCall', 138 | EntityCollectionFunctionImportCall = 'EntityCollectionFunctionImportCall', 139 | EntityFunctionImportCall = 'EntityFunctionImportCall', 140 | ComplexCollectionFunctionImportCall = 'ComplexCollectionFunctionImportCall', 141 | ComplexFunctionImportCall = 'ComplexFunctionImportCall', 142 | PrimitiveCollectionFunctionImportCall = 'PrimitiveCollectionFunctionImportCall', 143 | PrimitiveFunctionImportCall = 'PrimitiveFunctionImportCall', 144 | FunctionParameters = 'FunctionParameters', 145 | FunctionParameter = 'FunctionParameter', 146 | ResourcePath = 'ResourcePath', 147 | CollectionNavigation = 'CollectionNavigation', 148 | CollectionNavigationPath = 'CollectionNavigationPath', 149 | SingleNavigation = 'SingleNavigation', 150 | PropertyPath = 'PropertyPath', 151 | ComplexPath = 'ComplexPath', 152 | BoundOperation = 'BoundOperation', 153 | BoundActionCall = 'BoundActionCall', 154 | BoundEntityFunctionCall = 'BoundEntityFunctionCall', 155 | BoundEntityCollectionFunctionCall = 'BoundEntityCollectionFunctionCall', 156 | BoundComplexFunctionCall = 'BoundComplexFunctionCall', 157 | BoundComplexCollectionFunctionCall = 'BoundComplexCollectionFunctionCall', 158 | BoundPrimitiveFunctionCall = 'BoundPrimitiveFunctionCall', 159 | BoundPrimitiveCollectionFunctionCall = 'BoundPrimitiveCollectionFunctionCall', 160 | ODataUri = 'ODataUri', 161 | Batch = 'Batch', 162 | Entity = 'Entity', 163 | Metadata = 'Metadata' 164 | } 165 | 166 | export const LexerTokenType = TokenType; 167 | export type LexerTokenType = TokenType; 168 | 169 | export class Token { 170 | position: number; 171 | next: number; 172 | value: any; 173 | type: TokenType; 174 | /** 175 | * raw string of token 176 | */ 177 | raw: string; 178 | metadata: any; 179 | constructor(token: { 180 | position: number; 181 | next: number; 182 | value: any; 183 | type: TokenType; 184 | raw: string; 185 | metadata?: any; 186 | }) { 187 | this.position = token.position; 188 | this.next = token.next; 189 | this.value = token.value; 190 | this.type = token.type; 191 | this.raw = token.raw; 192 | if (token.metadata) { 193 | this.metadata = token.metadata; 194 | } 195 | } 196 | } 197 | 198 | export type LexerToken = Token; 199 | 200 | export function tokenize( 201 | value: SourceArray, 202 | index: number, 203 | next: number, 204 | tokenValue: any, 205 | tokenType: TokenType, 206 | metadataContextContainer?: Token 207 | ): Token { 208 | const token = new Token({ 209 | position: index, 210 | next, 211 | value: tokenValue, 212 | type: tokenType, 213 | raw: Utils.stringify(value, index, next) 214 | }); 215 | if (metadataContextContainer && metadataContextContainer.metadata) { 216 | token.metadata = metadataContextContainer.metadata; 217 | delete metadataContextContainer.metadata; 218 | } 219 | return token; 220 | } 221 | 222 | 223 | export function clone(token): Token { 224 | return new Token({ 225 | position: token.position, 226 | next: token.next, 227 | value: token.value, 228 | type: token.type, 229 | raw: token.raw 230 | }); 231 | } 232 | 233 | // core definitions 234 | export function ALPHA(value: number): boolean { 235 | return ( 236 | (value >= 0x41 && value <= 0x5a) || 237 | (value >= 0x61 && value <= 0x7a) || 238 | value >= 0x80 239 | ); 240 | } 241 | export function DIGIT(value: number): boolean { 242 | return value >= 0x30 && value <= 0x39; 243 | } 244 | export function HEXDIG(value: number): boolean { 245 | return DIGIT(value) || AtoF(value); 246 | } 247 | export function AtoF(value: number): boolean { 248 | return (value >= 0x41 && value <= 0x46) || (value >= 0x61 && value <= 0x66); 249 | } 250 | export function DQUOTE(value: number): boolean { 251 | return value === 0x22; 252 | } 253 | export function SP(value: number): boolean { 254 | return value === 0x20; 255 | } 256 | export function HTAB(value: number): boolean { 257 | return value === 0x09; 258 | } 259 | export function VCHAR(value: number): boolean { 260 | return value >= 0x21 && value <= 0x7e; 261 | } 262 | 263 | // punctuation 264 | export function whitespaceLength(value, index) { 265 | if (Utils.equals(value, index, '%20') || Utils.equals(value, index, '%09')) { 266 | return 3; 267 | } else if ( 268 | SP(value[index]) || 269 | HTAB(value[index]) || 270 | value[index] === 0x20 || 271 | value[index] === 0x09 272 | ) { 273 | return 1; 274 | } 275 | } 276 | 277 | export function OWS(value: SourceArray, index: number): number { 278 | index = index || 0; 279 | let inc = whitespaceLength(value, index); 280 | while (inc) { 281 | index += inc; 282 | inc = whitespaceLength(value, index); 283 | } 284 | return index; 285 | } 286 | export function RWS(value: SourceArray, index: number): number { 287 | return OWS(value, index); 288 | } 289 | export function BWS(value: SourceArray, index: number): number { 290 | return OWS(value, index); 291 | } 292 | 293 | export function AT(value: SourceArray, index: number): number { 294 | if (value[index] === 0x40) { 295 | return index + 1; 296 | } else if (Utils.equals(value, index, '%40')) { 297 | return index + 3; 298 | } 299 | } 300 | export function COLON(value: SourceArray, index: number): number { 301 | if (value[index] === 0x3a) { 302 | return index + 1; 303 | } else if (Utils.equals(value, index, '%3A')) { 304 | return index + 3; 305 | } 306 | } 307 | export function COMMA(value: SourceArray, index: number): number { 308 | if (value[index] === 0x2c) { 309 | return index + 1; 310 | } else if (Utils.equals(value, index, '%2C')) { 311 | return index + 3; 312 | } 313 | } 314 | export function EQ(value: SourceArray, index: number): number { 315 | if (value[index] === 0x3d) { 316 | return index + 1; 317 | } 318 | } 319 | export function SIGN(value: SourceArray, index: number): number { 320 | if (value[index] === 0x2b || value[index] === 0x2d) { 321 | return index + 1; 322 | } else if (Utils.equals(value, index, '%2B')) { 323 | return index + 3; 324 | } 325 | } 326 | export function SEMI(value: SourceArray, index: number): number { 327 | if (value[index] === 0x3b) { 328 | return index + 1; 329 | } else if (Utils.equals(value, index, '%3B')) { 330 | return index + 3; 331 | } 332 | } 333 | export function STAR(value: SourceArray, index: number): number { 334 | if (value[index] === 0x2a) { 335 | return index + 1; 336 | } else if (Utils.equals(value, index, '%2A')) { 337 | return index + 3; 338 | } 339 | } 340 | export function SQUOTE(value: SourceArray, index: number): number { 341 | if (value[index] === 0x27) { 342 | return index + 1; 343 | } else if (Utils.equals(value, index, '%27')) { 344 | return index + 3; 345 | } 346 | } 347 | export function OPEN(value: SourceArray, index: number): number { 348 | if (value[index] === 0x28) { 349 | return index + 1; 350 | } else if (Utils.equals(value, index, '%28')) { 351 | return index + 3; 352 | } 353 | } 354 | export function CLOSE(value: SourceArray, index: number): number { 355 | if (value[index] === 0x29) { 356 | return index + 1; 357 | } else if (Utils.equals(value, index, '%29')) { 358 | return index + 3; 359 | } 360 | } 361 | // unreserved ALPHA / DIGIT / "-" / "." / "_" / "~" 362 | export function unreserved(value: number): boolean { 363 | return ( 364 | ALPHA(value) || 365 | DIGIT(value) || 366 | value === 0x2d || 367 | value === 0x2e || 368 | value === 0x5f || 369 | value === 0x7e 370 | ); 371 | } 372 | // other-delims "!" / "(" / ")" / "*" / "+" / "," / ";" 373 | export function otherDelims(value: SourceArray, index: number): number { 374 | if (value[index] === 0x21 || value[index] === 0x2b) { 375 | return index + 1; 376 | } 377 | return ( 378 | OPEN(value, index) || 379 | CLOSE(value, index) || 380 | STAR(value, index) || 381 | COMMA(value, index) || 382 | SEMI(value, index) 383 | ); 384 | } 385 | // sub-delims = "$" / "&" / "'" / "=" / other-delims 386 | export function subDelims(value: SourceArray, index: number): number { 387 | if (value[index] === 0x24 || value[index] === 0x26) { 388 | return index + 1; 389 | } 390 | return SQUOTE(value, index) || EQ(value, index) || otherDelims(value, index); 391 | } 392 | export function pctEncoded(value: SourceArray, index: number): number { 393 | if ( 394 | value[index] !== 0x25 || 395 | !HEXDIG(value[index + 1]) || 396 | !HEXDIG(value[index + 2]) 397 | ) { 398 | return index; 399 | } 400 | return index + 3; 401 | } 402 | // pct-encoded-no-SQUOTE = "%" ( "0" / "1" / "3" / "4" / "5" / "6" / "8" / "9" / A-to-F ) HEXDIG 403 | // / "%" "2" ( "0" / "1" / "2" / "3" / "4" / "5" / "6" / "8" / "9" / A-to-F ) 404 | export function pctEncodedNoSQUOTE(value: SourceArray, index: number): number { 405 | if (Utils.equals(value, index, '%27')) { 406 | return index; 407 | } 408 | return pctEncoded(value, index); 409 | } 410 | export function pctEncodedUnescaped(value: SourceArray, index: number): number { 411 | if ( 412 | Utils.equals(value, index, '%22') || 413 | Utils.equals(value, index, '%3') || 414 | Utils.equals(value, index, '%4') || 415 | Utils.equals(value, index, '%5C') 416 | ) { 417 | return index; 418 | } 419 | return pctEncoded(value, index); 420 | } 421 | export function pchar(value: SourceArray, index: number): number { 422 | if (unreserved(value[index])) { 423 | return index + 1; 424 | } 425 | return ( 426 | subDelims(value, index) || 427 | COLON(value, index) || 428 | AT(value, index) || 429 | pctEncoded(value, index) || 430 | index 431 | ); 432 | } 433 | 434 | export function pcharNoSQUOTE(value: SourceArray, index: number): number { 435 | if ( 436 | unreserved(value[index]) || 437 | value[index] === 0x24 || 438 | value[index] === 0x26 439 | ) { 440 | return index + 1; 441 | } 442 | return VCHAR(value[index]) === true ? index + 1 : index; 443 | } 444 | export function qcharNoAMP(value: SourceArray, index: number): number { 445 | if ( 446 | unreserved(value[index]) || 447 | value[index] === 0x3a || 448 | value[index] === 0x40 || 449 | value[index] === 0x2f || 450 | value[index] === 0x3f || 451 | value[index] === 0x24 || 452 | value[index] === 0x27 || 453 | value[index] === 0x3d 454 | ) { 455 | return index + 1; 456 | } 457 | return pctEncoded(value, index) || otherDelims(value, index) || index; 458 | } 459 | export function qcharNoAMPDQUOTE(value: SourceArray, index: number): number { 460 | index = BWS(value, index); 461 | if ( 462 | unreserved(value[index]) || 463 | value[index] === 0x3a || 464 | value[index] === 0x40 || 465 | value[index] === 0x2f || 466 | value[index] === 0x3f || 467 | value[index] === 0x24 || 468 | value[index] === 0x27 || 469 | value[index] === 0x3d 470 | ) { 471 | return index + 1; 472 | } 473 | return otherDelims(value, index) || pctEncodedUnescaped(value, index); 474 | } 475 | // export function pchar(value:number):boolean { return unreserved(value) || otherDelims(value) || value == 0x24 || value == 0x26 || EQ(value) || COLON(value) || AT(value); } 476 | export function base64char(value: number): boolean { 477 | return ALPHA(value) || DIGIT(value) || value === 0x2d || value === 0x5f; 478 | } 479 | export function base64b16(value: SourceArray, index: number): number { 480 | const start = index; 481 | if (!base64char(value[index]) && !base64char(value[index + 1])) { 482 | return start; 483 | } 484 | index += 2; 485 | 486 | if (!Utils.is(value[index], 'AEIMQUYcgkosw048')) { 487 | return start; 488 | } 489 | index++; 490 | 491 | if (value[index] === 0x3d) { 492 | index++; 493 | } 494 | return index; 495 | } 496 | export function base64b8(value: SourceArray, index: number): number { 497 | const start = index; 498 | if (!base64char(value[index])) { 499 | return start; 500 | } 501 | index++; 502 | 503 | if ( 504 | value[index] !== 0x41 || 505 | value[index] !== 0x51 || 506 | value[index] !== 0x67 || 507 | value[index] !== 0x77 508 | ) { 509 | return start; 510 | } 511 | index++; 512 | 513 | if (value[index] === 0x3d && value[index + 1] === 0x3d) { 514 | index += 2; 515 | } 516 | return index; 517 | } 518 | export function nanInfinity(value: SourceArray, index: number): number { 519 | return ( 520 | Utils.equals(value, index, 'NaN') || 521 | Utils.equals(value, index, '-INF') || 522 | Utils.equals(value, index, 'INF') 523 | ); 524 | } 525 | export function oneToNine(value: number): boolean { 526 | return value !== 0x30 && DIGIT(value); 527 | } 528 | export function zeroToFiftyNine(value: SourceArray, index: number): number { 529 | if (value[index] >= 0x30 && value[index] <= 0x35 && DIGIT(value[index + 1])) { 530 | return index + 2; 531 | } 532 | return index; 533 | } 534 | export function year(value: SourceArray, index: number): number { 535 | const start = index; 536 | let end = index; 537 | if (value[index] === 0x2d) { 538 | index++; 539 | } 540 | if ( 541 | (value[index] === 0x30 && 542 | (end = Utils.required(value, index + 1, DIGIT, 3, 3))) || 543 | (oneToNine(value[index]) && 544 | (end = Utils.required(value, index + 1, DIGIT, 3))) 545 | ) { 546 | return end; 547 | } 548 | return start; 549 | } 550 | export function month(value: SourceArray, index: number): number { 551 | if ( 552 | (value[index] === 0x30 && oneToNine(value[index + 1])) || 553 | (value[index] === 0x31 && 554 | value[index + 1] >= 0x30 && 555 | value[index + 1] <= 0x32) 556 | ) { 557 | return index + 2; 558 | } 559 | return index; 560 | } 561 | export function day(value: SourceArray, index: number): number { 562 | if ( 563 | (value[index] === 0x30 && oneToNine(value[index + 1])) || 564 | ((value[index] === 0x31 || value[index] === 0x32) && 565 | DIGIT(value[index + 1])) || 566 | (value[index] === 0x33 && 567 | (value[index + 1] === 0x30 || value[index + 1] === 0x31)) 568 | ) { 569 | return index + 2; 570 | } 571 | return index; 572 | } 573 | export function hour(value: SourceArray, index: number): number { 574 | if ( 575 | ((value[index] === 0x30 || value[index] === 0x31) && 576 | DIGIT(value[index + 1])) || 577 | (value[index] === 0x32 && 578 | (value[index + 1] === 0x30 || 579 | value[index + 1] === 0x31 || 580 | value[index + 1] === 0x32 || 581 | value[index + 1] === 0x33)) 582 | ) { 583 | return index + 2; 584 | } 585 | return index; 586 | } 587 | export function minute(value: SourceArray, index: number): number { 588 | return zeroToFiftyNine(value, index); 589 | } 590 | export function second(value: SourceArray, index: number): number { 591 | return zeroToFiftyNine(value, index); 592 | } 593 | export function fractionalSeconds(value: SourceArray, index: number): number { 594 | return Utils.required(value, index, DIGIT, 1, 12); 595 | } 596 | export function geographyPrefix(value: SourceArray, index: number): number { 597 | return Utils.equals(value, index, 'geography') ? index + 9 : index; 598 | } 599 | export function geometryPrefix(value: SourceArray, index: number): number { 600 | return Utils.equals(value, index, 'geometry') ? index + 8 : index; 601 | } 602 | export function identifierLeadingCharacter(value: number): boolean { 603 | return ALPHA(value) || value === 0x5f; 604 | } 605 | export function identifierCharacter(value: number): boolean { 606 | return identifierLeadingCharacter(value) || DIGIT(value); 607 | } 608 | export function beginObject(value: SourceArray, index: number): number { 609 | let bws = BWS(value, index); 610 | const start = index; 611 | index = bws; 612 | if (Utils.equals(value, index, '{')) { 613 | index++; 614 | } else if (Utils.equals(value, index, '%7B')) { 615 | index += 3; 616 | } 617 | if (index === bws) { 618 | return start; 619 | } 620 | 621 | bws = BWS(value, index); 622 | return bws; 623 | } 624 | export function endObject(value: SourceArray, index: number): number { 625 | let bws = BWS(value, index); 626 | const start = index; 627 | index = bws; 628 | if (Utils.equals(value, index, '}')) { 629 | index++; 630 | } else if (Utils.equals(value, index, '%7D')) { 631 | index += 3; 632 | } 633 | if (index === bws) { 634 | return start; 635 | } 636 | 637 | bws = BWS(value, index); 638 | return bws; 639 | } 640 | export function beginArray(value: SourceArray, index: number): number { 641 | let bws = BWS(value, index); 642 | const start = index; 643 | index = bws; 644 | if (Utils.equals(value, index, '[')) { 645 | index++; 646 | } else if (Utils.equals(value, index, '%5B')) { 647 | index += 3; 648 | } 649 | if (index === bws) { 650 | return start; 651 | } 652 | 653 | bws = BWS(value, index); 654 | return bws; 655 | } 656 | export function endArray(value: SourceArray, index: number): number { 657 | let bws = BWS(value, index); 658 | const start = index; 659 | index = bws; 660 | if (Utils.equals(value, index, ']')) { 661 | index++; 662 | } else if (Utils.equals(value, index, '%5D')) { 663 | index += 3; 664 | } 665 | if (index === bws) { 666 | return start; 667 | } 668 | 669 | bws = BWS(value, index); 670 | return bws; 671 | } 672 | export function quotationMark(value: SourceArray, index: number): number { 673 | if (DQUOTE(value[index])) { 674 | return index + 1; 675 | } 676 | if (Utils.equals(value, index, '%22')) { 677 | return index + 3; 678 | } 679 | return index; 680 | } 681 | export function nameSeparator(value: SourceArray, index: number): number { 682 | let bws = BWS(value, index); 683 | const start = index; 684 | index = bws; 685 | const colon = COLON(value, index); 686 | if (!colon) { 687 | return start; 688 | } 689 | index = colon; 690 | bws = BWS(value, index); 691 | return bws; 692 | } 693 | export function valueSeparator(value: SourceArray, index: number): number { 694 | let bws = BWS(value, index); 695 | const start = index; 696 | index = bws; 697 | const comma = COMMA(value, index); 698 | if (!comma) { 699 | return start; 700 | } 701 | index = comma; 702 | bws = BWS(value, index); 703 | return bws; 704 | } 705 | export function escape(value: SourceArray, index: number): number { 706 | if (Utils.equals(value, index, '\\')) { 707 | return index + 1; 708 | } 709 | if (Utils.equals(value, index, '%5C')) { 710 | return index + 3; 711 | } 712 | return index; 713 | } 714 | -------------------------------------------------------------------------------- /src/odataUri.ts: -------------------------------------------------------------------------------- 1 | import * as Lexer from './lexer'; 2 | import * as Query from './query'; 3 | import * as ResourcePath from './resourcePath'; 4 | import { SourceArray } from './utils'; 5 | 6 | export function odataUri( 7 | value: SourceArray, 8 | index: number, 9 | metadataContext?: any 10 | ): Lexer.Token { 11 | let resource = ResourcePath.resourcePath(value, index, metadataContext); 12 | while (!resource && index < value.length) { 13 | while (value[++index] !== 0x2f && index < value.length) {} 14 | resource = ResourcePath.resourcePath(value, index, metadataContext); 15 | } 16 | if (!resource) { 17 | return; 18 | } 19 | const start = index; 20 | index = resource.next; 21 | metadataContext = resource.metadata; 22 | 23 | let query; 24 | if (value[index] === 0x3f) { 25 | query = Query.queryOptions(value, index + 1, metadataContext); 26 | if (!query) { 27 | return; 28 | } 29 | index = query.next; 30 | delete resource.metadata; 31 | } 32 | 33 | return Lexer.tokenize( 34 | value, 35 | start, 36 | index, 37 | { resource, query }, 38 | Lexer.TokenType.ODataUri, 39 | { metadata: metadataContext } 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /src/parser.ts: -------------------------------------------------------------------------------- 1 | import * as Expressions from './expressions'; 2 | import * as ArrayOrObject from './json'; 3 | import * as Lexer from './lexer'; 4 | import * as ODataUri from './odataUri'; 5 | import * as PrimitiveLiteral from './primitiveLiteral'; 6 | import * as Query from './query'; 7 | import * as ResourcePath from './resourcePath'; 8 | import { QueryOptionsToken } from './token'; 9 | 10 | export const parserFactory = function(fn) { 11 | return function(source, options) { 12 | options = options || {}; 13 | const raw = new Uint16Array(source.length); 14 | const pos = 0; 15 | for (let i = 0; i < source.length; i++) { 16 | raw[i] = source.charCodeAt(i); 17 | } 18 | const result = fn(raw, pos, options.metadata); 19 | if (!result) { 20 | throw new Error(`Fail at ${pos}`); 21 | } 22 | if (result.next < raw.length) { 23 | throw new Error(`Unexpected character at ${result.next}`); 24 | } 25 | return result; 26 | }; 27 | }; 28 | 29 | /** 30 | * odata uri parser 31 | */ 32 | export class Parser { 33 | /** 34 | * parser ast node with full odata uri 35 | * 36 | * @param source 37 | * @param options 38 | */ 39 | odataUri(source: string, options?: any): Lexer.Token { 40 | return parserFactory(ODataUri.odataUri)(source, options); 41 | } 42 | resourcePath(source: string, options?: any): Lexer.Token { 43 | return parserFactory(ResourcePath.resourcePath)(source, options); 44 | } 45 | query(source: string, options?: any): QueryOptionsToken { 46 | return parserFactory(Query.queryOptions)(source, options); 47 | } 48 | filter(source: string, options?: any): Lexer.Token { 49 | return parserFactory(Expressions.boolCommonExpr)(source, options); 50 | } 51 | keys(source: string, options?: any): Lexer.Token { 52 | return parserFactory(Expressions.keyPredicate)(source, options); 53 | } 54 | literal(source: string, options?: any): Lexer.Token { 55 | return parserFactory(PrimitiveLiteral.primitiveLiteral)(source, options); 56 | } 57 | arrayOrObject(source: string, index?: number): Lexer.Token { 58 | return parserFactory(ArrayOrObject.arrayOrObject)(source, index); 59 | } 60 | } 61 | 62 | export function odataUri(source: string, options?: any): Lexer.Token { 63 | return parserFactory(ODataUri.odataUri)(source, options); 64 | } 65 | export function resourcePath(source: string, options?: any): Lexer.Token { 66 | return parserFactory(ResourcePath.resourcePath)(source, options); 67 | } 68 | export function query(source: string, options?: any): Lexer.Token { 69 | return parserFactory(Query.queryOptions)(source, options); 70 | } 71 | export function filter(source: string, options?: any): Lexer.Token { 72 | return parserFactory(Expressions.boolCommonExpr)(source, options); 73 | } 74 | export function keys(source: string, options?: any): Lexer.Token { 75 | return parserFactory(Expressions.keyPredicate)(source, options); 76 | } 77 | export function literal(source: string, options?: any): Lexer.Token { 78 | return parserFactory(PrimitiveLiteral.primitiveLiteral)(source, options); 79 | } 80 | 81 | export * from './types'; 82 | -------------------------------------------------------------------------------- /src/resourcePath.ts: -------------------------------------------------------------------------------- 1 | import * as Expressions from './expressions'; 2 | import * as Lexer from './lexer'; 3 | import * as NameOrIdentifier from './nameOrIdentifier'; 4 | import * as PrimitiveLiteral from './primitiveLiteral'; 5 | import Utils, { SourceArray } from './utils'; 6 | 7 | export function resourcePath( 8 | value: SourceArray, 9 | index: number, 10 | metadataContext?: any 11 | ): Lexer.Token { 12 | if (value[index] === 0x2f) { 13 | index++; 14 | } 15 | const token = 16 | batch(value, index) || 17 | entity(value, index, metadataContext) || 18 | metadata(value, index); 19 | if (token) { 20 | return token; 21 | } 22 | 23 | const resource = 24 | NameOrIdentifier.entitySetName(value, index, metadataContext) || 25 | functionImportCall(value, index, metadataContext) || 26 | crossjoin(value, index) || 27 | all(value, index) || 28 | actionImportCall(value, index, metadataContext) || 29 | NameOrIdentifier.singletonEntity(value, index); 30 | 31 | if (!resource) { 32 | return; 33 | } 34 | const start = index; 35 | index = resource.next; 36 | let navigation: Lexer.Token; 37 | 38 | switch (resource.type) { 39 | case Lexer.TokenType.EntitySetName: 40 | navigation = collectionNavigation( 41 | value, 42 | resource.next, 43 | resource.metadata 44 | ); 45 | metadataContext = resource.metadata; 46 | delete resource.metadata; 47 | break; 48 | case Lexer.TokenType.EntityCollectionFunctionImportCall: 49 | navigation = collectionNavigation( 50 | value, 51 | resource.next, 52 | resource.value.import.metadata 53 | ); 54 | metadataContext = resource.value.import.metadata; 55 | delete resource.value.import.metadata; 56 | break; 57 | case Lexer.TokenType.SingletonEntity: 58 | navigation = singleNavigation(value, resource.next, resource.metadata); 59 | metadataContext = resource.metadata; 60 | delete resource.metadata; 61 | break; 62 | case Lexer.TokenType.EntityFunctionImportCall: 63 | navigation = singleNavigation( 64 | value, 65 | resource.next, 66 | resource.value.import.metadata 67 | ); 68 | metadataContext = resource.value.import.metadata; 69 | delete resource.value.import.metadata; 70 | break; 71 | case Lexer.TokenType.ComplexCollectionFunctionImportCall: 72 | case Lexer.TokenType.PrimitiveCollectionFunctionImportCall: 73 | navigation = collectionPath( 74 | value, 75 | resource.next, 76 | resource.value.import.metadata 77 | ); 78 | metadataContext = resource.value.import.metadata; 79 | delete resource.value.import.metadata; 80 | break; 81 | case Lexer.TokenType.ComplexFunctionImportCall: 82 | navigation = complexPath( 83 | value, 84 | resource.next, 85 | resource.value.import.metadata 86 | ); 87 | metadataContext = resource.value.import.metadata; 88 | delete resource.value.import.metadata; 89 | break; 90 | case Lexer.TokenType.PrimitiveFunctionImportCall: 91 | navigation = singlePath( 92 | value, 93 | resource.next, 94 | resource.value.import.metadata 95 | ); 96 | metadataContext = resource.value.import.metadata; 97 | delete resource.value.import.metadata; 98 | break; 99 | } 100 | 101 | if (navigation) { 102 | index = navigation.next; 103 | } 104 | if (value[index] === 0x2f) { 105 | index++; 106 | } 107 | if (resource) { 108 | return Lexer.tokenize( 109 | value, 110 | start, 111 | index, 112 | { resource, navigation }, 113 | Lexer.TokenType.ResourcePath, 114 | navigation || { metadata: metadataContext } 115 | ); 116 | } 117 | } 118 | 119 | export function batch(value: SourceArray, index: number): Lexer.Token { 120 | if (Utils.equals(value, index, '$batch')) { 121 | return Lexer.tokenize( 122 | value, 123 | index, 124 | index + 6, 125 | '$batch', 126 | Lexer.TokenType.Batch 127 | ); 128 | } 129 | } 130 | 131 | export function entity( 132 | value: SourceArray, 133 | index: number, 134 | metadataContext?: any 135 | ): Lexer.Token { 136 | if (Utils.equals(value, index, '$entity')) { 137 | const start = index; 138 | index += 7; 139 | 140 | let name; 141 | if (value[index] === 0x2f) { 142 | name = NameOrIdentifier.qualifiedEntityTypeName( 143 | value, 144 | index + 1, 145 | metadataContext 146 | ); 147 | if (!name) { 148 | return; 149 | } 150 | index = name.next; 151 | } 152 | 153 | return Lexer.tokenize( 154 | value, 155 | start, 156 | index, 157 | name || '$entity', 158 | Lexer.TokenType.Entity 159 | ); 160 | } 161 | } 162 | 163 | export function metadata(value: SourceArray, index: number): Lexer.Token { 164 | if (Utils.equals(value, index, '$metadata')) { 165 | return Lexer.tokenize( 166 | value, 167 | index, 168 | index + 9, 169 | '$metadata', 170 | Lexer.TokenType.Metadata 171 | ); 172 | } 173 | } 174 | 175 | export function collectionNavigation( 176 | value: SourceArray, 177 | index: number, 178 | metadataContext?: any 179 | ): Lexer.Token { 180 | const start = index; 181 | let name; 182 | if (value[index] === 0x2f) { 183 | name = NameOrIdentifier.qualifiedEntityTypeName( 184 | value, 185 | index + 1, 186 | metadataContext 187 | ); 188 | if (name) { 189 | index = name.next; 190 | metadataContext = name.value.metadata; 191 | delete name.value.metadata; 192 | } 193 | } 194 | 195 | const path = collectionNavigationPath(value, index, metadataContext); 196 | if (path) { 197 | index = path.next; 198 | } 199 | 200 | if (!name && !path) { 201 | return; 202 | } 203 | 204 | return Lexer.tokenize( 205 | value, 206 | start, 207 | index, 208 | { name, path }, 209 | Lexer.TokenType.CollectionNavigation, 210 | path || name 211 | ); 212 | } 213 | 214 | export function collectionNavigationPath( 215 | value: SourceArray, 216 | index: number, 217 | metadataContext?: any 218 | ): Lexer.Token { 219 | const start = index; 220 | const token = 221 | collectionPath(value, index, metadataContext) || 222 | Expressions.refExpr(value, index); 223 | if (token) { 224 | return token; 225 | } 226 | 227 | const predicate = Expressions.keyPredicate(value, index, metadataContext); 228 | if (predicate) { 229 | let tokenValue: any = { predicate }; 230 | index = predicate.next; 231 | 232 | const navigation = singleNavigation(value, index, metadataContext); 233 | if (navigation) { 234 | tokenValue = { predicate, navigation }; 235 | index = navigation.next; 236 | } 237 | 238 | return Lexer.tokenize( 239 | value, 240 | start, 241 | index, 242 | tokenValue, 243 | Lexer.TokenType.CollectionNavigationPath, 244 | navigation || { metadata: metadataContext } 245 | ); 246 | } 247 | } 248 | 249 | export function singleNavigation( 250 | value: SourceArray, 251 | index: number, 252 | metadataContext?: any 253 | ): Lexer.Token { 254 | let token = 255 | boundOperation(value, index, false, metadataContext) || 256 | Expressions.refExpr(value, index) || 257 | Expressions.valueExpr(value, index); 258 | if (token) { 259 | return token; 260 | } 261 | 262 | const start = index; 263 | let name; 264 | 265 | if (value[index] === 0x2f) { 266 | name = NameOrIdentifier.qualifiedEntityTypeName( 267 | value, 268 | index + 1, 269 | metadataContext 270 | ); 271 | if (name) { 272 | index = name.next; 273 | metadataContext = name.value.metadata; 274 | delete name.value.metadata; 275 | } 276 | } 277 | 278 | if (value[index] === 0x2f) { 279 | token = propertyPath(value, index + 1, metadataContext); 280 | if (token) { 281 | index = token.next; 282 | } 283 | } 284 | 285 | if (!name && !token) { 286 | return; 287 | } 288 | 289 | return Lexer.tokenize( 290 | value, 291 | start, 292 | index, 293 | { name, path: token }, 294 | Lexer.TokenType.SingleNavigation, 295 | token 296 | ); 297 | } 298 | 299 | export function propertyPath( 300 | value: SourceArray, 301 | index: number, 302 | metadataContext?: any 303 | ): Lexer.Token { 304 | const token = 305 | NameOrIdentifier.entityColNavigationProperty( 306 | value, 307 | index, 308 | metadataContext 309 | ) || 310 | NameOrIdentifier.entityNavigationProperty(value, index, metadataContext) || 311 | NameOrIdentifier.complexColProperty(value, index, metadataContext) || 312 | NameOrIdentifier.complexProperty(value, index, metadataContext) || 313 | NameOrIdentifier.primitiveColProperty(value, index, metadataContext) || 314 | NameOrIdentifier.primitiveProperty(value, index, metadataContext) || 315 | NameOrIdentifier.streamProperty(value, index, metadataContext); 316 | 317 | if (!token) { 318 | return; 319 | } 320 | const start = index; 321 | index = token.next; 322 | 323 | let navigation; 324 | switch (token.type) { 325 | case Lexer.TokenType.EntityCollectionNavigationProperty: 326 | navigation = collectionNavigation(value, index, token.metadata); 327 | delete token.metadata; 328 | break; 329 | case Lexer.TokenType.EntityNavigationProperty: 330 | navigation = singleNavigation(value, index, token.metadata); 331 | delete token.metadata; 332 | break; 333 | case Lexer.TokenType.ComplexCollectionProperty: 334 | navigation = collectionPath(value, index, token.metadata); 335 | delete token.metadata; 336 | break; 337 | case Lexer.TokenType.ComplexProperty: 338 | navigation = complexPath(value, index, token.metadata); 339 | delete token.metadata; 340 | break; 341 | case Lexer.TokenType.PrimitiveCollectionProperty: 342 | navigation = collectionPath(value, index, token.metadata); 343 | delete token.metadata; 344 | break; 345 | case Lexer.TokenType.PrimitiveKeyProperty: 346 | case Lexer.TokenType.PrimitiveProperty: 347 | navigation = singlePath(value, index, token.metadata); 348 | delete token.metadata; 349 | break; 350 | case Lexer.TokenType.StreamProperty: 351 | navigation = boundOperation(value, index, token.metadata); 352 | delete token.metadata; 353 | break; 354 | } 355 | 356 | if (navigation) { 357 | index = navigation.next; 358 | } 359 | 360 | return Lexer.tokenize( 361 | value, 362 | start, 363 | index, 364 | { path: token, navigation }, 365 | Lexer.TokenType.PropertyPath, 366 | navigation 367 | ); 368 | } 369 | 370 | export function collectionPath( 371 | value: SourceArray, 372 | index: number, 373 | metadataContext?: any 374 | ): Lexer.Token { 375 | return ( 376 | Expressions.countExpr(value, index) || 377 | boundOperation(value, index, true, metadataContext) 378 | ); 379 | } 380 | 381 | export function singlePath( 382 | value: SourceArray, 383 | index: number, 384 | metadataContext?: any 385 | ): Lexer.Token { 386 | return ( 387 | Expressions.valueExpr(value, index) || 388 | boundOperation(value, index, false, metadataContext) 389 | ); 390 | } 391 | 392 | export function complexPath( 393 | value: SourceArray, 394 | index: number, 395 | metadataContext?: any 396 | ): Lexer.Token { 397 | const start = index; 398 | let name, token; 399 | if (value[index] === 0x2f) { 400 | name = NameOrIdentifier.qualifiedComplexTypeName( 401 | value, 402 | index + 1, 403 | metadataContext 404 | ); 405 | if (name) { 406 | index = name.next; 407 | } 408 | } 409 | 410 | if (value[index] === 0x2f) { 411 | token = propertyPath(value, index + 1, metadataContext); 412 | if (!token) { 413 | return; 414 | } 415 | index = token.next; 416 | } else { 417 | token = boundOperation(value, index, false, metadataContext); 418 | } 419 | 420 | if (!name && !token) { 421 | return; 422 | } 423 | 424 | return Lexer.tokenize( 425 | value, 426 | start, 427 | index, 428 | { name, path: token }, 429 | Lexer.TokenType.ComplexPath, 430 | token 431 | ); 432 | } 433 | 434 | export function boundOperation( 435 | value: SourceArray, 436 | index: number, 437 | isCollection: boolean, 438 | metadataContext?: any 439 | ): Lexer.Token { 440 | if (value[index] !== 0x2f) { 441 | return; 442 | } 443 | const start = index; 444 | index++; 445 | 446 | const operation = 447 | boundEntityColFuncCall(value, index, isCollection, metadataContext) || 448 | boundEntityFuncCall(value, index, isCollection, metadataContext) || 449 | boundComplexColFuncCall(value, index, isCollection, metadataContext) || 450 | boundComplexFuncCall(value, index, isCollection, metadataContext) || 451 | boundPrimitiveColFuncCall(value, index, isCollection, metadataContext) || 452 | boundPrimitiveFuncCall(value, index, isCollection, metadataContext) || 453 | boundActionCall(value, index, isCollection, metadataContext); 454 | if (!operation) { 455 | return; 456 | } 457 | index = operation.next; 458 | 459 | let name, navigation; 460 | switch (operation.type) { 461 | case Lexer.TokenType.BoundActionCall: 462 | break; 463 | case Lexer.TokenType.BoundEntityCollectionFunctionCall: 464 | navigation = collectionNavigation( 465 | value, 466 | index, 467 | operation.value.call.metadata 468 | ); 469 | delete operation.metadata; 470 | break; 471 | case Lexer.TokenType.BoundEntityFunctionCall: 472 | navigation = singleNavigation( 473 | value, 474 | index, 475 | operation.value.call.metadata 476 | ); 477 | delete operation.metadata; 478 | break; 479 | case Lexer.TokenType.BoundComplexCollectionFunctionCall: 480 | if (value[index] === 0x2f) { 481 | name = NameOrIdentifier.qualifiedComplexTypeName( 482 | value, 483 | index + 1, 484 | operation.value.call.metadata 485 | ); 486 | if (name) { 487 | index = name.next; 488 | } 489 | } 490 | navigation = collectionPath(value, index, operation.value.call.metadata); 491 | delete operation.metadata; 492 | break; 493 | case Lexer.TokenType.BoundComplexFunctionCall: 494 | navigation = complexPath(value, index, operation.value.call.metadata); 495 | delete operation.metadata; 496 | break; 497 | case Lexer.TokenType.BoundPrimitiveCollectionFunctionCall: 498 | navigation = collectionPath(value, index, operation.value.call.metadata); 499 | delete operation.metadata; 500 | break; 501 | case Lexer.TokenType.BoundPrimitiveFunctionCall: 502 | navigation = singlePath(value, index, operation.value.call.metadata); 503 | delete operation.metadata; 504 | break; 505 | } 506 | 507 | if (navigation) { 508 | index = navigation.next; 509 | } 510 | 511 | return Lexer.tokenize( 512 | value, 513 | start, 514 | index, 515 | { operation, name, navigation }, 516 | Lexer.TokenType.BoundOperation, 517 | navigation 518 | ); 519 | } 520 | 521 | export function boundActionCall( 522 | value: SourceArray, 523 | index: number, 524 | isCollection: boolean, 525 | metadataContext?: any 526 | ): Lexer.Token { 527 | const namespaceNext = NameOrIdentifier.namespace(value, index); 528 | if (namespaceNext === index) { 529 | return; 530 | } 531 | const start = index; 532 | index = namespaceNext; 533 | 534 | if (value[index] !== 0x2e) { 535 | return; 536 | } 537 | index++; 538 | 539 | const action = NameOrIdentifier.action( 540 | value, 541 | index, 542 | isCollection, 543 | metadataContext 544 | ); 545 | if (!action) { 546 | return; 547 | } 548 | action.value.namespace = Utils.stringify(value, start, namespaceNext); 549 | 550 | return Lexer.tokenize( 551 | value, 552 | start, 553 | action.next, 554 | action, 555 | Lexer.TokenType.BoundActionCall, 556 | action 557 | ); 558 | } 559 | 560 | export function boundFunctionCall( 561 | value: SourceArray, 562 | index: number, 563 | odataFunction: Function, 564 | tokenType: Lexer.TokenType, 565 | isCollection: boolean, 566 | metadataContext?: any 567 | ): Lexer.Token { 568 | const namespaceNext = NameOrIdentifier.namespace(value, index); 569 | if (namespaceNext === index) { 570 | return; 571 | } 572 | const start = index; 573 | index = namespaceNext; 574 | 575 | if (value[index] !== 0x2e) { 576 | return; 577 | } 578 | index++; 579 | 580 | const call = odataFunction(value, index, isCollection, metadataContext); 581 | if (!call) { 582 | return; 583 | } 584 | call.value.namespace = Utils.stringify(value, start, namespaceNext); 585 | index = call.next; 586 | 587 | const params = functionParameters(value, index); 588 | if (!params) { 589 | return; 590 | } 591 | index = params.next; 592 | 593 | return Lexer.tokenize(value, start, index, { call, params }, tokenType, call); 594 | } 595 | 596 | export function boundEntityFuncCall( 597 | value: SourceArray, 598 | index: number, 599 | isCollection: boolean, 600 | metadataContext?: any 601 | ): Lexer.Token { 602 | return boundFunctionCall( 603 | value, 604 | index, 605 | NameOrIdentifier.entityFunction, 606 | Lexer.TokenType.BoundEntityFunctionCall, 607 | isCollection, 608 | metadataContext 609 | ); 610 | } 611 | export function boundEntityColFuncCall( 612 | value: SourceArray, 613 | index: number, 614 | isCollection: boolean, 615 | metadataContext?: any 616 | ): Lexer.Token { 617 | return boundFunctionCall( 618 | value, 619 | index, 620 | NameOrIdentifier.entityColFunction, 621 | Lexer.TokenType.BoundEntityCollectionFunctionCall, 622 | isCollection, 623 | metadataContext 624 | ); 625 | } 626 | export function boundComplexFuncCall( 627 | value: SourceArray, 628 | index: number, 629 | isCollection: boolean, 630 | metadataContext?: any 631 | ): Lexer.Token { 632 | return boundFunctionCall( 633 | value, 634 | index, 635 | NameOrIdentifier.complexFunction, 636 | Lexer.TokenType.BoundComplexFunctionCall, 637 | isCollection, 638 | metadataContext 639 | ); 640 | } 641 | export function boundComplexColFuncCall( 642 | value: SourceArray, 643 | index: number, 644 | isCollection: boolean, 645 | metadataContext?: any 646 | ): Lexer.Token { 647 | return boundFunctionCall( 648 | value, 649 | index, 650 | NameOrIdentifier.complexColFunction, 651 | Lexer.TokenType.BoundComplexCollectionFunctionCall, 652 | isCollection, 653 | metadataContext 654 | ); 655 | } 656 | export function boundPrimitiveFuncCall( 657 | value: SourceArray, 658 | index: number, 659 | isCollection: boolean, 660 | metadataContext?: any 661 | ): Lexer.Token { 662 | return boundFunctionCall( 663 | value, 664 | index, 665 | NameOrIdentifier.primitiveFunction, 666 | Lexer.TokenType.BoundPrimitiveFunctionCall, 667 | isCollection, 668 | metadataContext 669 | ); 670 | } 671 | export function boundPrimitiveColFuncCall( 672 | value: SourceArray, 673 | index: number, 674 | isCollection: boolean, 675 | metadataContext?: any 676 | ): Lexer.Token { 677 | return boundFunctionCall( 678 | value, 679 | index, 680 | NameOrIdentifier.primitiveColFunction, 681 | Lexer.TokenType.BoundPrimitiveCollectionFunctionCall, 682 | isCollection, 683 | metadataContext 684 | ); 685 | } 686 | 687 | export function actionImportCall( 688 | value: SourceArray, 689 | index: number, 690 | metadataContext?: any 691 | ): Lexer.Token { 692 | const action = NameOrIdentifier.actionImport(value, index, metadataContext); 693 | if (action) { 694 | return Lexer.tokenize( 695 | value, 696 | index, 697 | action.next, 698 | action, 699 | Lexer.TokenType.ActionImportCall, 700 | action 701 | ); 702 | } 703 | } 704 | 705 | export function functionImportCall( 706 | value: SourceArray, 707 | index: number, 708 | metadataContext?: any 709 | ): Lexer.Token { 710 | const fnImport = 711 | NameOrIdentifier.entityFunctionImport(value, index, metadataContext) || 712 | NameOrIdentifier.entityColFunctionImport(value, index, metadataContext) || 713 | NameOrIdentifier.complexFunctionImport(value, index, metadataContext) || 714 | NameOrIdentifier.complexColFunctionImport(value, index, metadataContext) || 715 | NameOrIdentifier.primitiveFunctionImport(value, index, metadataContext) || 716 | NameOrIdentifier.primitiveColFunctionImport(value, index, metadataContext); 717 | 718 | if (!fnImport) { 719 | return; 720 | } 721 | const start = index; 722 | index = fnImport.next; 723 | 724 | const params = functionParameters(value, index); 725 | if (!params) { 726 | return; 727 | } 728 | index = params.next; 729 | 730 | return Lexer.tokenize( 731 | value, 732 | start, 733 | index, 734 | { import: fnImport, params: params.value }, 735 | `${fnImport.type}Call`, 736 | fnImport 737 | ); 738 | } 739 | 740 | export function functionParameters( 741 | value: SourceArray, 742 | index: number, 743 | metadataContext?: any 744 | ): Lexer.Token { 745 | const open = Lexer.OPEN(value, index); 746 | if (!open) { 747 | return; 748 | } 749 | const start = index; 750 | index = open; 751 | 752 | const params = []; 753 | let token = functionParameter(value, index); 754 | while (token) { 755 | params.push(token); 756 | index = token.next; 757 | 758 | const comma = Lexer.COMMA(value, index); 759 | if (comma) { 760 | index = comma; 761 | token = functionParameter(value, index); 762 | if (!token) { 763 | return; 764 | } 765 | } else { 766 | break; 767 | } 768 | } 769 | 770 | const close = Lexer.CLOSE(value, index); 771 | if (!close) { 772 | return; 773 | } 774 | index = close; 775 | 776 | return Lexer.tokenize( 777 | value, 778 | start, 779 | index, 780 | params, 781 | Lexer.TokenType.FunctionParameters 782 | ); 783 | } 784 | 785 | export function functionParameter( 786 | value: SourceArray, 787 | index: number, 788 | metadataContext?: any 789 | ): Lexer.Token { 790 | const name = Expressions.parameterName(value, index); 791 | if (!name) { 792 | return; 793 | } 794 | const start = index; 795 | index = name.next; 796 | 797 | const eq = Lexer.EQ(value, index); 798 | if (!eq) { 799 | return; 800 | } 801 | index = eq; 802 | 803 | const token = 804 | Expressions.parameterAlias(value, index) || 805 | PrimitiveLiteral.primitiveLiteral(value, index); 806 | 807 | if (!token) { 808 | return; 809 | } 810 | index = token.next; 811 | 812 | return Lexer.tokenize( 813 | value, 814 | start, 815 | index, 816 | { name, value: token }, 817 | Lexer.TokenType.FunctionParameter 818 | ); 819 | } 820 | 821 | export function crossjoin( 822 | value: SourceArray, 823 | index: number, 824 | metadataContext?: any 825 | ): Lexer.Token { 826 | if (!Utils.equals(value, index, '$crossjoin')) { 827 | return; 828 | } 829 | const start = index; 830 | index += 10; 831 | 832 | const open = Lexer.OPEN(value, index); 833 | if (!open) { 834 | return; 835 | } 836 | index = open; 837 | 838 | const names = []; 839 | let token = NameOrIdentifier.entitySetName(value, index, metadataContext); 840 | if (!token) { 841 | return; 842 | } 843 | 844 | while (token) { 845 | names.push(token); 846 | index = token.next; 847 | 848 | const comma = Lexer.COMMA(value, index); 849 | if (comma) { 850 | index = comma; 851 | token = NameOrIdentifier.entitySetName(value, index, metadataContext); 852 | if (!token) { 853 | return; 854 | } 855 | } else { 856 | break; 857 | } 858 | } 859 | 860 | const close = Lexer.CLOSE(value, index); 861 | if (!close) { 862 | return; 863 | } 864 | 865 | return Lexer.tokenize( 866 | value, 867 | start, 868 | index, 869 | { names }, 870 | Lexer.TokenType.Crossjoin 871 | ); 872 | } 873 | 874 | export function all(value: SourceArray, index: number): Lexer.Token { 875 | if (Utils.equals(value, index, '$all')) { 876 | return Lexer.tokenize( 877 | value, 878 | index, 879 | index + 4, 880 | '$all', 881 | Lexer.TokenType.AllResource 882 | ); 883 | } 884 | } 885 | -------------------------------------------------------------------------------- /src/token.ts: -------------------------------------------------------------------------------- 1 | import { PrimitiveTypeEnum } from '@odata/metadata'; 2 | import { Token, TokenType } from './lexer'; 3 | 4 | export interface QueryOptionsToken extends Token { 5 | type: TokenType.QueryOptions; 6 | value: { 7 | options: Token[]; 8 | }; 9 | } 10 | 11 | export interface CustomQueryOptionToken extends Token { 12 | type: TokenType.CustomQueryOption; 13 | value: { 14 | key: string; 15 | value: any; 16 | }; 17 | } 18 | 19 | export interface LiteralToken extends Token { 20 | type: TokenType.Literal; 21 | /** 22 | * edm type 23 | */ 24 | value: PrimitiveTypeEnum; 25 | } 26 | 27 | export interface SkipToken extends Token { 28 | type: TokenType.Skip; 29 | value: LiteralToken; 30 | } 31 | 32 | export interface TopToken extends Token { 33 | type: TokenType.Top; 34 | value: LiteralToken; 35 | } 36 | 37 | export interface FormatToken extends Token { 38 | type: TokenType.Format; 39 | value: { format: string }; 40 | } 41 | 42 | export interface FilterToken extends Token { 43 | type: TokenType.Filter; 44 | value: EqualsExpressionToken | OrExpressionToken | AndExpressionToken; 45 | } 46 | 47 | export interface ExpandToken extends Token { 48 | type: TokenType.Expand; 49 | value: { 50 | items: Token[]; 51 | }; 52 | } 53 | 54 | export interface SearchToken extends Token { 55 | type: TokenType.Search; 56 | value: SearchWordToken; 57 | } 58 | 59 | export interface SearchWordToken extends Token { 60 | type: TokenType.SearchWord; 61 | value: string; 62 | } 63 | 64 | export interface LeftRightExpressionToken extends Token { 65 | value: { 66 | left: Token; 67 | right: Token; 68 | }; 69 | } 70 | 71 | export interface MemberExpressionToken extends Token { 72 | type: TokenType.MemberExpression; 73 | value: Token; 74 | } 75 | 76 | export interface FirstMemberExpressionToken extends Token { 77 | type: TokenType.FirstMemberExpression; 78 | value: Token; 79 | } 80 | 81 | export interface EqualsExpressionToken extends LeftRightExpressionToken { 82 | type: TokenType.EqualsExpression; 83 | } 84 | 85 | export interface AndExpressionToken extends LeftRightExpressionToken { 86 | type: TokenType.AndExpression; 87 | } 88 | 89 | export interface OrExpressionToken extends LeftRightExpressionToken { 90 | type: TokenType.OrExpression; 91 | } 92 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { Token } from './lexer'; 2 | 3 | export interface QueryOptionsNode extends Token { 4 | value: Array; 5 | } 6 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { map } from '@newdash/newdash/map'; 2 | import { Token, TokenType } from './lexer'; 3 | import { 4 | CustomQueryOptionToken, 5 | ExpandToken, 6 | FormatToken, 7 | SearchToken, 8 | SkipToken, 9 | TopToken 10 | } from './token'; 11 | import { createTraverser } from './visitor'; 12 | 13 | export type SourceArray = number[] | Uint16Array; 14 | 15 | export function stringify( 16 | value: SourceArray, 17 | index: number, 18 | next: number 19 | ): string { 20 | return map(value.slice(index, next), (ch) => String.fromCharCode(ch)).join( 21 | '' 22 | ); 23 | } 24 | 25 | export function is(value: number, compare: string) { 26 | for (let i = 0; i < compare.length; i++) { 27 | if (value === compare.charCodeAt(i)) { 28 | return true; 29 | } 30 | } 31 | 32 | return false; 33 | } 34 | 35 | export function equals(value: SourceArray, index: number, compare: string) { 36 | let i = 0; 37 | while (value[index + i] === compare.charCodeAt(i) && i < compare.length) { 38 | i++; 39 | } 40 | return i === compare.length ? i : 0; 41 | } 42 | 43 | export function required( 44 | value: SourceArray, 45 | index: number, 46 | comparer: Function, 47 | min?: number, 48 | max?: number 49 | ) { 50 | let i = 0; 51 | 52 | max = max || value.length - index; 53 | while (i < max && comparer(value[index + i])) { 54 | i++; 55 | } 56 | 57 | return i >= (min || 0) && i <= max ? index + i : 0; 58 | } 59 | 60 | export function isType( 61 | node: Token, 62 | type: TokenType.CustomQueryOption 63 | ): node is CustomQueryOptionToken; 64 | export function isType(node: Token, type: TokenType.Skip): node is SkipToken; 65 | export function isType(node: Token, type: TokenType.Top): node is TopToken; 66 | export function isType(node: Token, type: TokenType): boolean { 67 | return node?.type == type; 68 | } 69 | 70 | /** 71 | * find one node in ast node by type 72 | * 73 | * @param node 74 | * @param type 75 | */ 76 | export function findOne(node: Token, type: TokenType.Top): TopToken; 77 | export function findOne(node: Token, type: TokenType.Skip): SkipToken; 78 | export function findOne(node: Token, type: TokenType.Expand): ExpandToken; 79 | export function findOne(node: Token, type: TokenType.Format): FormatToken; 80 | export function findOne(node: Token, type: TokenType.Search): SearchToken; 81 | export function findOne(node: Token, type: TokenType): Token; 82 | export function findOne(node: Token, type: any): Token { 83 | let rt: Token; 84 | createTraverser({ 85 | [type]: (v: Token) => { 86 | rt = v; 87 | } 88 | })(node); 89 | return rt; 90 | } 91 | 92 | /** 93 | * find all nodes in ast node by type 94 | * 95 | * @param node 96 | * @param type 97 | */ 98 | export function findAll(node: Token, type: TokenType): Array { 99 | const rt: Array = []; 100 | createTraverser({ 101 | [type]: (v: Token) => { 102 | rt.push(v); 103 | } 104 | })(node); 105 | return rt; 106 | } 107 | 108 | export default { stringify, is, equals, required }; 109 | -------------------------------------------------------------------------------- /src/visitor.ts: -------------------------------------------------------------------------------- 1 | import { forEach } from '@newdash/newdash/forEach'; 2 | import { isArray } from '@newdash/newdash/isArray'; 3 | import { isPlainObject } from '@newdash/newdash/isPlainObject'; 4 | import { Token, TokenType } from './lexer'; 5 | 6 | /** 7 | * AST Traverser 8 | */ 9 | export type Traverser = { 10 | [key in TokenType]?: (token: Token, parent?: Token) => void; 11 | }; 12 | 13 | /** 14 | * AST Visitor 15 | * 16 | * @alias Traverser 17 | */ 18 | export type Visitor = Traverser; 19 | 20 | /** 21 | * Traverse AST with traverser 22 | * 23 | * @param traverser 24 | * @param node 25 | */ 26 | export function traverseAst( 27 | traverser: Traverser, 28 | node: Token, 29 | parent?: Token 30 | ): void { 31 | if (node instanceof Token) { 32 | if (node?.type in traverser) { 33 | traverser[node?.type](node, parent); 34 | } 35 | } 36 | 37 | if (isArray(node?.value) || isPlainObject(node?.value)) { 38 | forEach(node?.value, (item) => { 39 | if (item instanceof Token) { 40 | traverseAst(traverser, item, node); 41 | } 42 | }); 43 | } 44 | 45 | if (isArray(node?.value?.options)) { 46 | forEach(node?.value?.options, (item) => { 47 | if (item instanceof Token) { 48 | traverseAst(traverser, item, node); 49 | } 50 | }); 51 | } 52 | 53 | if (isArray(node?.value?.items)) { 54 | forEach(node?.value?.items, (item) => { 55 | if (item instanceof Token) { 56 | traverseAst(traverser, item, node); 57 | } 58 | }); 59 | } 60 | 61 | if (node?.value instanceof Token) { 62 | traverseAst(traverser, node?.value, node); 63 | } 64 | } 65 | 66 | /** 67 | * Traverse AST with traverser (Deep First) 68 | * 69 | * @param traverser 70 | * @param node 71 | * @param parent 72 | */ 73 | export function traverseAstDeepFirst( 74 | traverser: Traverser, 75 | node: Token, 76 | parent?: Token 77 | ): void { 78 | if (isArray(node?.value) || isPlainObject(node?.value)) { 79 | forEach(node?.value, (item) => { 80 | if (item instanceof Token) { 81 | traverseAstDeepFirst(traverser, item, node); 82 | } 83 | }); 84 | } 85 | 86 | if (node?.value instanceof Token) { 87 | traverseAstDeepFirst(traverser, node?.value, node); 88 | } 89 | 90 | if (isArray(node?.value?.options)) { 91 | forEach(node?.value?.options, (item) => { 92 | if (item instanceof Token) { 93 | traverseAstDeepFirst(traverser, item, node); 94 | } 95 | }); 96 | } 97 | 98 | if (isArray(node?.value?.items)) { 99 | forEach(node?.value?.items, (item) => { 100 | if (item instanceof Token) { 101 | traverseAstDeepFirst(traverser, item, node); 102 | } 103 | }); 104 | } 105 | 106 | if (node instanceof Token) { 107 | if (node?.type in traverser) { 108 | traverser[node.type](node, parent); 109 | } 110 | } 111 | } 112 | 113 | export function createTraverser(traverser: Traverser, deepFirst = false) { 114 | return (node: Token): void => { 115 | if (deepFirst) { 116 | traverseAstDeepFirst(traverser, node); 117 | } else { 118 | traverseAst(traverser, node); 119 | } 120 | }; 121 | } 122 | -------------------------------------------------------------------------------- /test/__snapshots__/filter.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Filter Test Suite shuold support filter with double quote 1`] = `"$filter=key eq 'val"'"`; 4 | 5 | exports[`Filter Test Suite shuold support filter with double quote 2`] = ` 6 | Token { 7 | "next": 21, 8 | "position": 0, 9 | "raw": "$filter=key eq 'val"'", 10 | "type": "QueryOptions", 11 | "value": { 12 | "options": [ 13 | Token { 14 | "next": 21, 15 | "position": 0, 16 | "raw": "$filter=key eq 'val"'", 17 | "type": "Filter", 18 | "value": Token { 19 | "next": 21, 20 | "position": 8, 21 | "raw": "key eq 'val"'", 22 | "type": "EqualsExpression", 23 | "value": { 24 | "left": Token { 25 | "next": 11, 26 | "position": 8, 27 | "raw": "key", 28 | "type": "FirstMemberExpression", 29 | "value": Token { 30 | "next": 11, 31 | "position": 8, 32 | "raw": "key", 33 | "type": "MemberExpression", 34 | "value": Token { 35 | "next": 11, 36 | "position": 8, 37 | "raw": "key", 38 | "type": "PropertyPathExpression", 39 | "value": Token { 40 | "next": 11, 41 | "position": 8, 42 | "raw": "key", 43 | "type": "ODataIdentifier", 44 | "value": { 45 | "name": "key", 46 | }, 47 | }, 48 | }, 49 | }, 50 | }, 51 | "right": Token { 52 | "next": 21, 53 | "position": 15, 54 | "raw": "'val"'", 55 | "type": "Literal", 56 | "value": "Edm.String", 57 | }, 58 | }, 59 | }, 60 | }, 61 | ], 62 | }, 63 | } 64 | `; 65 | 66 | exports[`Filter Test Suite shuold support filter with other chars 1`] = `"$filter=key eq 'val$%^&*()_'"`; 67 | 68 | exports[`Filter Test Suite shuold support filter with other chars 2`] = ` 69 | Token { 70 | "next": 28, 71 | "position": 0, 72 | "raw": "$filter=key eq 'val$%^&*()_'", 73 | "type": "QueryOptions", 74 | "value": { 75 | "options": [ 76 | Token { 77 | "next": 28, 78 | "position": 0, 79 | "raw": "$filter=key eq 'val$%^&*()_'", 80 | "type": "Filter", 81 | "value": Token { 82 | "next": 28, 83 | "position": 8, 84 | "raw": "key eq 'val$%^&*()_'", 85 | "type": "EqualsExpression", 86 | "value": { 87 | "left": Token { 88 | "next": 11, 89 | "position": 8, 90 | "raw": "key", 91 | "type": "FirstMemberExpression", 92 | "value": Token { 93 | "next": 11, 94 | "position": 8, 95 | "raw": "key", 96 | "type": "MemberExpression", 97 | "value": Token { 98 | "next": 11, 99 | "position": 8, 100 | "raw": "key", 101 | "type": "PropertyPathExpression", 102 | "value": Token { 103 | "next": 11, 104 | "position": 8, 105 | "raw": "key", 106 | "type": "ODataIdentifier", 107 | "value": { 108 | "name": "key", 109 | }, 110 | }, 111 | }, 112 | }, 113 | }, 114 | "right": Token { 115 | "next": 28, 116 | "position": 15, 117 | "raw": "'val$%^&*()_'", 118 | "type": "Literal", 119 | "value": "Edm.String", 120 | }, 121 | }, 122 | }, 123 | }, 124 | ], 125 | }, 126 | } 127 | `; 128 | 129 | exports[`Filter Test Suite shuold support filter with single quote 1`] = `"$filter=key eq 'T''A First'"`; 130 | 131 | exports[`Filter Test Suite shuold support filter with single quote 2`] = ` 132 | Token { 133 | "next": 27, 134 | "position": 0, 135 | "raw": "$filter=key eq 'T''A First'", 136 | "type": "QueryOptions", 137 | "value": { 138 | "options": [ 139 | Token { 140 | "next": 27, 141 | "position": 0, 142 | "raw": "$filter=key eq 'T''A First'", 143 | "type": "Filter", 144 | "value": Token { 145 | "next": 27, 146 | "position": 8, 147 | "raw": "key eq 'T''A First'", 148 | "type": "EqualsExpression", 149 | "value": { 150 | "left": Token { 151 | "next": 11, 152 | "position": 8, 153 | "raw": "key", 154 | "type": "FirstMemberExpression", 155 | "value": Token { 156 | "next": 11, 157 | "position": 8, 158 | "raw": "key", 159 | "type": "MemberExpression", 160 | "value": Token { 161 | "next": 11, 162 | "position": 8, 163 | "raw": "key", 164 | "type": "PropertyPathExpression", 165 | "value": Token { 166 | "next": 11, 167 | "position": 8, 168 | "raw": "key", 169 | "type": "ODataIdentifier", 170 | "value": { 171 | "name": "key", 172 | }, 173 | }, 174 | }, 175 | }, 176 | }, 177 | "right": Token { 178 | "next": 27, 179 | "position": 15, 180 | "raw": "'T''A First'", 181 | "type": "Literal", 182 | "value": "Edm.String", 183 | }, 184 | }, 185 | }, 186 | }, 187 | ], 188 | }, 189 | } 190 | `; 191 | 192 | exports[`Filter Test Suite shuold support filter without double quote 1`] = `"$filter=key eq 'val'"`; 193 | 194 | exports[`Filter Test Suite shuold support filter without double quote 2`] = ` 195 | Token { 196 | "next": 20, 197 | "position": 0, 198 | "raw": "$filter=key eq 'val'", 199 | "type": "QueryOptions", 200 | "value": { 201 | "options": [ 202 | Token { 203 | "next": 20, 204 | "position": 0, 205 | "raw": "$filter=key eq 'val'", 206 | "type": "Filter", 207 | "value": Token { 208 | "next": 20, 209 | "position": 8, 210 | "raw": "key eq 'val'", 211 | "type": "EqualsExpression", 212 | "value": { 213 | "left": Token { 214 | "next": 11, 215 | "position": 8, 216 | "raw": "key", 217 | "type": "FirstMemberExpression", 218 | "value": Token { 219 | "next": 11, 220 | "position": 8, 221 | "raw": "key", 222 | "type": "MemberExpression", 223 | "value": Token { 224 | "next": 11, 225 | "position": 8, 226 | "raw": "key", 227 | "type": "PropertyPathExpression", 228 | "value": Token { 229 | "next": 11, 230 | "position": 8, 231 | "raw": "key", 232 | "type": "ODataIdentifier", 233 | "value": { 234 | "name": "key", 235 | }, 236 | }, 237 | }, 238 | }, 239 | }, 240 | "right": Token { 241 | "next": 20, 242 | "position": 15, 243 | "raw": "'val'", 244 | "type": "Literal", 245 | "value": "Edm.String", 246 | }, 247 | }, 248 | }, 249 | }, 250 | ], 251 | }, 252 | } 253 | `; 254 | -------------------------------------------------------------------------------- /test/builder/filter.test.ts: -------------------------------------------------------------------------------- 1 | import { filter, literalValues, ODataFilter } from '../../src'; 2 | 3 | describe('OData Query Builder - Filter Test Suite', () => { 4 | 5 | it('should support filter by value/name', () => { 6 | 7 | expect(ODataFilter.New().field('A').eq('a').toString()).toBe("A eq 'a'"); 8 | expect(ODataFilter.New().field('A').eq(literalValues.String('a')).toString()).toBe("A eq 'a'"); 9 | expect(ODataFilter.New().field('A').eq(1).toString()).toBe('A eq 1'); 10 | 11 | }); 12 | 13 | it('should support filter by object', () => { 14 | interface Type { 15 | A: number; 16 | B: string; 17 | } 18 | expect(ODataFilter.New({ A: 1, B: '2' }).toString()).toBe("A eq 1 and B eq '2'"); 19 | 20 | expect(ODataFilter.New({ 21 | A: literalValues.Int16(12), 22 | B: literalValues.String('12') 23 | }).toString()).toBe("A eq 12 and B eq '12'"); 24 | }); 25 | 26 | it('should support filter alias', () => { 27 | expect(filter({ A: 1 }).build()).toBe('A eq 1'); 28 | }); 29 | 30 | it('should support filter guid', () => { 31 | expect( 32 | filter() 33 | .field('A') 34 | .eq(literalValues.Guid('253f842d-d739-41b8-ac8c-139ac7a9dd14')) 35 | .build() 36 | ).toBe('A eq 253f842d-d739-41b8-ac8c-139ac7a9dd14'); 37 | 38 | expect( 39 | filter({ A: literalValues.Guid('253f842d-d739-41b8-ac8c-139ac7a9dd14') }) 40 | .build() 41 | ).toBe('A eq 253f842d-d739-41b8-ac8c-139ac7a9dd14'); 42 | }); 43 | 44 | it('should support filter with type', () => { 45 | expect(filter({ A: 1 }).build()).toBe('A eq 1'); 46 | expect(filter({ A: literalValues.String('1') }).build()).toBe("A eq '1'"); 47 | expect(filter({ A: literalValues.Guid('253f842d-d739-41b8-ac8c-139ac7a9dd14') }).build()).toBe('A eq 253f842d-d739-41b8-ac8c-139ac7a9dd14'); 48 | }); 49 | 50 | }); 51 | -------------------------------------------------------------------------------- /test/builder/params.test.ts: -------------------------------------------------------------------------------- 1 | import { ODataFilter, ODataParam, ODataQueryParam, param } from '../../src'; 2 | 3 | describe('ODataParams Test', () => { 4 | 5 | test('ODataQueryParam alias', () => { 6 | expect(ODataParam).toEqual(ODataQueryParam); 7 | }); 8 | 9 | test('ODataParam skip and top', () => { 10 | const param = ODataParam.New().skip(30).top(10); 11 | expect(decodeURIComponent(param.toString())).toEqual('$skip=30&$top=10'); 12 | }); 13 | 14 | test('ODataParam orderby', () => { 15 | const param = ODataParam.New().orderby('CreationDateTime'); 16 | expect(decodeURIComponent(param.toString())).toEqual('$orderby=CreationDateTime desc'); 17 | }); 18 | 19 | test('ODataParam filter', () => { 20 | const param = ODataParam.New().filter(ODataFilter.New().field('A').eqString('test')); 21 | expect(param.toString()).toEqual("$filter=A eq 'test'"); 22 | const param2 = ODataParam.New().filter("A eq 'test'"); 23 | expect(decodeURIComponent(param2.toString())).toEqual("$filter=A eq 'test'"); 24 | expect(() => { 25 | ODataParam.New().filter(undefined); 26 | }).toThrow(); 27 | }); 28 | 29 | test('ODataParam count true', () => { 30 | const param = ODataParam.New().count(true); 31 | expect(decodeURIComponent(param.toString())).toEqual('$count=true'); 32 | }); 33 | 34 | test('ODataParam orderby multi', () => { 35 | const param = ODataParam.New().orderbyMulti([{ field: 'A' }, { field: 'B', order: 'asc' }]); 36 | expect(decodeURIComponent(param.toString())).toEqual('$orderby=A desc,B asc'); 37 | const p2 = ODataParam.New().orderby([{ field: 'A' }, { field: 'B', order: 'asc' }]); 38 | expect(decodeURIComponent(p2.toString())).toEqual('$orderby=A desc,B asc'); 39 | }); 40 | 41 | test('ODataParam search', () => { 42 | const param = ODataParam.New().search('any word'); 43 | expect(param.toString()).toEqual('$search=any word'); 44 | }); 45 | 46 | test('ODataParam select', () => { 47 | const param = ODataParam.New().select('ObjectID'); 48 | expect(decodeURIComponent(param.toString())).toEqual('$select=ObjectID'); 49 | }); 50 | 51 | test('ODataParam select (duplicate)', () => { 52 | const param = ODataParam.New().select(['ObjectID', 'F1', 'F1']); 53 | expect(decodeURIComponent(param.toString())).toEqual('$select=ObjectID,F1'); 54 | }); 55 | 56 | test('ODataParam select multi', () => { 57 | const param = ODataParam 58 | .New() 59 | .select('ObjectID') 60 | .select('Name'); 61 | expect(decodeURIComponent(param.toString())).toEqual('$select=ObjectID,Name'); 62 | }); 63 | 64 | test('expand navigation & replace', () => { 65 | const param = ODataParam.New().expand('Customer'); 66 | expect(decodeURIComponent(param.toString())).toEqual('$expand=Customer'); 67 | param.expand(['Customer', 'Employee'], true); 68 | expect(decodeURIComponent(param.toString())).toEqual('$expand=Customer,Employee'); 69 | expect(decodeURIComponent(ODataParam.New().expand('*').toString())).toEqual('$expand=*'); 70 | }); 71 | 72 | it('should support param.filter(obj)', () => { 73 | const param = ODataParam.New().filter({ A: true, B: 1, C: 'B' }); 74 | expect(param.toString()).toBe("$filter=A eq true and B eq 1 and C eq 'B'"); 75 | }); 76 | 77 | it('should support params alias', () => { 78 | expect(param().top(1).toString()).toBe('$top=1'); 79 | }); 80 | 81 | it('should support params with count', () => { 82 | expect(param().count(true).toString('v2')).toBe('$inlinecount=allpages'); 83 | expect(param().count(true).toString('v4')).toBe('$count=true'); 84 | }); 85 | 86 | }); 87 | -------------------------------------------------------------------------------- /test/filter.spec.ts: -------------------------------------------------------------------------------- 1 | import { get } from '@newdash/newdash'; 2 | import { Edm } from '@odata/metadata'; 3 | import { defaultParser, ODataFilter, ODataParam } from '../src'; 4 | 5 | 6 | describe('Filter Test Suite', () => { 7 | 8 | it('should support simple eq', () => { 9 | defaultParser.query('$format=json&$filter=A eq 2'); 10 | }); 11 | 12 | it('should support complex query', () => { 13 | defaultParser.query('$format=json&$filter=(A eq 2) and (B eq 3 or B eq 4)'); 14 | }); 15 | 16 | it('should support eq with guid & string', () => { 17 | const ast = defaultParser.query("$filter=A eq 702dac82-923d-4958-805b-ca41c593d74f and B eq 'strValue'"); 18 | expect(ast).not.toBeNull(); 19 | expect(get(ast, 'value.options[0].value.value.left.value.right.value')).toBe(Edm.Guid.className); 20 | expect(get(ast, 'value.options[0].value.value.right.value.right.value')).toBe(Edm.String.className); 21 | }); 22 | 23 | it('shuold support filter without double quote', () => { 24 | const filter = ODataFilter.New().field("key").eq('val') 25 | const filterStr = ODataParam.New().filter(filter).toString() 26 | expect(filterStr).toMatchSnapshot() 27 | const ast = defaultParser.query(filterStr) 28 | expect(ast).toMatchSnapshot() 29 | }) 30 | 31 | it('shuold support filter with other chars', () => { 32 | const filter = ODataFilter.New().field("key").eq('val$%^&*()_') 33 | const filterStr = ODataParam.New().filter(filter).toString() 34 | expect(filterStr).toMatchSnapshot() 35 | const ast = defaultParser.query(filterStr) 36 | expect(ast).toMatchSnapshot() 37 | }) 38 | 39 | it('shuold support filter with double quote', () => { 40 | const filter = ODataFilter.New().field("key").eq('val"') 41 | const filterStr = ODataParam.New().filter(filter).toString() 42 | expect(filterStr).toMatchSnapshot() 43 | const ast = defaultParser.query(filterStr) 44 | expect(ast).toMatchSnapshot() 45 | }) 46 | 47 | it('shuold support filter with single quote', () => { 48 | const filter = ODataFilter.New().field("key").eq("T''A First") 49 | const filterStr = ODataParam.New().filter(filter).toString() 50 | expect(filterStr).toMatchSnapshot() 51 | const ast = defaultParser.query(filterStr) 52 | expect(ast).toMatchSnapshot() 53 | }) 54 | 55 | it('should support complex filter', () => { 56 | 57 | const sFilter = ODataFilter.New() 58 | .field('A').eq(1).field('A').eq(2) 59 | .field('B').gt(3) 60 | .field('C').between(1, 3) 61 | .field('F').between(1, 3, true) 62 | .field('E').in(['a', 'c', 'd']) 63 | .field('year(Date)').eq(2010) 64 | .field('Date2').gt(Edm.DateTimeOffset.createValue('2020-07-30T03:16:27.023Z')) 65 | .field('Date3').lt(Edm.DateTimeOffset.createValue(new Date())) 66 | .toString(); 67 | 68 | defaultParser.filter(sFilter); 69 | 70 | }); 71 | 72 | }); 73 | -------------------------------------------------------------------------------- /test/issue.spec.ts: -------------------------------------------------------------------------------- 1 | import { defaultParser } from '../src'; 2 | 3 | describe('Issue Related Test Suite', () => { 4 | 5 | it('should support parser uri with geography - Polygon', () => { 6 | 7 | const t = defaultParser.odataUri("/Products?$filter=geo.intersects(Location, geography'SRID=12345;Polygon((-127.89734578345 45.234534534,-127.89734578345 45.234534534,-127.89734578345 45.234534534,-127.89734578345 45.234534534))')"); 8 | expect(t).not.toBeUndefined(); 9 | 10 | }); 11 | 12 | it('should support parser uri with geography - POLYGON', () => { 13 | const t = defaultParser.odataUri("/Products?$filter=geo.intersects(Location, geography'SRID=12345;POLYGON((-127.89734578345 45.234534534,-127.89734578345 45.234534534,-127.89734578345 45.234534534,-127.89734578345 45.234534534))')"); 14 | expect(t).not.toBeUndefined(); 15 | }); 16 | 17 | 18 | it('should support parse filter with not full POINT', () => { 19 | defaultParser.filter("geo.distance(location,geography'POINT(-1.702 48.113)') le 200"); 20 | }); 21 | 22 | }); 23 | -------------------------------------------------------------------------------- /test/json.spec.ts: -------------------------------------------------------------------------------- 1 | import { Parser } from '../src/parser'; 2 | 3 | 4 | describe('json parser', () => { 5 | const parser = new Parser(); 6 | 7 | it('should support key-value', () => { 8 | 9 | const jsonObject = JSON.stringify({ 'v1': false, 'v2': 1, 'v3': null, 'v4': '' }); 10 | const token = parser.arrayOrObject(jsonObject); 11 | expect(token?.value?.value?.items?.map((item) => item?.value?.value?.value)) 12 | .toEqual(['boolean', 'number', 'null', 'string']); 13 | 14 | }); 15 | 16 | }); 17 | 18 | -------------------------------------------------------------------------------- /test/odata-uri.spec.ts: -------------------------------------------------------------------------------- 1 | import { ServiceMetadata } from '@odata/metadata'; 2 | import { defaultParser } from '../src'; 3 | 4 | 5 | describe('OData URI Test Suite', () => { 6 | 7 | const schoolMeta = ServiceMetadata.processMetadataJson(require('./resources/school.edmx.json')); 8 | 9 | it('should support parser uri', () => { 10 | 11 | defaultParser.odataUri('/Categories?$skip=30'); 12 | defaultParser.odataUri('/Categories(10)?$expand=A,C&$select=D,E'); 13 | const ast = defaultParser.odataUri('/Classes?$expand=students/student', { metadata: schoolMeta.edmx }); 14 | 15 | expect(ast).not.toBeUndefined(); 16 | 17 | }); 18 | 19 | it('should support empty query', () => { 20 | const ast = defaultParser.odataUri('/Categories?'); 21 | expect(ast).not.toBeUndefined(); 22 | expect(ast?.value?.query).not.toBeUndefined(); 23 | expect(ast.value?.query?.value?.options).toBeNull(); 24 | expect(ast.value?.resource?.raw).toBe('Categories'); 25 | 26 | }); 27 | 28 | }); 29 | -------------------------------------------------------------------------------- /test/parser.spec.ts: -------------------------------------------------------------------------------- 1 | import { defaultParser, TokenType } from '../src'; 2 | import { Parser } from '../src/parser'; 3 | import { isType } from '../src/utils'; 4 | 5 | describe('Parser', () => { 6 | 7 | it('should instantiate odata parser', () => { 8 | const parser = new Parser(); 9 | const ast = parser.filter("Categories/all(d:d/Title eq 'alma')"); 10 | expect( 11 | ast.value.value.value.value.next.value.value.predicate.value.value.right 12 | .value 13 | ).toEqual('Edm.String'); 14 | }); 15 | 16 | it('should parse query string', () => { 17 | const parser = new Parser(); 18 | const ast = parser.query("$filter=Title eq 'alma'"); 19 | expect(ast.value.options[0].type).toEqual('Filter'); 20 | }); 21 | 22 | it('should parse multiple orderby params', () => { 23 | const parser = new Parser(); 24 | const ast = parser.query('$orderby=foo,bar'); 25 | expect(ast.value.options[0].value.items[0].raw).toEqual('foo'); 26 | expect(ast.value.options[0].value.items[1].raw).toEqual('bar'); 27 | }); 28 | 29 | it('should parse multiple orderby params with optional space', () => { 30 | const parser = new Parser(); 31 | const ast = parser.query('$orderby=foo, bar'); 32 | expect(ast.value.options[0].value.items[0].raw).toEqual('foo'); 33 | expect(ast.value.options[0].value.items[1].raw).toEqual('bar'); 34 | }); 35 | 36 | it('should parse custom query options', () => { 37 | const parser = new Parser(); 38 | const ast = parser.query('foo=123&bar=foobar'); 39 | 40 | expect(ast.value.options[0].type).toBe(TokenType.CustomQueryOption); 41 | expect(ast.value.options[1].type).toBe(TokenType.CustomQueryOption); 42 | 43 | if (isType(ast.value.options[0],TokenType.CustomQueryOption)) { 44 | expect(ast.value.options[0].value.key).toEqual('foo'); 45 | expect(ast.value.options[0].value.value).toEqual('123'); 46 | } 47 | 48 | if (isType(ast.value.options[1],TokenType.CustomQueryOption)) { 49 | expect(ast.value.options[1].value.key).toEqual('bar'); 50 | expect(ast.value.options[1].value.value).toEqual('foobar'); 51 | } 52 | 53 | }); 54 | 55 | it('should throw error parsing invalid custom query options', () => { 56 | expect(() => {defaultParser.query('$foo=123');}).toThrow(); 57 | }); 58 | 59 | }); 60 | -------------------------------------------------------------------------------- /test/primitive-cases.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | '-Name': 'Binary with X', 4 | '-Rule': 'binaryValue', 5 | '-FailAt': '0', 6 | 'Input': "X'1a2B3c4D'", 7 | 'result_error': { 8 | 'position': 0, 9 | 'type': 'Literal', 10 | 'value': 'Edm.Binary' 11 | } 12 | }, 13 | { 14 | '-Name': 'Binary - empty', 15 | '-Rule': 'binary', 16 | 'Input': "binary''", 17 | 'result': { 18 | 'position': 0, 19 | 'type': 'Literal', 20 | 'value': 'Edm.Binary' 21 | } 22 | }, 23 | { 24 | '-Name': 'Binary - f', 25 | '-Rule': 'binary', 26 | 'Input': "binary'Zg=='", 27 | 'result': { 28 | 'position': 0, 29 | 'type': 'Literal', 30 | 'value': 'Edm.Binary' 31 | } 32 | }, 33 | { 34 | '-Name': 'Binary - f (pad character is optional)', 35 | '-Rule': 'binary', 36 | 'Input': "binary'Zg'", 37 | 'result': { 38 | 'position': 0, 39 | 'type': 'Literal', 40 | 'value': 'Edm.Binary' 41 | } 42 | }, 43 | { 44 | '-Name': 'Binary - fo', 45 | '-Rule': 'binary', 46 | 'Input': "binary'Zm8='", 47 | 'result': { 48 | 'position': 0, 49 | 'type': 'Literal', 50 | 'value': 'Edm.Binary' 51 | } 52 | }, 53 | { 54 | '-Name': 'Binary - fo (pad character is optional)', 55 | '-Rule': 'binary', 56 | 'Input': "binary'Zm8='", 57 | 'result': { 58 | 'position': 0, 59 | 'type': 'Literal', 60 | 'value': 'Edm.Binary' 61 | } 62 | }, 63 | { 64 | '-Name': 'Binary - foo', 65 | '-Rule': 'binary', 66 | 'Input': "binary'Zm9v'", 67 | 'result': { 68 | 'position': 0, 69 | 'type': 'Literal', 70 | 'value': 'Edm.Binary' 71 | } 72 | }, 73 | { 74 | '-Name': 'Binary - foob', 75 | '-Rule': 'binary', 76 | 'Input': "binary'Zm9vYg=='", 77 | 'result': { 78 | 'position': 0, 79 | 'type': 'Literal', 80 | 'value': 'Edm.Binary' 81 | } 82 | }, 83 | { 84 | '-Name': 'Binary - fooba', 85 | '-Rule': 'binary', 86 | 'Input': "binary'Zm9vYmE='", 87 | 'result': { 88 | 'position': 0, 89 | 'type': 'Literal', 90 | 'value': 'Edm.Binary' 91 | } 92 | }, 93 | { 94 | '-Name': 'Binary - foobar', 95 | '-Rule': 'binary', 96 | 'Input': "binary'Zm9vYmFy'", 97 | 'result': { 98 | 'position': 0, 99 | 'type': 'Literal', 100 | 'value': 'Edm.Binary' 101 | } 102 | }, 103 | { 104 | '-Name': 'Date in URL or body', 105 | '-Rule': 'dateValue', 106 | 'Input': '2012-09-03', 107 | 'result': { 108 | 'position': 0, 109 | 'type': 'Literal', 110 | 'value': 'Edm.Date' 111 | } 112 | }, 113 | { 114 | '-Name': 'Date', 115 | '-Rule': 'dateValue', 116 | 'Input': '2012-09-10', 117 | 'result': { 118 | 'position': 0, 119 | 'type': 'Literal', 120 | 'value': 'Edm.Date' 121 | } 122 | }, 123 | { 124 | '-Name': 'Date', 125 | '-Rule': 'dateValue', 126 | 'Input': '2012-09-20', 127 | 'result': { 128 | 'position': 0, 129 | 'type': 'Literal', 130 | 'value': 'Edm.Date' 131 | } 132 | }, 133 | { 134 | '-Name': 'Date', 135 | '-Rule': 'dateValue', 136 | 'Input': '2012-09-03', 137 | 'result': { 138 | 'position': 0, 139 | 'type': 'Literal', 140 | 'value': 'Edm.Date' 141 | } 142 | }, 143 | { 144 | '-Name': 'Date: year zero', 145 | '-Rule': 'dateValue', 146 | 'Input': '0000-01-01', 147 | 'result': { 148 | 'position': 0, 149 | 'type': 'Literal', 150 | 'value': 'Edm.Date' 151 | } 152 | }, 153 | { 154 | '-Name': 'Date: negative', 155 | '-Rule': 'dateValue', 156 | 'Input': '-10000-04-01', 157 | 'result': { 158 | 'position': 0, 159 | 'type': 'Literal', 160 | 'value': 'Edm.Date' 161 | } 162 | }, 163 | { 164 | '-Name': 'DateTimeOffset: no seconds', 165 | '-Rule': 'dateTimeOffsetValue', 166 | 'Input': '2012-09-03T13:52Z', 167 | 'result': { 168 | 'position': 0, 169 | 'type': 'Literal', 170 | 'value': 'Edm.DateTimeOffset' 171 | } 172 | }, 173 | { 174 | '-Name': 'DateTimeOffset: seconds', 175 | '-Rule': 'dateTimeOffsetValue', 176 | 'Input': '2012-09-03T08:09:02Z', 177 | 'result': { 178 | 'position': 0, 179 | 'type': 'Literal', 180 | 'value': 'Edm.DateTimeOffset' 181 | } 182 | }, 183 | { 184 | '-Name': 'DateTimeOffset: subseconds', 185 | '-Rule': 'dateTimeOffsetValue', 186 | 'Input': '2012-08-31T18:19:22.1Z', 187 | 'result': { 188 | 'position': 0, 189 | 'type': 'Literal', 190 | 'value': 'Edm.DateTimeOffset' 191 | } 192 | }, 193 | { 194 | '-Name': 'DateTimeOffset: year zero', 195 | '-Rule': 'dateTimeOffsetValue', 196 | 'Input': '0000-01-01T00:00Z', 197 | 'result': { 198 | 'position': 0, 199 | 'type': 'Literal', 200 | 'value': 'Edm.DateTimeOffset' 201 | } 202 | }, 203 | { 204 | '-Name': 'DateTimeOffset: negative', 205 | '-Rule': 'dateTimeOffsetValue', 206 | 'Input': '-10000-04-01T00:00Z', 207 | 'result': { 208 | 'position': 0, 209 | 'type': 'Literal', 210 | 'value': 'Edm.DateTimeOffset' 211 | } 212 | }, 213 | { 214 | '-Name': 'DateTimeOffset: Midnight this day', 215 | '-Rule': 'dateTimeOffsetValue', 216 | '-FailAt': '12', 217 | 'Input': '2011-12-31T24:00Z', 218 | 'result': { 219 | 'position': 0, 220 | 'type': 'Literal', 221 | 'value': 'Edm.DateTimeOffset' 222 | } 223 | }, 224 | { 225 | '-Name': 'DateTimeOffset: Midnight this day with seconds', 226 | '-Rule': 'dateTimeOffsetValue', 227 | '-FailAt': '12', 228 | 'Input': '2011-12-31T24:00:00Z', 229 | 'result': { 230 | 'position': 0, 231 | 'type': 'Literal', 232 | 'value': 'Edm.DateTimeOffset' 233 | } 234 | }, 235 | { 236 | '-Name': 'DateTimeOffset: CEST', 237 | '-Rule': 'dateTimeOffsetValue', 238 | 'Input': '2012-09-03T14:53+02:00', 239 | 'result': { 240 | 'position': 0, 241 | 'type': 'Literal', 242 | 'value': 'Edm.DateTimeOffset' 243 | } 244 | }, 245 | { 246 | '-Name': 'DateTimeOffset: UTC', 247 | '-Rule': 'dateTimeOffsetValue', 248 | 'Input': '2012-09-03T12:53Z', 249 | 'result': { 250 | 'position': 0, 251 | 'type': 'Literal', 252 | 'value': 'Edm.DateTimeOffset' 253 | } 254 | }, 255 | { 256 | '-Name': 'DateTimeOffset: 24:00', 257 | '-Rule': 'dateTimeOffsetValue', 258 | '-FailAt': '12', 259 | 'Input': '2012-09-03T24:00-03:00', 260 | 'result': { 261 | 'position': 0, 262 | 'type': 'Literal', 263 | 'value': 'Edm.DateTimeOffset' 264 | } 265 | }, 266 | { 267 | '-Name': 'DateTimeOffset: 20th hour UTC', 268 | '-Rule': 'dateTimeOffsetValue', 269 | 'Input': '2012-11-28T20:00:00.000Z', 270 | 'result': { 271 | 'position': 0, 272 | 'type': 'Literal', 273 | 'value': 'Edm.DateTimeOffset' 274 | } 275 | }, 276 | { 277 | '-Name': 'Decimal', 278 | '-Rule': 'decimalValue', 279 | 'Input': '3.14', 280 | 'result': { 281 | 'position': 0, 282 | 'type': 'Literal', 283 | 'value': 'Edm.Decimal' 284 | } 285 | }, 286 | { 287 | '-Name': 'Duration in body', 288 | '-Rule': 'durationValue', 289 | 'Input': 'P6DT23H59M59.9999S', 290 | 'result_error': { 291 | 'position': 0, 292 | 'type': 'Literal', 293 | 'value': 'Edm.Duration' 294 | } 295 | }, 296 | { 297 | '-Name': 'Duration in body: no years allowed', 298 | '-Rule': 'durationValue', 299 | '-FailAt': '2', 300 | 'Input': 'P1Y6DT23H59M59.9999S', 301 | 'result': { 302 | 'position': 0, 303 | 'type': 'Literal', 304 | 'value': 'Edm.Duration' 305 | } 306 | }, 307 | { 308 | '-Name': 'Duration in body: no months allowed', 309 | '-Rule': 'durationValue', 310 | '-FailAt': '2', 311 | 'Input': 'P1M6DT23H59M59.9999S', 312 | 'result': { 313 | 'position': 0, 314 | 'type': 'Literal', 315 | 'value': 'Edm.Duration' 316 | } 317 | }, 318 | { 319 | '-Name': 'Duration in URL', 320 | '-Rule': 'duration', 321 | 'Input': "duration'P6DT23H59M59.9999S'", 322 | 'result': { 323 | 'position': 0, 324 | 'type': 'Literal', 325 | 'value': 'Edm.Duration' 326 | } 327 | }, 328 | { 329 | '-Name': 'Decimal: integer', 330 | '-Rule': 'decimalValue', 331 | 'Input': '-2', 332 | 'result_error': { 333 | 'position': 0, 334 | 'type': 'Literal', 335 | 'value': 'Edm.Decimal' 336 | } 337 | }, 338 | { 339 | '-Name': 'Decimal: integer', 340 | '-Rule': 'decimalValue', 341 | '-FailAt': '4', 342 | 'Input': '+42.', 343 | 'result_error': { 344 | 'position': 0, 345 | 'type': 'Literal', 346 | 'value': 'Edm.Decimal' 347 | } 348 | }, 349 | { 350 | '-Name': 'Decimal: no digit before decimal point', 351 | '-Rule': 'decimalValue', 352 | '-FailAt': '0', 353 | 'Input': '.1', 354 | 'result': { 355 | 'position': 0, 356 | 'type': 'Literal', 357 | 'value': 'Edm.Decimal' 358 | } 359 | }, 360 | { 361 | '-Name': 'Decimal in URL', 362 | '-Rule': 'decimalValue', 363 | 'Input': '3.14', 364 | 'result': { 365 | 'position': 0, 366 | 'type': 'Literal', 367 | 'value': 'Edm.Decimal' 368 | } 369 | }, 370 | { 371 | '-Name': 'Double', 372 | '-Rule': 'doubleValue', 373 | 'Input': '3.14', 374 | 'result': { 375 | 'position': 0, 376 | 'type': 'Literal', 377 | 'value': 'Edm.Double' 378 | } 379 | }, 380 | { 381 | '-Name': 'Double with exponent', 382 | '-Rule': 'doubleValue', 383 | 'Input': '-0.314e1', 384 | 'result': { 385 | 'position': 0, 386 | 'type': 'Literal', 387 | 'value': 'Edm.Double' 388 | } 389 | }, 390 | { 391 | '-Name': 'Negative infinity', 392 | '-Rule': 'doubleValue', 393 | 'Input': '-INF', 394 | 'result': { 395 | 'position': 0, 396 | 'type': 'Literal', 397 | 'value': 'Edm.Double' 398 | } 399 | }, 400 | { 401 | '-Name': 'Positive infinity', 402 | '-Rule': 'doubleValue', 403 | 'Input': 'INF', 404 | 'result': { 405 | 'position': 0, 406 | 'type': 'Literal', 407 | 'value': 'Edm.Double' 408 | } 409 | }, 410 | { 411 | '-Name': 'Not a Number', 412 | '-Rule': 'doubleValue', 413 | 'Input': 'NaN', 414 | 'result': { 415 | 'position': 0, 416 | 'type': 'Literal', 417 | 'value': 'Edm.Double' 418 | } 419 | }, 420 | { 421 | '-Name': 'Double in URL', 422 | '-Rule': 'doubleValue', 423 | 'Input': '-0.314e1', 424 | 'result': { 425 | 'position': 0, 426 | 'type': 'Literal', 427 | 'value': 'Edm.Double' 428 | } 429 | }, 430 | { 431 | '-Name': 'Single in URL', 432 | '-Rule': 'singleValue', 433 | 'Input': '-0.314e1', 434 | 'result': { 435 | 'position': 0, 436 | 'type': 'Literal', 437 | 'value': 'Edm.Single' 438 | } 439 | }, 440 | { 441 | '-Name': 'Byte', 442 | '-Rule': 'byteValue', 443 | 'Input': '255', 444 | 'result': { 445 | 'position': 0, 446 | 'type': 'Literal', 447 | 'value': 'Edm.Byte' 448 | } 449 | }, 450 | { 451 | '-Name': 'SByte', 452 | '-Rule': 'sbyteValue', 453 | 'Input': '-128', 454 | 'result': { 455 | 'position': 0, 456 | 'type': 'Literal', 457 | 'value': 'Edm.SByte' 458 | } 459 | }, 460 | { 461 | '-Name': 'Int16', 462 | '-Rule': 'int16Value', 463 | 'Input': '+32000', 464 | 'result': { 465 | 'position': 0, 466 | 'type': 'Literal', 467 | 'value': 'Edm.Int16' 468 | } 469 | }, 470 | { 471 | '-Name': 'Int32', 472 | '-Rule': 'int32Value', 473 | 'Input': '-2000000000', 474 | 'result': { 475 | 'position': 0, 476 | 'type': 'Literal', 477 | 'value': 'Edm.Int32' 478 | } 479 | }, 480 | { 481 | '-Name': 'Int64', 482 | '-Rule': 'int64Value', 483 | 'Input': '1234567890123456789', 484 | 'result': { 485 | 'position': 0, 486 | 'type': 'Literal', 487 | 'value': 'Edm.Int64' 488 | } 489 | }, 490 | { 491 | '-Name': 'Null: unqualified', 492 | '-Rule': 'nullValue', 493 | 'Input': 'null', 494 | 'result': { 495 | 'position': 0, 496 | 'type': 'Literal', 497 | 'value': 'null' 498 | } 499 | }, 500 | { 501 | '-Name': 'String', 502 | '-Rule': 'string', 503 | 'Input': "'ABCDEFGHIHJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~!$&('')*+,;=@'", 504 | 'result': { 505 | 'position': 0, 506 | 'type': 'Literal', 507 | 'value': 'Edm.String' 508 | } 509 | }, 510 | { 511 | '-Name': 'String', 512 | '-Rule': 'string', 513 | 'Input': "'O''Neil'", 514 | 'result': { 515 | 'position': 0, 516 | 'type': 'Literal', 517 | 'value': 'Edm.String' 518 | } 519 | }, 520 | { 521 | '-Name': 'String', 522 | '-Rule': 'string', 523 | 'Input': "%27O'%27Neil'", 524 | 'result': { 525 | 'position': 0, 526 | 'type': 'Literal', 527 | 'value': 'Edm.String' 528 | } 529 | }, 530 | { 531 | '-Name': 'String', 532 | '-Rule': 'string', 533 | '-FailAt': '3', 534 | 'Input': "'O'Neil'", 535 | 'result_error': { 536 | 'position': 0, 537 | 'type': 'Literal', 538 | 'value': 'Edm.String' 539 | } 540 | }, 541 | { 542 | '-Name': 'String', 543 | '-Rule': 'string', 544 | '-FailAt': '5', 545 | 'Input': "'O%27Neil'", 546 | 'result': { 547 | 'position': 0, 548 | 'type': 'Literal', 549 | 'value': 'Edm.String' 550 | } 551 | }, 552 | { 553 | '-Name': 'String', 554 | '-Rule': 'string', 555 | 'Input': "'%26%28'", 556 | 'result': { 557 | 'position': 0, 558 | 'type': 'Literal', 559 | 'value': 'Edm.String' 560 | } 561 | }, 562 | { 563 | '-Name': 'primitive value in request body - enumeration member', 564 | '-Rule': 'primitiveValue', 565 | 'Input': 'Yellow', 566 | 'result_error': { 567 | 'position': 0, 568 | 'type': 'Literal', 569 | 'value': 'Edm.???' 570 | } 571 | }, 572 | { 573 | '-Name': 'primitive value in request body - geo point', 574 | '-Rule': 'primitiveValue', 575 | 'Input': 'SRID=0;Point(142.1 64.1)', 576 | 'result_error': { 577 | 'position': 0, 578 | 'type': 'Literal', 579 | 'value': 'Edm.GeographyPoint' 580 | } 581 | }, 582 | { 583 | '-Name': 'primitive value in request body - integer', 584 | '-Rule': 'primitiveValue', 585 | 'Input': '0123456789', 586 | 'result': { 587 | 'position': 0, 588 | 'type': 'Literal', 589 | 'value': 'Edm.Int32' 590 | } 591 | }, 592 | { 593 | '-Name': 'primitive value in request body - guid', 594 | '-Rule': 'primitiveValue', 595 | 'Input': '01234567-89ab-cdef-0123-456789abcdef', 596 | 'result': { 597 | 'position': 0, 598 | 'type': 'Literal', 599 | 'value': 'Edm.Guid' 600 | } 601 | }, 602 | { 603 | '-Name': 'primitive value in request body - binary', 604 | '-Rule': 'primitiveValue', 605 | 'Input': 'a123456789abcdefABA=', 606 | 'result_error': { 607 | 'position': 0, 608 | 'type': 'Literal', 609 | 'value': 'Edm.Binary' 610 | } 611 | }, 612 | { 613 | '-Name': 'primitive value in request body - binary with line breaks and spaces', 614 | '-Rule': 'primitiveValue', 615 | '-FailAt': '8', 616 | 'Input': 'a1234567 89abcdefABA', 617 | 'result': { 618 | 'position': 0, 619 | 'type': 'Literal', 620 | 'value': 'Edm.Binary' 621 | } 622 | }, 623 | { 624 | '-Name': 'Key', 625 | '-Rule': 'string', 626 | 'Input': "'Hugo''s%20Tavern'", 627 | 'result': { 628 | 'position': 0, 629 | 'type': 'Literal', 630 | 'value': 'Edm.String' 631 | } 632 | }, 633 | { 634 | '-Name': 'Correct Guid', 635 | '-Rule': 'guidValue', 636 | 'Input': '01234567-89ab-cdef-0123-456789abcdef', 637 | 'result': { 638 | 'position': 0, 639 | 'type': 'Literal', 640 | 'value': 'Edm.Guid' 641 | } 642 | }, 643 | { 644 | '-Name': 'Guid with wrong character', 645 | '-Rule': 'guidValue', 646 | '-FailAt': '5', 647 | 'Input': '01234g67-89ab-cdef-0123-456789abcdef', 648 | 'result': { 649 | 'position': 0, 650 | 'type': 'Literal', 651 | 'value': 'Edm.Guid' 652 | } 653 | }, 654 | { 655 | '-Name': 'Guid with less than 32 digits', 656 | '-Rule': 'guidValue', 657 | '-FailAt': '23', 658 | 'Input': '01234567-89ab-cdef-456789abcdef', 659 | 'result': { 660 | 'position': 0, 661 | 'type': 'Literal', 662 | 'value': 'Edm.Guid' 663 | } 664 | }, 665 | { 666 | '-Name': 'TimeOfDay', 667 | '-Rule': 'timeOfDayValue', 668 | 'Input': '11:22:33', 669 | 'result': { 670 | 'position': 0, 671 | 'type': 'Literal', 672 | 'value': 'Edm.TimeOfDay' 673 | } 674 | }, 675 | { 676 | '-Name': 'TimeOfDay: no seconds', 677 | '-Rule': 'timeOfDayValue', 678 | 'Input': '11:22', 679 | 'result': { 680 | 'position': 0, 681 | 'type': 'Literal', 682 | 'value': 'Edm.TimeOfDay' 683 | } 684 | }, 685 | { 686 | '-Name': 'TimeOfDay: fractional seconds', 687 | '-Rule': 'timeOfDayValue', 688 | 'Input': '11:22:33.4444444', 689 | 'result': { 690 | 'position': 0, 691 | 'type': 'Literal', 692 | 'value': 'Edm.TimeOfDay' 693 | } 694 | }, 695 | { 696 | '-Name': 'TimeOfDay: 24:00', 697 | '-Rule': 'timeOfDayValue', 698 | '-FailAt': '1', 699 | 'Input': '24:00:00', 700 | 'result': { 701 | 'position': 0, 702 | 'type': 'Literal', 703 | 'value': 'Edm.TimeOfDay' 704 | } 705 | }, 706 | { 707 | '-Name': '5.1.1.1.1 boolean - only true and false', 708 | '-Rule': 'booleanValue', 709 | '-FailAt': '0', 710 | 'Input': '0', 711 | 'result': { 712 | 'position': 0, 713 | 'type': 'Literal', 714 | 'value': 'Edm.Boolean' 715 | } 716 | }, 717 | { 718 | '-Name': '5.1.1.1.1 boolean - only true and false', 719 | '-Rule': 'booleanValue', 720 | '-FailAt': '0', 721 | 'Input': '1', 722 | 'result': { 723 | 'position': 0, 724 | 'type': 'Literal', 725 | 'value': 'Edm.Boolean' 726 | } 727 | }, 728 | { 729 | '-Name': 'GeographyCollection', 730 | '-Rule': 'geographyCollection', 731 | 'Input': "geography'SRID=0;Collection(LineString(142.1 64.1,3.14 2.78))'", 732 | 'result': { 733 | 'position': 0, 734 | 'type': 'Literal', 735 | 'value': 'Edm.GeographyCollection' 736 | } 737 | }, 738 | { 739 | '-Name': 'GeographyLineString', 740 | '-Rule': 'geographyLineString', 741 | 'Input': "geography'SRID=0;LineString(142.1 64.1,3.14 2.78)'", 742 | 'result': { 743 | 'position': 0, 744 | 'type': 'Literal', 745 | 'value': 'Edm.GeographyLineString' 746 | } 747 | }, 748 | { 749 | '-Name': 'GeographyMultiLineString', 750 | '-Rule': 'geographyMultiLineString', 751 | 'Input': "geography'SRID=0;MultiLineString((142.1 64.1,3.14 2.78),(142.1 64.1,3.14 2.78))'", 752 | 'result': { 753 | 'position': 0, 754 | 'type': 'Literal', 755 | 'value': 'Edm.GeographyMultiLineString' 756 | } 757 | }, 758 | { 759 | '-Name': 'GeographyMultiPoint', 760 | '-Rule': 'geographyMultiPoint', 761 | 'Input': "geography'SRID=0;MultiPoint()'", 762 | 'result': { 763 | 'position': 0, 764 | 'type': 'Literal', 765 | 'value': 'Edm.GeographyMultiPoint' 766 | } 767 | }, 768 | { 769 | '-Name': 'GeographyMultiPoint', 770 | '-Rule': 'geographyMultiPoint', 771 | 'Input': "geography'SRID=0;MultiPoint((142.1 64.1),(1 2))'", 772 | 'result_error': { 773 | 'position': 0, 774 | 'type': 'Literal', 775 | 'value': 'Edm.GeographyMultiPoint' 776 | } 777 | }, 778 | { 779 | '-Name': 'GeographyMultiPolygon', 780 | '-Rule': 'geographyMultiPolygon', 781 | 'Input': "geography'SRID=0;MultiPolygon(((1 1,1 1),(1 1,2 2,3 3,1 1)))'", 782 | 'result_error': { 783 | 'position': 0, 784 | 'type': 'Literal', 785 | 'value': 'Edm.GeographyMultiPolygon' 786 | } 787 | }, 788 | { 789 | '-Name': 'GeographyPoint', 790 | '-Rule': 'geographyPoint', 791 | 'Input': "geography'SRID=0;Point(142.1 64.1)'", 792 | 'result': { 793 | 'position': 0, 794 | 'type': 'Literal', 795 | 'value': 'Edm.GeographyPoint' 796 | } 797 | }, 798 | { 799 | '-Name': 'GeographyPolygon', 800 | '-Rule': 'geographyPolygon', 801 | 'Input': "geography'SRID=0;Polygon((1 1,1 1),(1 1,2 2,3 3,1 1))'", 802 | 'result_error': { 803 | 'position': 0, 804 | 'type': 'Literal', 805 | 'value': 'Edm.GeographyPolygon' 806 | } 807 | }, 808 | { 809 | '-Name': 'GeometryCollection', 810 | '-Rule': 'geometryCollection', 811 | 'Input': "geometry'SRID=0;Collection(LineString(142.1 64.1,3.14 2.78))'", 812 | 'result': { 813 | 'position': 0, 814 | 'type': 'Literal', 815 | 'value': 'Edm.GeometryCollection' 816 | } 817 | }, 818 | { 819 | '-Name': 'GeometryLineString', 820 | '-Rule': 'geometryLineString', 821 | 'Input': "geometry'SRID=0;LineString(142.1 64.1,3.14 2.78)'", 822 | 'result': { 823 | 'position': 0, 824 | 'type': 'Literal', 825 | 'value': 'Edm.GeometryLineString' 826 | } 827 | }, 828 | { 829 | '-Name': 'GeometryMultiLineString', 830 | '-Rule': 'geometryMultiLineString', 831 | 'Input': "geometry'SRID=0;MultiLineString((142.1 64.1,3.14 2.78),(142.1 64.1,3.14 2.78))'", 832 | 'result': { 833 | 'position': 0, 834 | 'type': 'Literal', 835 | 'value': 'Edm.GeometryMultiLineString' 836 | } 837 | }, 838 | { 839 | '-Name': 'GeometryMultiPoint', 840 | '-Rule': 'geometryMultiPoint', 841 | 'Input': "geometry'SRID=0;MultiPoint()'", 842 | 'result': { 843 | 'position': 0, 844 | 'type': 'Literal', 845 | 'value': 'Edm.GeometryMultiPoint' 846 | } 847 | }, 848 | { 849 | '-Name': 'GeometryMultiPoint', 850 | '-Rule': 'geometryMultiPoint', 851 | 'Input': "geometry'SRID=0;MultiPoint((142.1 64.1),(1 2))'", 852 | 'result_error': { 853 | 'position': 0, 854 | 'type': 'Literal', 855 | 'value': 'Edm.GeometryMultiPoint' 856 | } 857 | }, 858 | { 859 | '-Name': 'GeometryMultiPolygon', 860 | '-Rule': 'geometryMultiPolygon', 861 | 'Input': "geometry'SRID=0;MultiPolygon(((1 1,1 1),(1 1,2 2,3 3,1 1)))'", 862 | 'result_error': { 863 | 'position': 0, 864 | 'type': 'Literal', 865 | 'value': 'Edm.GeometryMultiPoint' 866 | } 867 | }, 868 | { 869 | '-Name': 'GeometryPoint', 870 | '-Rule': 'geometryPoint', 871 | 'Input': "geometry'SRID=0;Point(142.1 64.1)'", 872 | 'result': { 873 | 'position': 0, 874 | 'type': 'Literal', 875 | 'value': 'Edm.GeometryPoint' 876 | } 877 | }, 878 | { 879 | '-Name': 'GeometryPolygon', 880 | '-Rule': 'geometryPolygon', 881 | 'Input': "geometry'SRID=0;Polygon((1 1,1 1),(1 1,2 2,3 3,1 1))'", 882 | 'result_error': { 883 | 'position': 0, 884 | 'type': 'Literal', 885 | 'value': 'Edm.GeometryPolygon' 886 | } 887 | } 888 | ]; 889 | -------------------------------------------------------------------------------- /test/primitiveLiteral.spec.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { defaultParser } from '../src'; 3 | import * as PrimitiveLiteral from '../src/primitiveLiteral'; 4 | import cases from './primitive-cases'; 5 | 6 | describe('Primitive literals from json', () => { 7 | cases.forEach((item, index, array) => { 8 | const title = `#${index} should parse ${item['-Name']}: ${item.Input}`; 9 | let resultName = 'result'; 10 | if (item.result === undefined) { 11 | resultName = 'result_error'; 12 | item['-FailAt'] = item['-FailAt'] || 0; 13 | } 14 | if (item[resultName] !== undefined) { 15 | it(title, () => { 16 | const source = new Uint8Array(Buffer.from(item.Input)); 17 | if (item[resultName].next === undefined) { item[resultName].next = item.Input.length; } 18 | if (item[resultName].raw === undefined) { item[resultName].raw = item.Input; } 19 | 20 | const literalFunctionName = getLiteralFunctionName(item['-Rule'] || 'primitiveLiteral'); 21 | const literal = (PrimitiveLiteral[literalFunctionName] || PrimitiveLiteral.primitiveLiteral)(source, 0); 22 | if (item['-FailAt'] !== undefined) { 23 | expect(literal).toBeUndefined(); 24 | return; 25 | } 26 | // extract properties 27 | expect({ ...literal }).toStrictEqual(item[resultName]); 28 | }); 29 | } 30 | }); 31 | 32 | it('should support geography - Polygon', () => { 33 | const t = defaultParser.literal("geography'SRID=12345;Polygon((-127.89734578345 45.234534534,-127.89734578345 45.234534534,-127.89734578345 45.234534534,-127.89734578345 45.234534534))'"); 34 | expect(t.value).toBe('Edm.GeographyPolygon'); 35 | expect(t).not.toBeUndefined(); 36 | }); 37 | 38 | it('should support geography - POLYGON', () => { 39 | const t = defaultParser.literal("geography'SRID=12345;POLYGON((-127.89734578345 45.234534534,-127.89734578345 45.234534534,-127.89734578345 45.234534534,-127.89734578345 45.234534534))'"); 40 | expect(t.value).toBe('Edm.GeographyPolygon'); 41 | expect(t).not.toBeUndefined(); 42 | }); 43 | 44 | 45 | it('should support geography - POINT', () => { 46 | const t = defaultParser.literal("geography'POINT(-127.89734578345 45.234534534)'"); 47 | expect(t.value).toBe('Edm.GeographyPoint'); 48 | expect(t).not.toBeUndefined(); 49 | }); 50 | 51 | it('should support geography - FULL POINT', () => { 52 | const t = defaultParser.literal("geography'SRID=12345;POINT(-127.89734578345 45.234534534)'"); 53 | expect(t.value).toBe('Edm.GeographyPoint'); 54 | expect(t).not.toBeUndefined(); 55 | }); 56 | 57 | 58 | }); 59 | 60 | function getLiteralFunctionName(itemRule) { 61 | switch (itemRule) { 62 | case 'string': 63 | return 'stringValue'; 64 | case 'primitiveValue': 65 | return 'primitiveLiteral'; 66 | default: 67 | return itemRule; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /test/query.spec.ts: -------------------------------------------------------------------------------- 1 | import { get } from '@newdash/newdash'; 2 | import { PrimitiveTypeEnum } from '@odata/metadata'; 3 | import { defaultParser, ODataFilter, ODataParam } from '../src'; 4 | import { TokenType } from '../src/lexer'; 5 | import { Parser } from '../src/parser'; 6 | import { findAll, findOne, isType } from '../src/utils'; 7 | 8 | describe('Query Test Suite', () => { 9 | 10 | const parser = new Parser(); 11 | 12 | const expands = [ 13 | [ODataParam.New().expand('A').toString(), 'A'], 14 | [ODataParam.New().expand('A/V').toString(), 'A/V'], 15 | [ODataParam.New().expand('A/B/C').toString(), 'A/B/C'], 16 | [ODataParam.New().expand(['A', 'B/C']).toString(), 'A'], 17 | [ODataParam.New().expand('*').toString(), '*'] 18 | ]; 19 | 20 | expands.forEach(([original, parsed]) => { 21 | 22 | it(`should parse ${original}`, () => { 23 | 24 | expect(parser.query(original).value.options[0].value.items[0].raw).toEqual(parsed); 25 | 26 | }); 27 | 28 | }); 29 | 30 | it('should parse $top', () => { 31 | 32 | const ast = parser.query('$top=1'); 33 | 34 | expect(ast.value.options[0].type).toBe(TokenType.Top); 35 | 36 | if (isType(ast.value.options[0], TokenType.Top)) { 37 | expect(ast.value.options[0].value.raw).toEqual('1'); 38 | } 39 | 40 | }); 41 | 42 | it('should parse $top and $skip', () => { 43 | const ast = defaultParser.query('$top=1&$skip=120'); 44 | expect(ast.value.options[0].type).toBe(TokenType.Top); 45 | expect(ast.value.options[1].type).toBe(TokenType.Skip); 46 | 47 | if (isType(ast.value.options[0], TokenType.Top)) { 48 | expect(ast.value.options[0].value.raw).toEqual('1'); 49 | } 50 | if (isType(ast.value.options[1], TokenType.Skip)) { 51 | expect(ast.value.options[1].value.raw).toEqual('120'); 52 | } 53 | }); 54 | 55 | it('should parse $select', () => { 56 | expect(parser.query('$select=A').value.options[0].value.items[0].value.raw).toEqual('A'); 57 | expect(parser.query('$select=*').value.options[0].value.items[0].value.value).toEqual('*'); 58 | let ast = defaultParser.query('$select=A,B,C'); 59 | expect(findAll(ast, TokenType.SelectPath).map((node) => get(node, 'value.value.name')) 60 | ).toStrictEqual(['A', 'B', 'C']); 61 | ast = defaultParser.query('$select=A, B,C'); 62 | expect(findAll(ast, TokenType.SelectPath).map((node) => get(node, 'value.value.name')) 63 | ).toStrictEqual(['A', 'B', 'C']); 64 | 65 | parser.query('$select=A/B'); 66 | }); 67 | 68 | it('should parse $search', () => { 69 | 70 | parser.query('$search=theo'); 71 | parser.query('$search="theo%20sun"'); 72 | 73 | }); 74 | 75 | it('should parse $orderby', () => { 76 | parser.query('$orderby=A desc'); 77 | parser.query('$orderby=A desc,B asc'); 78 | parser.query('$orderby=A desc, B asc'); 79 | 80 | }); 81 | 82 | it('should parse $format', () => { 83 | parser.query('$format=xml'); 84 | parser.query('$format=json'); 85 | parser.query('$format=JSON'); 86 | parser.query('$format=atom'); 87 | }); 88 | 89 | it('should parse $count', () => { 90 | const ast = parser.query('$count=true'); 91 | const node = findOne(ast, TokenType.InlineCount); 92 | expect(node).not.toBeUndefined(); 93 | }); 94 | 95 | it('should parse $filter only', () => { 96 | const ast = defaultParser.query('$filter=id eq 1'); 97 | expect(ast).not.toBeUndefined(); 98 | expect(ast.value.options).not.toBeUndefined(); 99 | expect(ast.value.options).toHaveLength(1); 100 | 101 | }); 102 | 103 | it('should parse complex uri', () => { 104 | 105 | const u1 = ODataParam.New() 106 | .top(1) 107 | .skip(10) 108 | .select(['A', 'B']) 109 | .orderbyMulti([{ field: 'C', order: 'asc' }, { field: 'D', order: 'desc' }]) 110 | .format('json') 111 | .search('A') 112 | .expand('F1,F2') 113 | .filter(ODataFilter.New().field('A').eq(1).toString()) 114 | .toString(); 115 | const ast = defaultParser.query(u1); 116 | 117 | expect(findOne(ast, TokenType.Filter).value).not.toBeNull(); 118 | 119 | expect(findOne(ast, TokenType.Top)).toMatchObject({ 120 | type: TokenType.Top, 121 | value: { 122 | type: TokenType.Literal, 123 | value: PrimitiveTypeEnum.Int32 124 | } 125 | }); 126 | expect(findOne(ast, TokenType.Skip).value.raw).toBe('10'); 127 | 128 | expect(findOne(ast, TokenType.Format).value.format).toBe('json'); 129 | expect(findOne(ast, TokenType.Search).value.value).toBe('A'); 130 | 131 | expect( 132 | findOne(ast, TokenType.Expand) 133 | .value 134 | .items 135 | .map((item) => item.raw) 136 | ).toStrictEqual(['F1', 'F2']); 137 | 138 | }); 139 | 140 | }); 141 | -------------------------------------------------------------------------------- /test/resource-path.spec.ts: -------------------------------------------------------------------------------- 1 | import { TokenType } from '../src/lexer'; 2 | import { Parser } from '../src/parser'; 3 | import { findOne } from '../src/utils'; 4 | 5 | describe('ResourcePath Test Suite', () => { 6 | 7 | const parser = new Parser(); 8 | 9 | it('should parse $metadata', () => { 10 | 11 | parser.resourcePath('$metadata'); 12 | parser.resourcePath('/$metadata'); 13 | 14 | }); 15 | 16 | it('should parse collection with parameter', () => { 17 | 18 | parser.resourcePath('Categories(123)'); 19 | parser.resourcePath('/Categories(123)'); 20 | parser.resourcePath('/Categories(A=123)'); 21 | parser.resourcePath("/Categories('123')"); 22 | 23 | }); 24 | 25 | it('should parse collection with navigation', () => { 26 | 27 | parser.resourcePath('Categories(1)/Product(2)'); 28 | parser.resourcePath('/Categories(2)/Product(ID=3)'); 29 | 30 | }); 31 | 32 | 33 | it('should parse collection with path', () => { 34 | 35 | expect(findOne(parser.resourcePath('Categories(123)/A'), TokenType.KeyPropertyValue).raw) 36 | .toEqual('123'); 37 | 38 | parser.resourcePath('/Categories(123)/A'); 39 | parser.resourcePath('/Categories(0)/$ref'); 40 | parser.resourcePath('/Categories(0)/Product/$ref'); 41 | 42 | }); 43 | 44 | it('should parse function call', () => { 45 | 46 | // function call is similar to collection resource 47 | 48 | parser.resourcePath('/ProductsByCategoryId(categoryId=2)'); 49 | 50 | parser.resourcePath('/ProductsByCategoryId'); 51 | 52 | }); 53 | 54 | it('should support $batch', () => { 55 | 56 | // function call is similar to collection resource 57 | 58 | parser.resourcePath('/$batch'); 59 | 60 | }); 61 | 62 | 63 | }); 64 | -------------------------------------------------------------------------------- /test/resources/school.edmx.json: -------------------------------------------------------------------------------- 1 | { 2 | "dataServices": { 3 | "schemas": [ 4 | { 5 | "namespace": "Default", 6 | "enumTypes": [], 7 | "typeDefinitions": [], 8 | "complexTypes": [], 9 | "entityTypes": [ 10 | { 11 | "name": "Class", 12 | "key": { 13 | "propertyRefs": [ 14 | { 15 | "name": "id" 16 | } 17 | ] 18 | }, 19 | "openType": false, 20 | "hasStream": false, 21 | "properties": [ 22 | { 23 | "name": "id", 24 | "type": "Edm.Int32", 25 | "nullable": false, 26 | "annotations": [] 27 | }, 28 | { 29 | "name": "name", 30 | "type": "Edm.String", 31 | "annotations": [] 32 | }, 33 | { 34 | "name": "desc", 35 | "type": "Edm.String", 36 | "annotations": [] 37 | }, 38 | { 39 | "name": "teacherOneId", 40 | "type": "Edm.Int32", 41 | "nullable": true, 42 | "annotations": [] 43 | } 44 | ], 45 | "navigationProperties": [ 46 | { 47 | "name": "teacher", 48 | "type": "Default.Teacher", 49 | "referentialConstraints": [], 50 | "annotations": [] 51 | }, 52 | { 53 | "name": "students", 54 | "type": "Collection(Default.RelStudentClassAssignment)", 55 | "referentialConstraints": [], 56 | "annotations": [] 57 | } 58 | ], 59 | "annotations": [] 60 | }, 61 | { 62 | "name": "Profile", 63 | "key": { 64 | "propertyRefs": [ 65 | { 66 | "name": "id" 67 | } 68 | ] 69 | }, 70 | "openType": false, 71 | "hasStream": false, 72 | "properties": [ 73 | { 74 | "name": "id", 75 | "type": "Edm.Int32", 76 | "nullable": false, 77 | "annotations": [] 78 | } 79 | ], 80 | "navigationProperties": [], 81 | "annotations": [] 82 | }, 83 | { 84 | "name": "RelStudentClassAssignment", 85 | "key": { 86 | "propertyRefs": [ 87 | { 88 | "name": "uuid" 89 | } 90 | ] 91 | }, 92 | "openType": false, 93 | "hasStream": false, 94 | "properties": [ 95 | { 96 | "name": "uuid", 97 | "type": "Edm.String", 98 | "nullable": false, 99 | "annotations": [] 100 | }, 101 | { 102 | "name": "studentId", 103 | "type": "Edm.Int32", 104 | "annotations": [] 105 | }, 106 | { 107 | "name": "classId", 108 | "type": "Edm.Int32", 109 | "annotations": [] 110 | } 111 | ], 112 | "navigationProperties": [ 113 | { 114 | "name": "student", 115 | "type": "Default.Student", 116 | "referentialConstraints": [], 117 | "annotations": [] 118 | }, 119 | { 120 | "name": "clazz", 121 | "type": "Default.Class", 122 | "referentialConstraints": [], 123 | "annotations": [] 124 | } 125 | ], 126 | "annotations": [] 127 | }, 128 | { 129 | "name": "Student", 130 | "key": { 131 | "propertyRefs": [ 132 | { 133 | "name": "id" 134 | } 135 | ] 136 | }, 137 | "openType": false, 138 | "hasStream": false, 139 | "properties": [ 140 | { 141 | "name": "id", 142 | "type": "Edm.Int32", 143 | "nullable": false, 144 | "annotations": [] 145 | }, 146 | { 147 | "name": "name", 148 | "type": "Edm.String", 149 | "annotations": [] 150 | }, 151 | { 152 | "name": "age", 153 | "type": "Edm.Int32", 154 | "nullable": true, 155 | "annotations": [] 156 | } 157 | ], 158 | "navigationProperties": [ 159 | { 160 | "name": "classes", 161 | "type": "Collection(Default.RelStudentClassAssignment)", 162 | "referentialConstraints": [], 163 | "annotations": [] 164 | } 165 | ], 166 | "annotations": [] 167 | }, 168 | { 169 | "name": "Teacher", 170 | "key": { 171 | "propertyRefs": [ 172 | { 173 | "name": "id" 174 | } 175 | ] 176 | }, 177 | "openType": false, 178 | "hasStream": false, 179 | "properties": [ 180 | { 181 | "name": "id", 182 | "type": "Edm.Int32", 183 | "nullable": false, 184 | "annotations": [] 185 | }, 186 | { 187 | "name": "name", 188 | "type": "Edm.String", 189 | "annotations": [] 190 | }, 191 | { 192 | "name": "profileId", 193 | "type": "Edm.Int32", 194 | "nullable": true, 195 | "annotations": [] 196 | } 197 | ], 198 | "navigationProperties": [ 199 | { 200 | "name": "profile", 201 | "type": "Default.Profile", 202 | "referentialConstraints": [], 203 | "annotations": [] 204 | }, 205 | { 206 | "name": "classes", 207 | "type": "Collection(Default.Class)", 208 | "referentialConstraints": [], 209 | "annotations": [] 210 | } 211 | ], 212 | "annotations": [] 213 | } 214 | ], 215 | "actions": [ 216 | { 217 | "name": "addClass", 218 | "isBound": true, 219 | "parameters": [ 220 | { 221 | "name": "bindingParameter", 222 | "type": "Default.Teacher", 223 | "annotations": [] 224 | }, 225 | { 226 | "name": "classId", 227 | "type": "Edm.Int32", 228 | "annotations": [] 229 | } 230 | ], 231 | "returnType": { 232 | "annotations": [] 233 | }, 234 | "annotations": [] 235 | } 236 | ], 237 | "functions": [ 238 | { 239 | "name": "queryClass", 240 | "isBound": true, 241 | "parameters": [ 242 | { 243 | "name": "bindingParameter", 244 | "type": "Default.Teacher", 245 | "annotations": [] 246 | } 247 | ], 248 | "returnType": { 249 | "type": "Collection(Edm.String)", 250 | "annotations": [] 251 | }, 252 | "annotations": [] 253 | } 254 | ], 255 | "entityContainer": [ 256 | { 257 | "name": "Default", 258 | "entitySets": [ 259 | { 260 | "name": "Students", 261 | "entityType": "Default.Student", 262 | "annotations": [] 263 | }, 264 | { 265 | "name": "Classes", 266 | "entityType": "Default.Class", 267 | "annotations": [] 268 | }, 269 | { 270 | "name": "Teachers", 271 | "entityType": "Default.Teacher", 272 | "annotations": [] 273 | }, 274 | { 275 | "name": "Profiles", 276 | "entityType": "Default.Profile", 277 | "annotations": [] 278 | }, 279 | { 280 | "name": "RelStudentClassAssignments", 281 | "entityType": "Default.RelStudentClassAssignment", 282 | "annotations": [] 283 | } 284 | ], 285 | "actionImports": [], 286 | "functionImports": [] 287 | } 288 | ], 289 | "annotations": [] 290 | } 291 | ] 292 | }, 293 | "references": [], 294 | "version": "4.0" 295 | } -------------------------------------------------------------------------------- /test/visitor.spec.ts: -------------------------------------------------------------------------------- 1 | import { createTraverser, defaultParser } from '../src'; 2 | 3 | describe('Visitor Parse Suite', () => { 4 | 5 | const createSeqTokenProcessor = (key: string, arr: Array) => () => arr.push(key); 6 | 7 | const createSeqTraverser = (deepFirst = false) => { 8 | const visitSequence = []; 9 | 10 | const visit = createTraverser({ 11 | Top: createSeqTokenProcessor('param:top', visitSequence), 12 | Skip: createSeqTokenProcessor('param:skip', visitSequence), 13 | Filter: createSeqTokenProcessor('param:filter', visitSequence), 14 | OrderBy: createSeqTokenProcessor('param:orderby', visitSequence), 15 | Format: createSeqTokenProcessor('param:format', visitSequence), 16 | Search: createSeqTokenProcessor('param:search', visitSequence), 17 | InlineCount: createSeqTokenProcessor('param:inlinecount', visitSequence), 18 | 19 | AndExpression: createSeqTokenProcessor('and', visitSequence), 20 | BoolParenExpression: createSeqTokenProcessor('paren', visitSequence), 21 | EqualsExpression: createSeqTokenProcessor('eq', visitSequence), 22 | OrExpression: createSeqTokenProcessor('or', visitSequence), 23 | Literal: createSeqTokenProcessor('lit', visitSequence) 24 | }, deepFirst); 25 | 26 | return { visit, visitSequence }; 27 | }; 28 | 29 | 30 | it('should visit filter', () => { 31 | 32 | const expectedSeq = ['and', 'paren', 'eq', 'lit', 'paren', 'eq', 'lit']; 33 | 34 | const { visit, visitSequence } = createSeqTraverser(); 35 | 36 | const node = defaultParser.filter('(A eq 2) and (V eq 3)'); 37 | 38 | visit(node); 39 | 40 | expect(visitSequence).toEqual(expectedSeq); 41 | 42 | }); 43 | 44 | it('should visit filter deep first', () => { 45 | 46 | const expectedSeq = ['lit', 'eq', 'lit', 'eq', 'or', 'paren', 'lit', 'eq', 'paren', 'and']; 47 | 48 | const { visit, visitSequence } = createSeqTraverser(true); 49 | 50 | const node = defaultParser.filter('(A eq 2 or A eq 3) and (V eq 3)'); 51 | 52 | visit(node); 53 | 54 | expect(visitSequence).toEqual(expectedSeq); 55 | 56 | }); 57 | 58 | it('should support visit undefined', () => { 59 | const { visit, visitSequence } = createSeqTraverser(true); 60 | visit(undefined as any); 61 | }); 62 | 63 | }); 64 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "lib", 4 | "allowJs": true, 5 | "module": "commonjs", 6 | "target": "ES2018", 7 | "declaration": true, 8 | "sourceMap": true, 9 | "newLine": "lf", 10 | "resolveJsonModule": true, 11 | "lib": [ 12 | "ES2015", 13 | "ES2016", 14 | "ES2017" 15 | ] 16 | }, 17 | "include": [ 18 | "src" 19 | ], 20 | "exclude": [ 21 | "lib", 22 | "node_modules" 23 | ] 24 | } --------------------------------------------------------------------------------