├── .eslintignore
├── .eslintrc.json
├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature-request.md
├── pull_request_template.md
└── workflows
│ ├── codecov.yml
│ ├── commitlint.yml
│ ├── housekeeping.yml
│ ├── pr-description-enforcer.yml
│ ├── release-please.yml
│ ├── semantic-pr.yml
│ ├── tests.yml
│ └── verify.yml
├── .gitignore
├── .husky
├── commit-msg
└── pre-commit
├── .nvmrc
├── .prettierignore
├── .prettierrc
├── .release-please-manifest.json
├── .vscode
├── launch.json
└── settings.json
├── CHANGELOG.md
├── CODEOWNERS
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── commitlint.config.js
├── index.html
├── jest.config.ts
├── package-lock.json
├── package.json
├── public
└── favicon.ico
├── readme.md
├── release-please-config.json
├── sonar-project.properties
├── src
├── constants.ts
├── engine.test.ts
├── engine.ts
├── errors
│ ├── index.ts
│ ├── lexer.ts
│ ├── mapping.ts
│ ├── parser.ts
│ └── translator.ts
├── index.ts
├── lexer.ts
├── operator.test.ts
├── operators.ts
├── parser.ts
├── reverse_translator.test.ts
├── reverse_translator.ts
├── translator.ts
├── types.ts
├── utils
│ ├── common.test.ts
│ ├── common.ts
│ ├── converter.test.ts
│ ├── converter.ts
│ ├── index.ts
│ └── translator.ts
└── vite-env.d.ts
├── stryker.conf.json
├── test
├── e2e.test.ts
├── scenario.test.ts
├── scenarios
│ ├── arrays
│ │ ├── data.ts
│ │ └── template.jt
│ ├── assignments
│ │ ├── data.ts
│ │ └── template.jt
│ ├── bad_templates
│ │ ├── bad_array_coalese_expr.jt
│ │ ├── bad_async_usage.jt
│ │ ├── bad_context_var.jt
│ │ ├── bad_function_params.jt
│ │ ├── bad_function_rest_param.jt
│ │ ├── bad_number.jt
│ │ ├── bad_regex.jt
│ │ ├── bad_string.jt
│ │ ├── data.ts
│ │ ├── empty_block.jt
│ │ ├── empty_object_vars_for_definition.jt
│ │ ├── incomplete_statement.jt
│ │ ├── invalid_new_function_call.jt
│ │ ├── invalid_object_vars_for_definition.jt
│ │ ├── invalid_token_after_function_def.jt
│ │ ├── invalid_variable_assignment1.jt
│ │ ├── invalid_variable_assignment2.jt
│ │ ├── invalid_variable_assignment3.jt
│ │ ├── invalid_variable_assignment4.jt
│ │ ├── invalid_variable_assignment5.jt
│ │ ├── invalid_variable_assignment6.jt
│ │ ├── invalid_variable_assignment7.jt
│ │ ├── invalid_variable_assignment8.jt
│ │ ├── invalid_variable_assignment9.jt
│ │ ├── invalid_variable_definition.jt
│ │ ├── object_with_invalid_closing.jt
│ │ ├── object_with_invalid_key.jt
│ │ ├── reserved_id.jt
│ │ ├── unknown_token.jt
│ │ └── unsupported_assignment.jt
│ ├── base
│ │ └── data.ts
│ ├── bindings
│ │ ├── async.jt
│ │ ├── data.ts
│ │ ├── new_operator.jt
│ │ └── template.jt
│ ├── block
│ │ ├── data.ts
│ │ └── template.jt
│ ├── comments
│ │ ├── data.ts
│ │ └── template.jt
│ ├── comparisons
│ │ ├── anyof.jt
│ │ ├── contains.jt
│ │ ├── data.ts
│ │ ├── empty.jt
│ │ ├── ends_with.jt
│ │ ├── ends_with_ignore_case.jt
│ │ ├── eq.jt
│ │ ├── ge.jt
│ │ ├── gte.jt
│ │ ├── in.jt
│ │ ├── le.jt
│ │ ├── lte.jt
│ │ ├── ne.jt
│ │ ├── noneof.jt
│ │ ├── not_in.jt
│ │ ├── regex.jt
│ │ ├── size.jt
│ │ ├── starts_with.jt
│ │ ├── starts_with_ignore_case.jt
│ │ ├── string_contains_ignore_case.jt
│ │ ├── string_eq.jt
│ │ ├── string_eq_ingore_case.jt
│ │ ├── string_ne.jt
│ │ ├── string_ne_ingore_case.jt
│ │ └── subsetof.jt
│ ├── compile_time_expressions
│ │ ├── data.ts
│ │ ├── template.jt
│ │ └── two_level_path_processing.jt
│ ├── conditions
│ │ ├── data.ts
│ │ ├── if_block.jt
│ │ ├── if_then.jt
│ │ ├── objects.jt
│ │ ├── template.jt
│ │ └── undefined_arr_cond.jt
│ ├── context_variables
│ │ ├── data.ts
│ │ ├── filter.jt
│ │ ├── function.jt
│ │ ├── selector.jt
│ │ └── template.jt
│ ├── filters
│ │ ├── array_filters.jt
│ │ ├── data.ts
│ │ ├── invalid_object_index_filters.jt
│ │ ├── object_filters.jt
│ │ └── object_indexes.jt
│ ├── functions
│ │ ├── array_functions.jt
│ │ ├── data.ts
│ │ ├── function_calls.jt
│ │ ├── js_date_function.jt
│ │ ├── new_operator.jt
│ │ ├── parent_scope_vars.jt
│ │ ├── promise.jt
│ │ └── template.jt
│ ├── increment_statements
│ │ ├── data.ts
│ │ ├── postfix_decrement_on_literal.jt
│ │ ├── postfix_decrement_on_non_id.jt
│ │ ├── postfix_increment_on_literal.jt
│ │ ├── postfix_increment_on_non_id.jt
│ │ ├── prefix_decrement_on_literal.jt
│ │ └── prefix_increment_on_literal.jt
│ ├── inputs
│ │ ├── data.ts
│ │ └── template.jt
│ ├── logics
│ │ ├── data.ts
│ │ └── template.jt
│ ├── loops
│ │ ├── break_without_condition.jt
│ │ ├── break_without_loop.jt
│ │ ├── complex_loop.jt
│ │ ├── continue.jt
│ │ ├── continue_without_condition.jt
│ │ ├── continue_without_loop.jt
│ │ ├── data.ts
│ │ ├── empty_loop.jt
│ │ ├── just_for.jt
│ │ ├── no_init.jt
│ │ ├── no_test.jt
│ │ ├── no_update.jt
│ │ ├── statement_after_break.jt
│ │ ├── statement_after_continue.jt
│ │ └── template.jt
│ ├── mappings
│ │ ├── all_features.json
│ │ ├── context_vars_mapping.json
│ │ ├── data.ts
│ │ ├── filters.json
│ │ ├── index_mappings.json
│ │ ├── invalid_array_index_mappings.json
│ │ ├── invalid_array_mappings.json
│ │ ├── invalid_object_mappings.json
│ │ ├── mappings_with_root_fields.json
│ │ ├── missing_array_index_mappings.json
│ │ ├── nested_mappings.json
│ │ ├── non_path_output.json
│ │ ├── object_mappings.json
│ │ ├── or_mappings.json
│ │ ├── override_nested_arrays.json
│ │ ├── root_array_mappings.json
│ │ ├── root_context_vars_mapping.json
│ │ ├── root_index_mappings.json
│ │ ├── root_mappings.json
│ │ ├── root_nested_mappings.json
│ │ ├── root_object_mappings.json
│ │ ├── simple_array_mappings.json
│ │ ├── template.jt
│ │ └── transformations.json
│ ├── math
│ │ ├── data.ts
│ │ └── template.jt
│ ├── objects
│ │ ├── context_props.jt
│ │ ├── data.ts
│ │ ├── invalid_context_prop.jt
│ │ └── template.jt
│ ├── paths
│ │ ├── block.jt
│ │ ├── data.ts
│ │ ├── json_path.jt
│ │ ├── options.jt
│ │ ├── rich_path.jt
│ │ ├── simple_path.jt
│ │ └── template.jt
│ ├── return
│ │ ├── data.ts
│ │ ├── return_no_value.jt
│ │ ├── return_value.jt
│ │ ├── return_without_condition.jt
│ │ └── statement_after_return.jt
│ ├── selectors
│ │ ├── data.ts
│ │ ├── template.jt
│ │ └── wild_cards.jt
│ ├── standard_functions
│ │ ├── data.ts
│ │ └── template.jt
│ ├── statements
│ │ ├── data.ts
│ │ └── template.jt
│ ├── template_strings
│ │ ├── data.ts
│ │ └── template.jt
│ └── throw
│ │ ├── data.ts
│ │ ├── statement_after_throw.jt
│ │ ├── template.jt
│ │ └── throw_without_condition.jt
├── types.ts
└── utils
│ ├── index.ts
│ └── scenario.ts
├── tsconfig.json
└── vite.config.mts
/.eslintignore:
--------------------------------------------------------------------------------
1 | build
2 | *.jt
3 | coverage
4 | test
5 | commitlint.config.js
6 | jest.config.ts
7 | vite.config.*ts
8 | app.ts
9 | *.properties
10 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es2021": true
5 | },
6 | "plugins": ["@typescript-eslint"],
7 | "extends": ["airbnb-base", "airbnb-typescript/base", "plugin:sonarjs/recommended", "prettier"],
8 | "parser": "@typescript-eslint/parser",
9 | "overrides": [],
10 | "parserOptions": {
11 | "requireConfigFile": false,
12 | "ecmaVersion": 12,
13 | "sourceType": "module",
14 | "project": "./tsconfig.json"
15 | },
16 | "rules": {
17 | "max-classes-per-file": "off",
18 | "import/prefer-default-export": "off",
19 | "@typescript-eslint/no-implied-eval": "warn",
20 | "@typescript-eslint/no-unused-vars": [
21 | "error",
22 | {
23 | "args": "all",
24 | "argsIgnorePattern": "^_"
25 | }
26 | ],
27 | "no-plusplus": "off",
28 | "consistent-return": "off", // https://eslint.org/docs/latest/rules/consistent-return#when-not-to-use-it
29 | "class-methods-use-this": "off",
30 | "no-param-reassign": "warn",
31 | "no-restricted-syntax": "off",
32 | "no-cond-assign": "warn",
33 | "no-useless-constructor": "warn",
34 | "@typescript-eslint/no-useless-constructor": "warn"
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: '[ISSUE]'
5 | labels: bug
6 | assignees: ''
7 | ---
8 |
9 | **Describe the issue**
10 | Enter a clear and concise description of what the bug/issue is.
11 |
12 | **To Reproduce**
13 | Mention the steps to reproduce the behavior that causes the bug/issue:
14 |
15 | **Expected behavior**
16 | A clear and concise description of what you expected to happen.
17 |
18 | **Screenshots**
19 | If applicable, add screenshots to help explain your problem.
20 |
21 | **Required information (please complete the following information):**
22 |
23 | - Package version: [e.g., v0.1.1]
24 |
25 | **Additional context**
26 | Add any other context about the problem here.
27 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature-request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature Request
3 | about: Suggest an idea for this project
4 | title: '[ENHANCEMENT]'
5 | labels: enhancement
6 | assignees: ''
7 | ---
8 |
9 | **Is your feature request related to a problem? Please describe.**
10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
11 |
12 | **Describe the solution you'd like**
13 | A clear and concise description of what you want to happen.
14 |
15 | **Describe alternatives you've considered**
16 | A clear and concise description of any alternative solutions or features you've considered.
17 |
18 | **Additional context**
19 | Add any other context or screenshots about the feature request here.
20 |
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 | ## What are the changes introduced in this PR?
2 |
3 | Write a brief explainer on your code changes.
4 |
5 | ## What is the related Linear task?
6 |
7 | Resolves INT-XXX
8 |
9 | ## Please explain the objectives of your changes below
10 |
11 | Put down any required details on the broader aspect of your changes. If there are any dependent changes, **mandatorily** mention them here
12 |
13 | ### Any changes to existing capabilities/behaviour, mention the reason & what are the changes ?
14 |
15 | N/A
16 |
17 | ### Any new dependencies introduced with this change?
18 |
19 | N/A
20 |
21 | ### Any new generic utility introduced or modified. Please explain the changes.
22 |
23 | N/A
24 |
25 | ### Any technical or performance related pointers to consider with the change?
26 |
27 | N/A
28 |
29 | @coderabbitai review
30 |
31 |
32 |
33 | ### Developer checklist
34 |
35 | - [ ] My code follows the style guidelines of this project
36 |
37 | - [ ] **No breaking changes are being introduced.**
38 |
39 | - [ ] All related docs linked with the PR?
40 |
41 | - [ ] All changes manually tested?
42 |
43 | - [ ] Any documentation changes needed with this change?
44 |
45 | - [ ] Is the PR limited to 10 file changes?
46 |
47 | - [ ] Is the PR limited to one linear task?
48 |
49 | - [ ] Are relevant unit and component test-cases added?
50 |
51 | ### Reviewer checklist
52 |
53 | - [ ] Is the type of change in the PR title appropriate as per the changes?
54 |
55 | - [ ] Verified that there are no credentials or confidential data exposed with the changes.
56 |
--------------------------------------------------------------------------------
/.github/workflows/codecov.yml:
--------------------------------------------------------------------------------
1 | # https://github.com/marketplace/actions/jest-coverage-report
2 | name: 'Code Coverage'
3 | on:
4 | pull_request:
5 | branches:
6 | - '*'
7 | jobs:
8 | coverage:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v4
12 | - uses: ArtiomTr/jest-coverage-report-action@v2
13 |
--------------------------------------------------------------------------------
/.github/workflows/commitlint.yml:
--------------------------------------------------------------------------------
1 | name: Commitlint
2 |
3 | on: [push]
4 |
5 | jobs:
6 | commitlint:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - name: Checkout
10 | uses: actions/checkout@v4
11 | with:
12 | fetch-depth: 0
13 |
14 | - name: Setup Node
15 | uses: actions/setup-node@v4
16 | with:
17 | node-version-file: '.nvmrc'
18 | cache: 'npm'
19 |
20 | - name: Install Dependencies
21 | run: npm ci
22 |
23 | - name: Print versions
24 | run: |
25 | git --version
26 | node --version
27 | npm --version
28 | npx commitlint --version
29 |
30 | # Run the commitlint action, considering its own dependencies and yours as well 🚀
31 | # `github.workspace` is the path to your repository.
32 | - uses: wagoid/commitlint-github-action@v6
33 | env:
34 | NODE_PATH: ${{ github.workspace }}/node_modules
35 | with:
36 | commitDepth: 1
37 |
--------------------------------------------------------------------------------
/.github/workflows/housekeeping.yml:
--------------------------------------------------------------------------------
1 | name: Handle stale PRs
2 |
3 | on:
4 | schedule:
5 | - cron: '42 1 * * *'
6 |
7 | jobs:
8 | prs:
9 | name: Clean up stale PRs
10 | runs-on: ubuntu-latest
11 |
12 | permissions:
13 | pull-requests: write
14 |
15 | steps:
16 | - uses: actions/stale@v9
17 | with:
18 | repo-token: ${{ secrets.GITHUB_TOKEN }}
19 | operations-per-run: 200
20 | stale-pr-message: 'This PR is considered to be stale. It has been open 20 days with no further activity thus it is going to be closed in 7 days. To avoid such a case please consider removing the stale label manually or add a comment to the PR.'
21 | days-before-pr-stale: 20
22 | days-before-pr-close: 7
23 | stale-pr-label: 'Stale'
24 |
25 | branches:
26 | name: Cleanup old branches
27 | runs-on: ubuntu-latest
28 | steps:
29 | - name: Checkout
30 | uses: actions/checkout@v4
31 |
32 | - name: Run delete-old-branches-action
33 | uses: beatlabs/delete-old-branches-action@v0.0.10
34 | with:
35 | repo_token: ${{ github.token }}
36 | date: '6 months ago'
37 | dry_run: false
38 | delete_tags: false
39 | extra_protected_branch_regex: ^(main|master|release.*|rudder-saas)$
40 | exclude_open_pr_branches: true
41 |
--------------------------------------------------------------------------------
/.github/workflows/pr-description-enforcer.yml:
--------------------------------------------------------------------------------
1 | name: 'Pull Request Description'
2 | on:
3 | pull_request:
4 | types:
5 | - opened
6 | - edited
7 | - reopened
8 |
9 | jobs:
10 | enforce:
11 | runs-on: ubuntu-latest
12 |
13 | steps:
14 | - uses: rudderlabs/pr-description-enforcer@v1.1.0
15 | with:
16 | repo-token: '${{ secrets.GITHUB_TOKEN }}'
17 |
--------------------------------------------------------------------------------
/.github/workflows/release-please.yml:
--------------------------------------------------------------------------------
1 | on:
2 | push:
3 | branches:
4 | - 'main'
5 | - 'release/*'
6 |
7 | name: release-please
8 |
9 | jobs:
10 | release:
11 | runs-on: ubuntu-latest
12 | outputs:
13 | release_created: ${{ steps.release.outputs.release_created }}
14 | steps:
15 | - name: Extract Branch Name
16 | shell: bash
17 | run: echo "##[set-output name=branch;]$(echo ${GITHUB_REF#refs/heads/})"
18 | id: extract_branch
19 |
20 | - uses: googleapis/release-please-action@v4
21 | id: release
22 | with:
23 | token: ${{ github.token }}
24 | release-type: node
25 |
26 | publish:
27 | runs-on: ubuntu-latest
28 | needs: release
29 | if: ${{ needs.release.outputs.release_created }}
30 |
31 | steps:
32 | # The logic below handles the npm publication:
33 | - name: Checkout
34 | uses: actions/checkout@v4
35 | # these if statements ensure that a publication only occurs when
36 | # a new release is created:
37 |
38 | - name: Setup Node
39 | uses: actions/setup-node@v4
40 | with:
41 | node-version-file: '.nvmrc'
42 | cache: 'npm'
43 |
44 | - name: Install Dependencies
45 | run: npm ci
46 |
47 | - name: Build Package
48 | run: npm run build
49 |
50 | - name: Configure NPM
51 | run: npm set //registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}
52 |
53 | - name: Publish Package to NPM
54 | run: npm publish
55 | env:
56 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
57 |
--------------------------------------------------------------------------------
/.github/workflows/semantic-pr.yml:
--------------------------------------------------------------------------------
1 | name: 'Semantic Pull Requests'
2 |
3 | on:
4 | pull_request:
5 | types:
6 | - opened
7 | - edited
8 | - labeled
9 | - unlabeled
10 | - converted_to_draft
11 | - ready_for_review
12 | - synchronize
13 |
14 | jobs:
15 | main:
16 | name: Validate PR Title
17 | runs-on: ubuntu-latest
18 | steps:
19 | - uses: amannn/action-semantic-pull-request@v5
20 | env:
21 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
22 | with:
23 | types: |
24 | fix
25 | feat
26 | chore
27 | refactor
28 | exp
29 | docs
30 | test
31 | requireScope: false
32 | subjectPattern: ^(?![A-Z]).+$
33 | subjectPatternError: |
34 | The subject "{subject}" found in the pull request title "{title}"
35 | didn't match the configured pattern. Please ensure that the subject
36 | doesn't start with an uppercase character.
37 | # For work-in-progress PRs you can typically use draft pull requests
38 | # from GitHub. However, private repositories on the free plan don't have
39 | # this option and therefore this action allows you to opt-in to using the
40 | # special "[WIP]" prefix to indicate this state. This will avoid the
41 | # validation of the PR title and the pull request checks remain pending.
42 | # Note that a second check will be reported if this is enabled.
43 | wip: true
44 | # When using "Squash and merge" on a PR with only one commit, GitHub
45 | # will suggest using that commit message instead of the PR title for the
46 | # merge commit, and it's easy to commit this by mistake. Enable this option
47 | # to also validate the commit message for one commit PRs.
48 | validateSingleCommit: true
49 | # Related to `validateSingleCommit` you can opt-in to validate that the PR
50 | # title matches a single commit to avoid confusion.
51 | validateSingleCommitMatchesPrTitle: true
52 | # If the PR contains one of these labels, the validation is skipped.
53 | # Multiple labels can be separated by newlines.
54 | # If you want to rerun the validation when labels change, you might want
55 | # to use the `labeled` and `unlabeled` event triggers in your workflow.
56 | ignoreLabels: |
57 | bot
58 | dependencies
59 |
--------------------------------------------------------------------------------
/.github/workflows/tests.yml:
--------------------------------------------------------------------------------
1 | name: Tests
2 |
3 | on: pull_request
4 |
5 | jobs:
6 | run-tests:
7 | name: Run Tests
8 | runs-on: ubuntu-latest
9 | steps:
10 | - name: Checkout
11 | uses: actions/checkout@v4
12 |
13 | - name: Setup Node
14 | uses: actions/setup-node@v4
15 | with:
16 | node-version-file: '.nvmrc'
17 | cache: 'npm'
18 |
19 | - name: Install Dependencies
20 | run: npm ci
21 |
22 | - name: Run Tests
23 | run: npm run test
24 |
25 | - name: Run Lint Checks
26 | run: |
27 | npm run check:lint
28 |
29 | - name: Upload coverage reports to Codecov
30 | uses: codecov/codecov-action@v5
31 | with:
32 | token: ${{ secrets.CODECOV_TOKEN }}
33 | directory: ./reports/coverage
34 |
35 | - name: Update sonar-project.properties
36 | run: |
37 | # Retrieve the version from package.json
38 | version=$(node -e "console.log(require('./package.json').version)")
39 | # Update the sonar-project.properties file with the version
40 | sed -i "s/sonar.projectVersion=.*$/sonar.projectVersion=$version/" sonar-project.properties
41 |
42 | - name: Fix filesystem paths in generated reports
43 | if: always()
44 | run: |
45 | sed -i 's+home/runner/work/rudder-json-template-engine/rudder-json-template-engine+/github/workspace+g' reports/coverage/lcov.info
46 | sed -i 's+/home/runner/work/rudder-json-template-engine/rudder-json-template-engine+/github/workspace+g' reports/eslint.json
47 |
48 | - name: SonarCloud Scan
49 | if: always()
50 | uses: SonarSource/sonarqube-scan-action@v4
51 | env:
52 | GITHUB_TOKEN: ${{ secrets.PAT }}
53 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
54 |
--------------------------------------------------------------------------------
/.github/workflows/verify.yml:
--------------------------------------------------------------------------------
1 | name: Verify
2 |
3 | on:
4 | pull_request:
5 |
6 | jobs:
7 | formatting-lint:
8 | name: Check for formatting & lint errors
9 | runs-on: ubuntu-latest
10 |
11 | steps:
12 | - name: Checkout
13 | uses: actions/checkout@v4
14 | with:
15 | # Make sure the actual branch is checked out when running on pull requests
16 | ref: ${{ github.head_ref }}
17 |
18 | - name: Setup Node
19 | uses: actions/setup-node@v4
20 | with:
21 | node-version-file: .nvmrc
22 | cache: 'npm'
23 |
24 | - name: Install Dependencies
25 | run: npm ci
26 |
27 | - name: Run Lint Checks
28 | run: |
29 | npm run lint
30 |
31 | - run: git diff --exit-code
32 |
33 | - name: Error message
34 | if: ${{ failure() }}
35 | run: |
36 | echo 'Eslint check is failing Ensure you have run `npm run lint` and committed the files locally.'
37 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 | .pnpm-debug.log*
9 |
10 | # Diagnostic reports (https://nodejs.org/api/report.html)
11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
12 | reports
13 |
14 | # Runtime data
15 | pids
16 | *.pid
17 | *.seed
18 | *.pid.lock
19 |
20 | # Directory for instrumented libs generated by jscoverage/JSCover
21 | lib-cov
22 |
23 | # Coverage directory used by tools like istanbul
24 | coverage
25 | *.lcov
26 |
27 | # nyc test coverage
28 | .nyc_output
29 |
30 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
31 | .grunt
32 |
33 | # Bower dependency directory (https://bower.io/)
34 | bower_components
35 |
36 | # node-waf configuration
37 | .lock-wscript
38 |
39 | # Compiled binary addons (https://nodejs.org/api/addons.html)
40 | build/Release
41 |
42 | # Dependency directories
43 | node_modules/
44 | jspm_packages/
45 |
46 | # Snowpack dependency directory (https://snowpack.dev/)
47 | web_modules/
48 |
49 | # TypeScript cache
50 | *.tsbuildinfo
51 |
52 | # Optional npm cache directory
53 | .npm
54 |
55 | # Optional eslint cache
56 | .eslintcache
57 |
58 | # Optional stylelint cache
59 | .stylelintcache
60 |
61 | # Microbundle cache
62 | .rpt2_cache/
63 | .rts2_cache_cjs/
64 | .rts2_cache_es/
65 | .rts2_cache_umd/
66 |
67 | # Optional REPL history
68 | .node_repl_history
69 |
70 | # Output of 'npm pack'
71 | *.tgz
72 |
73 | # Yarn Integrity file
74 | .yarn-integrity
75 |
76 | # dotenv environment variable files
77 | .env
78 | .env.development.local
79 | .env.test.local
80 | .env.production.local
81 | .env.local
82 |
83 | # parcel-bundler cache (https://parceljs.org/)
84 | .cache
85 | .parcel-cache
86 |
87 | # Next.js build output
88 | .next
89 | out
90 |
91 | # Nuxt.js build / generate output
92 | .nuxt
93 | dist
94 |
95 | # Gatsby files
96 | .cache/
97 | # Comment in the public line in if your project uses Gatsby and not Next.js
98 | # https://nextjs.org/blog/next-9-1#public-directory-support
99 | # public
100 |
101 | # vuepress build output
102 | .vuepress/dist
103 |
104 | # vuepress v2.x temp and cache directory
105 | .temp
106 | .cache
107 |
108 | # Docusaurus cache and generated files
109 | .docusaurus
110 |
111 | # Serverless directories
112 | .serverless/
113 |
114 | # FuseBox cache
115 | .fusebox/
116 |
117 | # DynamoDB Local files
118 | .dynamodb/
119 |
120 | # TernJS port file
121 | .tern-port
122 |
123 | # Stores VSCode versions used for testing VSCode extensions
124 | .vscode-test
125 |
126 | # yarn v2
127 | .yarn/cache
128 | .yarn/unplugged
129 | .yarn/build-state.yml
130 | .yarn/install-state.gz
131 | .pnp.*
132 |
133 | # build files
134 | build/
135 |
136 | # stryker temp files
137 | .stryker-tmp
138 |
139 | Mac
140 | .DS_Store
141 |
142 | test/test_engine.ts
--------------------------------------------------------------------------------
/.husky/commit-msg:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | . "$(dirname -- "$0")/_/husky.sh"
3 |
4 | npx commitlint --edit $1
5 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | . "$(dirname -- "$0")/_/husky.sh"
3 |
4 | npm test
5 | npm run lint:fix
6 | npm run lint:check
7 | npm run lint-staged
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | 20.18.3
2 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | build/
2 | node_modules/
3 | coverage/
4 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "all",
3 | "tabWidth": 2,
4 | "semi": true,
5 | "singleQuote": true,
6 | "printWidth": 100
7 | }
8 |
--------------------------------------------------------------------------------
/.release-please-manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | ".": "0.19.5"
3 | }
4 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "name": "Test Current TS File",
9 | "program": "${relativeFile}",
10 | "request": "launch",
11 | "runtimeArgs": ["--nolazy", "-r", "ts-node/register/transpile-only"],
12 | "skipFiles": ["/**"],
13 | "type": "node"
14 | },
15 | {
16 | "name": "Run Current JS File",
17 | "program": "${relativeFile}",
18 | "request": "launch",
19 | "skipFiles": ["/**"],
20 | "type": "node"
21 | },
22 | {
23 | "runtimeExecutable": "/usr/local/bin/node",
24 | "type": "node",
25 | "request": "launch",
26 | "name": "Jest Current File",
27 | "program": "${workspaceFolder}/node_modules/.bin/jest",
28 | "args": ["${fileBasenameNoExtension}", "--config", "jest.config.ts"],
29 | "console": "integratedTerminal",
30 | "internalConsoleOptions": "neverOpen",
31 | "windows": {
32 | "program": "${workspaceFolder}/node_modules/jest/bin/jest"
33 | }
34 | },
35 | {
36 | "runtimeExecutable": "/usr/local/bin/node",
37 | "name": "Jest Scenario",
38 | "program": "${workspaceFolder}/node_modules/.bin/jest",
39 | "request": "launch",
40 | "skipFiles": ["/**"],
41 | "args": [
42 | "scenario.test",
43 | "--config",
44 | "jest.config.ts",
45 | "--scenario=${input:scenario}",
46 | "--index=${input:index}"
47 | ],
48 | "type": "node",
49 | "console": "integratedTerminal",
50 | "internalConsoleOptions": "neverOpen",
51 | "windows": {
52 | "program": "${workspaceFolder}/node_modules/jest/bin/jest"
53 | }
54 | },
55 | {
56 | "runtimeExecutable": "/usr/local/bin/node",
57 | "type": "node",
58 | "request": "launch",
59 | "name": "Jest Scenarios",
60 | "program": "${workspaceFolder}/node_modules/.bin/jest",
61 | "args": ["test/e2e.test.ts", "--config", "jest.config.ts", "--scenarios=${input:scenarios}"],
62 | "console": "integratedTerminal",
63 | "internalConsoleOptions": "neverOpen",
64 | "windows": {
65 | "program": "${workspaceFolder}/node_modules/jest/bin/jest"
66 | }
67 | },
68 | {
69 | "name": "Test Engine",
70 | "program": "${workspaceFolder}/test/test_engine.ts",
71 | "request": "launch",
72 | "runtimeArgs": ["--nolazy", "-r", "ts-node/register/transpile-only"],
73 | "skipFiles": ["/**"],
74 | "type": "node"
75 | }
76 | ],
77 | "inputs": [
78 | {
79 | "id": "scenarios",
80 | "type": "promptString",
81 | "description": "Enter Scenarios",
82 | "default": "all"
83 | },
84 | {
85 | "id": "scenario",
86 | "type": "promptString",
87 | "description": "Enter Scenario",
88 | "default": "assignments"
89 | },
90 | {
91 | "id": "index",
92 | "type": "promptString",
93 | "description": "Enter test index",
94 | "default": "0"
95 | }
96 | ]
97 | }
98 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "prettier.requireConfig": true,
3 | "prettier.configPath": ".prettierrc",
4 | "[typescript]": {
5 | "editor.defaultFormatter": "esbenp.prettier-vscode",
6 | "editor.formatOnSave": true
7 | },
8 | "[javascript]": {
9 | "editor.defaultFormatter": "esbenp.prettier-vscode",
10 | "editor.formatOnSave": true
11 | },
12 | "[jsonc]": {
13 | "editor.defaultFormatter": "esbenp.prettier-vscode",
14 | "editor.formatOnSave": true
15 | },
16 | "[yaml]": {
17 | "editor.defaultFormatter": "esbenp.prettier-vscode",
18 | "editor.formatOnSave": true
19 | },
20 | "editor.codeActionsOnSave": {
21 | "source.organizeImports": "never"
22 | },
23 | "eslint.validate": ["javascript", "typescript"]
24 | }
25 |
--------------------------------------------------------------------------------
/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @rudderlabs/integrations
2 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, sex characteristics, gender identity and expression,
9 | level of experience, education, socio-economic status, nationality, personal
10 | appearance, race, religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | - Using welcoming and inclusive language
18 | - Being respectful of differing viewpoints and experiences
19 | - Gracefully accepting constructive criticism
20 | - Focusing on what is best for the community
21 | - Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | - The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | - Trolling, insulting/derogatory comments, and personal or political attacks
28 | - Public or private harassment
29 | - Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | - Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at **contact@rudderstack.com**. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at [https://www.contributor-covenant.org][contributor-covenant].
72 |
73 | For answers to common questions about this code of conduct, see the [FAQs][faqs].
74 |
75 |
76 |
77 | [homepage]: https://www.contributor-covenant.org
78 | [contributor-covenant]: https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
79 | [faqs]: https://www.contributor-covenant.org/faq
80 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to RudderStack
2 |
3 | Thanks for taking the time and for your help in improving this project!
4 |
5 | ## Table of contents
6 |
7 | - [**RudderStack Contributor Agreement**](#rudderstack-contributor-agreement)
8 | - [**How you can contribute to RudderStack**](#how-you-can-contribute-to-rudderstack)
9 | - [**Committing**](#committing)
10 | - [**Getting help**](#getting-help)
11 |
12 | ## RudderStack Contributor Agreement
13 |
14 | To contribute to this project, we need you to sign the [**Contributor License Agreement (“CLA”)**][CLA] for the first commit you make. By agreeing to the [**CLA**][CLA], we can add you to list of approved contributors and review the changes proposed by you.
15 |
16 | ## How you can contribute to RudderStack
17 |
18 | If you come across any issues or bugs, or have any suggestions for improvement, you can navigate to the specific file in the [**repo**](https://github.com/rudderlabs/rudder-repo-template), make the change, and raise a PR.
19 |
20 | You can also contribute to any open-source RudderStack project. View our [**GitHub page**](https://github.com/rudderlabs) to see all the different projects.
21 |
22 | ## Committing
23 |
24 | We prefer squash or rebase commits so that all changes from a branch are committed to master as a single commit. All pull requests are squashed when merged, but rebasing prior to merge gives you better control over the commit message.
25 |
26 | ## Getting help
27 |
28 | For any questions, concerns, or queries, you can start by asking a question in our [**Slack**](https://rudderstack.com/join-rudderstack-slack-community/) community.
29 |
30 | ### We look forward to your feedback on improving this project!
31 |
32 |
33 |
34 | [issue]: https://github.com/rudderlabs/rudder-server/issues/new
35 | [CLA]: https://rudderlabs.wufoo.com/forms/rudderlabs-contributor-license-agreement
36 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 RudderStack
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 | Copyright (c) 2012 Dmitry Filatov
24 |
25 | Permission is hereby granted, free of charge, to any person obtaining a copy
26 | of this software and associated documentation files (the "Software"), to deal
27 | in the Software without restriction, including without limitation the rights
28 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
29 | copies of the Software, and to permit persons to whom the Software is
30 | furnished to do so, subject to the following conditions:
31 |
32 | The above copyright notice and this permission notice shall be included in
33 | all copies or substantial portions of the Software.
34 |
35 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
36 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
37 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
38 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
39 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
40 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
41 | THE SOFTWARE.
42 |
--------------------------------------------------------------------------------
/commitlint.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: ['@commitlint/config-conventional'],
3 | };
4 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Rudder Json Template Engine
8 |
9 |
10 |
11 |
12 |
13 |
Template:
14 |
17 |
18 |
19 |
Data:
20 |
23 |
24 |
28 |
29 |
30 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/jest.config.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * For a detailed explanation regarding each configuration property and type check, visit:
3 | * https://jestjs.io/docs/configuration
4 | */
5 |
6 | export default {
7 | // All imported modules in your tests should be mocked automatically
8 | // automock: false,
9 |
10 | // Stop running tests after `n` failures
11 | // bail: 0,
12 |
13 | // The directory where Jest should store its cached dependency information
14 | // cacheDirectory: "/private/var/folders/62/f7brm_9n2p75sc79zx9718_w0000gp/T/jest_dy",
15 |
16 | // Automatically clear mock calls, instances, contexts and results before every test
17 | clearMocks: true,
18 |
19 | // Indicates whether the coverage information should be collected while executing the test
20 | // collectCoverage: false,
21 |
22 | // An array of glob patterns indicating a set of files for which coverage information should be collected
23 | collectCoverageFrom: ['/src/**/*.[jt]s?(x)'],
24 |
25 | // The directory where Jest should output its coverage files
26 | coverageDirectory: 'reports/coverage',
27 |
28 | // An array of regexp pattern strings used to skip coverage collection
29 | coveragePathIgnorePatterns: ['/node_modules/', '/build/', 'test', 'vite-env.d.ts'],
30 |
31 | // Indicates which provider should be used to instrument code for coverage
32 | coverageProvider: 'v8',
33 |
34 | // A list of reporter names that Jest uses when writing coverage reports
35 | coverageReporters: ['json', 'text', 'lcov', 'clover'],
36 |
37 | // An object that configures minimum threshold enforcement for coverage results
38 | coverageThreshold: {
39 | // global: {
40 | // branches: 100,
41 | // functions: 100,
42 | // lines: 100,
43 | // statements: 100,
44 | // },
45 | },
46 |
47 | // A path to a custom dependency extractor
48 | // dependencyExtractor: undefined,
49 |
50 | // Make calling deprecated APIs throw helpful error messages
51 | // errorOnDeprecated: false,
52 |
53 | // The default configuration for fake timers
54 | // fakeTimers: {
55 | // "enableGlobally": false
56 | // },
57 |
58 | // Force coverage collection from ignored files using an array of glob patterns
59 | // forceCoverageMatch: [],
60 |
61 | // A path to a module which exports an async function that is triggered once before all test suites
62 | // globalSetup: undefined,
63 |
64 | // A path to a module which exports an async function that is triggered once after all test suites
65 | // globalTeardown: undefined,
66 |
67 | // A set of global variables that need to be available in all test environments
68 | // globals: {},
69 |
70 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
71 | // maxWorkers: "50%",
72 |
73 | // An array of directory names to be searched recursively up from the requiring module's location
74 | // moduleDirectories: [
75 | // "node_modules"
76 | // ],
77 |
78 | // An array of file extensions your modules use
79 | // moduleFileExtensions: [
80 | // "js",
81 | // "mjs",
82 | // "cjs",
83 | // "jsx",
84 | // "ts",
85 | // "tsx",
86 | // "json",
87 | // "node"
88 | // ],
89 |
90 | // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
91 | // moduleNameMapper: {},
92 |
93 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
94 | // modulePathIgnorePatterns: [],
95 |
96 | // Activates notifications for test results
97 | // notify: false,
98 |
99 | // An enum that specifies notification mode. Requires { notify: true }
100 | // notifyMode: "failure-change",
101 |
102 | // A preset that is used as a base for Jest's configuration
103 | preset: 'ts-jest',
104 |
105 | // Run tests from one or more projects
106 | // projects: undefined,
107 |
108 | // Use this configuration option to add custom reporters to Jest
109 | // reporters: undefined,
110 |
111 | // Automatically reset mock state before every test
112 | // resetMocks: false,
113 |
114 | // Reset the module registry before running each individual test
115 | // resetModules: false,
116 |
117 | // A path to a custom resolver
118 | // resolver: undefined,
119 |
120 | // Automatically restore mock state and implementation before every test
121 | // restoreMocks: false,
122 |
123 | // The root directory that Jest should scan for tests and modules within
124 | // rootDir: undefined,
125 |
126 | // A list of paths to directories that Jest should use to search for files in
127 | // roots: [
128 | // ""
129 | // ],
130 |
131 | // Allows you to use a custom runner instead of Jest's default test runner
132 | // runner: "jest-runner",
133 |
134 | // The paths to modules that run some code to configure or set up the testing environment before each test
135 | // setupFiles: [],
136 |
137 | // A list of paths to modules that run some code to configure or set up the testing framework before each test
138 | // setupFilesAfterEnv: [],
139 |
140 | // The number of seconds after which a test is considered as slow and reported as such in the results.
141 | // slowTestThreshold: 5,
142 |
143 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing
144 | // snapshotSerializers: [],
145 |
146 | // The test environment that will be used for testing
147 | testEnvironment: 'node',
148 |
149 | // Options that will be passed to the testEnvironment
150 | // testEnvironmentOptions: {},
151 |
152 | // Adds a location field to test results
153 | // testLocationInResults: false,
154 |
155 | // The glob patterns Jest uses to detect test files
156 | testMatch: ['/**/__tests__/**/*.[jt]s?(x)', '/**/?(*.)+(spec|test).[tj]s?(x)'],
157 |
158 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
159 | testPathIgnorePatterns: ['/node_modules/', '/build/'],
160 |
161 | // The regexp pattern or array of patterns that Jest uses to detect test files
162 | // testRegex: [],
163 |
164 | // This option allows the use of a custom results processor
165 | // testResultsProcessor: undefined,
166 |
167 | // This option allows use of a custom test runner
168 | // testRunner: "jest-circus/runner",
169 |
170 | // A map from regular expressions to paths to transformers
171 | // transform: undefined,
172 |
173 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
174 | // transformIgnorePatterns: [
175 | // "/node_modules/",
176 | // "\\.pnp\\.[^\\/]+$"
177 | // ],
178 |
179 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
180 | // unmockedModulePathPatterns: undefined,
181 |
182 | // Indicates whether each individual test should be reported during the run
183 | // verbose: undefined,
184 |
185 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
186 | // watchPathIgnorePatterns: [],
187 |
188 | // Whether to use watchman for file crawling
189 | // watchman: true,
190 | };
191 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@rudderstack/json-template-engine",
3 | "version": "0.19.5",
4 | "homepage": "https://github.com/rudderlabs/rudder-json-template-engine",
5 | "description": "A library for evaluating JSON template expressions.",
6 | "main": "build/index.js",
7 | "types": "build/index.d.ts",
8 | "keywords": [
9 | "json",
10 | "jsonpath",
11 | "rudder",
12 | "rudderstack",
13 | "cdp",
14 | "engine"
15 | ],
16 | "author": "RudderStack",
17 | "license": "MIT",
18 | "repository": {
19 | "type": "git",
20 | "url": "https://github.com/rudderlabs/rudder-json-template-engine.git"
21 | },
22 | "publishConfig": {
23 | "access": "public"
24 | },
25 | "devDependencies": {
26 | "@babel/eslint-parser": "^7.19.1",
27 | "@commitlint/cli": "^17.8.1",
28 | "@commitlint/config-conventional": "^17.8.1",
29 | "@types/jest": "^29.4.0",
30 | "@types/mocha": "^10.0.1",
31 | "@types/node": "^18.14.6",
32 | "@typescript-eslint/eslint-plugin": "^6.19.1",
33 | "@typescript-eslint/parser": "^6.19.1",
34 | "commander": "^10.0.0",
35 | "eslint": "^8.35.0",
36 | "eslint-config-airbnb-base": "^15.0.0",
37 | "eslint-config-airbnb-typescript": "^17.1.0",
38 | "eslint-config-prettier": "^8.7.0",
39 | "eslint-plugin-import": "^2.27.5",
40 | "eslint-plugin-promise": "^6.1.1",
41 | "eslint-plugin-sonarjs": "^0.23.0",
42 | "glob": "^10.3.10",
43 | "husky": "^8.0.3",
44 | "jest": "^29.4.3",
45 | "lint-staged": "^15.2.10",
46 | "prettier": "^2.8.4",
47 | "ts-jest": "^29.0.5",
48 | "ts-node": "^10.9.1",
49 | "typescript": "^5.0.0",
50 | "vite": "^6.2.1"
51 | },
52 | "scripts": {
53 | "test": "jest --coverage --verbose",
54 | "build": "vite build && tsc",
55 | "dev": "vite",
56 | "clean": "rm -rf build",
57 | "build:clean": "npm run clean && npm run build",
58 | "lint:fix": "eslint . --fix",
59 | "lint:check": "eslint . || exit 1",
60 | "format": "prettier --write '**/*.ts' '**/*.js' '**/*.json'",
61 | "lint": "npm run format && npm run lint:fix",
62 | "lint-staged": "lint-staged",
63 | "prepare": "husky install",
64 | "jest:scenarios": "jest e2e.test.ts --verbose",
65 | "test:scenario": "jest test/scenario.test.ts --verbose",
66 | "test:stryker": "stryker run",
67 | "check:lint": "eslint . -f json -o reports/eslint.json || exit 0"
68 | },
69 | "lint-staged": {
70 | "*.(ts|json)": "prettier --write"
71 | },
72 | "files": [
73 | "build/**/*.[jt]s",
74 | "CHANGELOG.md"
75 | ]
76 | }
77 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rudderlabs/rudder-json-template-engine/b8cfa75cc7a5084cff6a88213e795676eb9bb0db/public/favicon.ico
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | The Customer Data Platform for Developers
8 |
9 |
10 |
11 | Website
12 | ·
13 | Documentation
14 | ·
15 | Community Slack
16 |
17 |
18 |
19 | ---
20 |
21 | # JSON Template Engine
22 |
23 | ## Overview
24 |
25 | Welcome to our JSON Template Engine! This powerful tool simplifies transforming JSON data from one format to another, making managing and maintaining complex integrations easier.
26 |
27 | ### Why JSON Template Engine?
28 |
29 | As an integration platform supporting over 200 integrations, we understand the challenges of maintaining and optimizing these connections. Traditionally, we used native JavaScript code for data transformation, which required significant effort and maintenance. While JSONata offered a more efficient way to manipulate JSON data, we still encountered performance bottlenecks due to its parsing and interpretation overhead.
30 |
31 | ### Our Solution
32 |
33 | To address these challenges, we've developed our own JSON Transformation Engine. This engine generates optimized JavaScript code from transformation templates, reducing runtime overhead and significantly improving performance.
34 |
35 | ## Key Features
36 |
37 | - **Efficiency**: Our engine generates JavaScript code that minimizes parsing and interpretation overhead, ensuring faster execution.
38 |
39 | - **Extensibility**: Easily add new transformation templates to meet your specific integration needs.
40 |
41 | - **Simplicity**: Write concise transformation templates that are easy to understand and maintain.
42 |
43 | ## Implementation
44 |
45 | This library generates a javascript function code from the template and then uses the function to evaluate the JSON data. It outputs the javascript code in the following stages:
46 |
47 | 1. [Lexing](src/lexer.ts) (Tokenization)
48 | 1. [Parsing](src/parser.ts) (AST Creation)
49 | 1. [Translation](src/translator.ts) (Code generation)
50 |
51 | ```mermaid
52 | flowchart TD;
53 | A[Code] --> B[Convert code to tokens];
54 | B --> C[Parse tokens to create Expressions];
55 | C --> D[Combine expressions to create statements];
56 | D --> E[Combine statements to create AST];
57 | E --> F[Translate AST to JS code]
58 | ```
59 |
60 | [Engine](src/engine.ts) class abstracts the above steps and provides a convenient way to use the json templates to evaluate the inputs.
61 |
62 | ## Getting started
63 |
64 | ### Use npm package
65 |
66 | `npm install @rudderstack/json-template-engine`
67 |
68 | ```ts
69 | const { JsonTemplateEngine } = require('@rudderstack/json-template-engine');
70 | const engine = JsonTemplateEngine.create(`'Hello ' + .name`);
71 | engine.evaluate({ name: 'World' }); // => 'Hello World'
72 | ```
73 |
74 | ### Use CDN URL directly in the browser
75 | Latest URL: https://cdn.jsdelivr.net/npm/@rudderstack/json-template-engine/build/json-template.min.js
76 |
77 | Versioned URL: https://cdn.jsdelivr.net/npm/@rudderstack/json-template-engine@0.19.5/build/json-template.min.js
78 |
79 | ```html
80 |
85 | ```
86 |
87 |
88 | Refer this [example](/index.html) for more details.
89 |
90 | [Demo](https://rudderlabs.github.io/rudder-json-template-engine/)
91 |
92 | ### Playground
93 | Give the JSON template engine a try in our [playground](https://transformers-workflow-engine.rudderstack.com/#/json-template) without needing to install anything.
94 |
95 | ## Features
96 |
97 | The template consists of multiple statements, with the output being the result of the final statement.
98 |
99 |
100 | ### Variables
101 |
102 | ```js
103 | const a = 1;
104 | let b = a + 2;
105 | a + b;
106 | ```
107 |
108 | Refer this [example](test/scenarios/assignments/template.jt) for more details.
109 |
110 | #### Template Strings
111 |
112 | ```js
113 | let a = `Input a=${.a}`;
114 | let b = `Input b=${.b}`;
115 | `${a}, ${b}`;
116 | ```
117 | Refer this [example](test/scenarios/template_strings/template.jt) for more details.
118 |
119 | ### Basic Expressions
120 |
121 | #### Conditions
122 |
123 | ```js
124 | a > b ? a : c;
125 | ```
126 |
127 | Refer this [example](test/scenarios/conditions/template.jt) for more details.
128 |
129 | #### Comparisons
130 |
131 | ```js
132 | a === b || c > d;
133 | ```
134 |
135 | Refer this [example](test/scenarios/comparisons/template.jt) for more details.
136 |
137 | #### Math Operations
138 |
139 | ```js
140 | 10 - 2 + 2 * 10;
141 | ```
142 |
143 | Refer this [example](test/scenarios/math/template.jt) for more details.
144 |
145 | #### Logical operations
146 |
147 | ```js
148 | false || true;
149 | ```
150 |
151 | Refer this [example](test/scenarios/logics/template.jt) for more details.
152 |
153 | ### Input and Bindings
154 |
155 | Input refers to the JSON document we would like to process using a template. Bindings refer to additional data or functions we would provide to process the data efficiently.
156 |
157 | Example:
158 |
159 | - Template: `"Hello " + (.name ?? $.defaultName)`
160 | - Evaluation: `engine.evaluate({name: 'World'}, {defaultName: 'World'});`
161 | - `{name: 'World'}` is input.
162 | - `^.name` refers to "name" property of the input. We can also use `.name` to refer the same. `^` always refers to the root of the input and `.` refers to current context. Refer this [example](test/scenarios/selectors/context_variables.jt) for more details.
163 | - `{defaultName: 'World'}` is bindings.
164 | - `$.defaultName` refers to "defaultName" property of the bindings. Refer this [example](test/scenarios/bindings/template.jt) for more details.
165 |
166 | ### Arrays
167 |
168 | ```js
169 | let arr = [1, 2, 3, 4]
170 | let a = arr[1, 2] // [2, 3]
171 | let b = arr[0:2] // [1, 2]
172 | let c = arr[-2:] // [3, 4]
173 | ```
174 |
175 | Refer this [example](test/scenarios/arrays/template.jt) for more details.
176 |
177 | ### Objects
178 |
179 | ```js
180 | let key = "some key"
181 | // { "a": 1, "b": 2, "c": 3, "some key": 4 }
182 | let obj = {a: 1, b: 2, c: 3, [key]: 4 }
183 | let a = obj["a"] // 1
184 | let b = obj.a // 1
185 | let c = obj{["a", "b"]} // { "a": 1, "b": 2}
186 | let d = obj{~["a", "b"]} // { "c": 3, "some key": 4}
187 | ```
188 |
189 | Refer this [example](test/scenarios/objects/template.jt) for more details.
190 |
191 | #### Object Context Props
192 | ```js
193 | let obj = {a: 1, b: 2, c: 3 };
194 | obj.({
195 | @e [e.key]: e.value * e.value, // @e refers to each key, value pairs,
196 | d: 16 // we can have other props also
197 | }) // { a: 1, b: 4, c: 9, d: 16}
198 | ```
199 | Refer this [example](test/scenarios/objects/context_props.jt) for more details.
200 |
201 | ### Functions
202 |
203 | #### Normal functions
204 |
205 | ```js
206 | let fn = function (arg1, arg2) {
207 | arg1 + arg2;
208 | };
209 | ```
210 |
211 | The result of the last statement of function will be returned as result of the function. We can also use rest params (`...args`).
212 |
213 | #### Lambda/Short functions
214 |
215 | ```js
216 | let fn = array.map(lambda 2 * ?0);
217 | ```
218 |
219 | This function gets converted to:
220 |
221 | ```js
222 | let fn = array.map(function (args) {
223 | 2 * args[0];
224 | });
225 | ```
226 |
227 | Lambda functions are short to express the intention and it is convenient sometimes.
228 |
229 | #### Async functions
230 |
231 | ```js
232 | let fn = async function (arg1, arg2) {
233 | const result = await doSomethingAsync(arg1, arg2);
234 | doSomethingSync(result);
235 | };
236 | ```
237 |
238 | **Note:** When we want to use async functions then we need to create template engine using `JsonTemplateEngine.create`. If you create a template this way then it will be created as an async function so we can `await` anywhere in the template.
239 |
240 | ```js
241 | let result = await doSomething(.a, .b)
242 | ```
243 |
244 | Refer this [example](test/scenarios/functions/template.jt) for more details.
245 |
246 | ### Paths
247 |
248 | Paths are used to access properties in `input`, `bindings` and `variables`.
249 |
250 | #### Simple Paths
251 |
252 | Simple paths support limited path features and get translated as direct property access statements in the generate javascript code.
253 | `a.b.c` gets translated to `a?.b?.c` so they are very fast compared to [Rich paths](#rich-paths). Simple paths are ideal when we know the object structure.
254 |
255 | **Supported features:**
256 |
257 | - [Simple Selectors](#simple-selectors)
258 | - [Single Index Filters](#single-index-or-property-filters)
259 | Refer this [example](test/scenarios/paths/simple_path.jt) for more details.
260 |
261 | #### Rich Paths
262 |
263 | Rich paths gets converted complex code to support different variations in the data.
264 |
265 | If we use this rich path`~r a.b.c` then it automatically handles following variations.
266 |
267 | - `[{"a": { "b": [{"c": 2}]}}]`
268 | - `{"a": { "b": [{"c": 2}]}}`
269 | - `{"a": [{ "b": [{"c": 2}]}]}`
270 | Refer this [example](test/scenarios/paths/rich_path.jt) for more details.
271 |
272 | #### Json Paths
273 | We support some features of [JSON Path](https://goessner.net/articles/JsonPath/index.html#) syntax using path option (`~j`).
274 | Note: This is an experimental feature and may not support all the features of JSON Paths.
275 |
276 | Refer this [example](test/scenarios/paths/json_path.jt) for more details.
277 |
278 | #### Simple selectors
279 |
280 | ```js
281 | let x = a.b.c;
282 | let y = a."some key".c
283 | ```
284 |
285 | Refer this [example](test/scenarios/selectors/template.jt) for more details.
286 |
287 | #### Wildcard selectors
288 |
289 | ```js
290 | a.*.c // selects c from any direct property of a
291 | ```
292 |
293 | Refer this [example](test/scenarios/selectors/wild_cards.jt) for more details.
294 |
295 | #### Descendent selectors
296 |
297 | ```js
298 | // selects c from any child property of a
299 | // a.b.c, a.b1.b2.c or a.b1.b2.b3.c
300 | let x = a..c;
301 | let y = a.."some key";
302 | ```
303 |
304 | Refer this [example](test/scenarios/selectors/template.jt) for more details.
305 |
306 | #### Single Index or Property Filters
307 |
308 | ```js
309 | let x = a[0].c;
310 | let y = a[-1].c; // selects last element from array
311 | let z = a['some key'].c;
312 | ```
313 |
314 | Refer this [example](test/scenarios/filters/array_filters.jt) for more details.
315 |
316 | #### Multi Indexes or Properties Filters
317 |
318 | ```js
319 | let x = a[(0, 2, 5)].c;
320 | let y = a[('some key1', 'some key2')].c;
321 | ```
322 |
323 | Refer this [example](test/scenarios/filters/array_filters.jt) for more details.
324 |
325 | #### Range filters
326 |
327 | ```js
328 | let x = a[2:5].c;
329 | let y = a[:-2].c;
330 | let z = a[2:].c;
331 | ```
332 |
333 | #### Object Property Filters
334 |
335 | ```js
336 | let x = obj{["a", "b"]}; // selects a and b
337 | let y = obj{~["a", "b"]}; // selects all properties except a and b
338 | ```
339 |
340 | Refer this [example](test/scenarios/filters/object_indexes.jt) for more details.
341 |
342 | #### Conditional or Object Filters
343 |
344 | ```js
345 | let x = obj{.a > 1};
346 | ```
347 |
348 | Refer this [example](test/scenarios/filters/object_filters.jt) for more details.
349 |
350 | #### Block expressions
351 |
352 | ```js
353 | let x = obj.({
354 | a: .a + 1,
355 | b: .b + 2
356 | });
357 | let x = obj.([.a+1, .b+2]);
358 | ```
359 |
360 | Refer this [example](test/scenarios/paths/block.jt) for more details.
361 |
362 | #### Context Variables
363 |
364 | ```js
365 | .orders@order#idx.products.({
366 | name: .name,
367 | price: .price,
368 | orderNum: idx,
369 | orderId: order.id
370 | })
371 | ```
372 |
373 | Use context variables: `@order` and `#idx`, we can combine properties of orders and products together. Refer this [example](test/scenarios/context_variables/template.jt) for more details.
374 |
375 | #### Path Options
376 |
377 | We can mention defaultPathType while creating engine instance.
378 |
379 | ```js
380 | // For using simple path as default path type
381 | // a.b.c will be treated as simple path
382 | JsonTemplateEngine.create(`a.b.c`, { defaultPathType: PathType.SIMPLE });
383 | // For using rich path as default path type
384 | // a.b.c will be treated as rich path
385 | JsonTemplateEngine.create(`a.b.c`, { defaultPathType: PathType.RICH });
386 | ```
387 |
388 | We can override the default path option using tags.
389 |
390 | ```js
391 | // Use ~s to treat a.b.c as simple path
392 | ~s a.b.c
393 | // Use ~r to treat a.b.c as rich path
394 | ~r a.b.c
395 | // Use ~j for using json paths
396 | ~j items[?(@.a>1)]
397 | ```
398 |
399 | **Note:** Rich paths are slower compare to the simple paths.
400 | Refer this [example](test/scenarios/paths/options.jt) for more details.
401 |
402 | ### Compile time expressions
403 |
404 | Compile time expressions are evaluated during compilation phase using compileTimeBindings option.
405 |
406 | ```js
407 | // {{$.a.b.c}} gets translated to 1 and
408 | // final translated code will be "let a = 1;"
409 | JsonTemplateEngine.create(`let a = {{$.a.b.c}};`, {
410 | compileTimeBindings: {
411 | a: {
412 | b: {
413 | c: 1,
414 | },
415 | },
416 | },
417 | });
418 | ```
419 |
420 | We can use compile time expressions to generate a template and then recompile it as expression. Refer these examples [simple compilation](test/scenarios/compile_time_expressions/template.jt) and [complex compilation](test/scenarios/compile_time_expressions/two_level_path_processing.jt) for more details.
421 |
422 | ### Mappings
423 | If you are familiar with [JSON Paths](https://goessner.net/articles/JsonPath/index.html#), you can easily begin working with JSON templates by leveraging your existing knowledge through the mappings feature.
424 |
425 | **Example:**
426 | * Let's say we want to transform the following data.
427 | * Input:
428 | ```json
429 | {
430 | "a": {
431 | "foo": 1,
432 | "bar": 2
433 | },
434 | "b": [
435 | {
436 | "firstName": "foo",
437 | "lastName": "bar"
438 | },
439 | {
440 | "firstName": "fizz",
441 | "lastName": "buzz"
442 | }
443 | ]
444 | }
445 | ```
446 | * Output:
447 | ```json
448 | {
449 | "foo": 1,
450 | "bar": 2,
451 | "items":[
452 | {
453 | "name": "foo bar"
454 | },
455 | {
456 | "name": "fizz buzz"
457 | }
458 | ]
459 | }
460 | ```
461 | * Mappings:
462 | ```json
463 | [
464 | {
465 | "description": "Copies properties of a to root level in the output",
466 | "input": "$.a",
467 | "output": "$"
468 | },
469 | {
470 | "description": "Combines first and last name in the output",
471 | "input": "$.b[*].(@.firstName + ' ' + @.lastName)",
472 | "output": "$.items[*].name"
473 | }
474 | ]
475 | ```
476 | * Try this example in our [playground](https://transformers-workflow-engine.rudderstack.com/#/mappings?gist=e25a6ac769ee5719e928720f5c439169).
477 |
478 | For more examples, refer [Mappings](test/scenarios/mappings/)
479 |
480 | ### Comments
481 |
482 | Supports both c style single line (`//`) and block comments (`/* .. */`).
483 | Refer this [example](test/scenarios/comments/template.jt) for more details.
484 |
485 | For more examples, refer [Scenarios](test/scenarios)
486 |
487 | ## Testing
488 |
489 | `npm test`
490 |
491 | ## Contribute
492 |
493 | We would love to see you contribute to RudderStack. Get more information on how to contribute [**here**](CONTRIBUTING.md).
494 |
495 | ## License
496 |
497 | The RudderStack `rudder-json-template-engine` is released under the [**MIT License**](https://opensource.org/licenses/MIT).
498 |
--------------------------------------------------------------------------------
/release-please-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
3 | "release-type": "node",
4 | "include-component-in-tag": false,
5 | "packages": {
6 | ".": {
7 | "extra-files": ["readme.md"]
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/sonar-project.properties:
--------------------------------------------------------------------------------
1 | sonar.log.level=INFO
2 | sonar.verbose=false
3 | sonar.qualitygate.wait=false
4 |
5 | # Project details
6 | sonar.projectKey=rudderlabs_rudder-json-template-engine
7 | sonar.organization=rudderlabs
8 | sonar.projectName=rudder-json-template-engine
9 | sonar.projectVersion=0.8.2
10 |
11 | # Meta-data for the project
12 | sonar.links.scm=https://github.com/rudderlabs/rudder-json-template-engine
13 | sonar.links.issue=https://github.com/rudderlabs/rudder-json-template-engine/issues
14 |
15 | # Path to reports
16 | sonar.javascript.lcov.reportPaths=reports/coverage/lcov.info
17 | sonar.eslint.reportPaths=reports/eslint.json
18 |
19 | # Path to sources
20 | sonar.sources=src
21 | sonar.inclusions=**/*.ts
22 | sonar.exclusions=**/*.json,**/*.html,**/*.png,**/*.jpg,**/*.gif,**/*.svg,**/*.yml
23 | # Path to tests
24 | sonar.tests=test
25 | sonar.test.inclusions=**/*.test.ts
26 | sonar.test.exclusions=**/*.json,**/*.html,**/*.png,**/*.jpg,**/*.gif,**/*.svg
27 | sonar.coverage.exclusions=test/**/*,**/*.json,**/*.html,**/*.png,**/*.jpg,**/*.gif,**/*.svg
28 |
29 | # Source encoding
30 | sonar.sourceEncoding=UTF-8
31 |
32 | # Exclusions for copy-paste detection
33 | sonar.cpd.exclusions=test/**/*
--------------------------------------------------------------------------------
/src/constants.ts:
--------------------------------------------------------------------------------
1 | import { SyntaxType } from './types';
2 |
3 | export const VARS_PREFIX = '___';
4 | export const DATA_PARAM_KEY = '___d';
5 | export const BINDINGS_PARAM_KEY = '___b';
6 | export const BINDINGS_CONTEXT_KEY = '___b.context.';
7 | export const RESULT_KEY = '___r';
8 | export const FUNCTION_RESULT_KEY = '___f';
9 | export const INDENTATION_SPACES = 4;
10 | export const EMPTY_EXPR = { type: SyntaxType.EMPTY };
11 |
--------------------------------------------------------------------------------
/src/engine.test.ts:
--------------------------------------------------------------------------------
1 | import { JsonTemplateEngine } from './engine';
2 | import { PathType } from './types';
3 |
4 | describe('engine', () => {
5 | describe('isValidJSONPath', () => {
6 | it('should return true for valid JSON root path', () => {
7 | expect(JsonTemplateEngine.isValidJSONPath('$.user.name')).toBeTruthy();
8 | });
9 |
10 | it('should return true for valid JSON relative path', () => {
11 | expect(JsonTemplateEngine.isValidJSONPath('.user.name')).toBeTruthy();
12 |
13 | expect(JsonTemplateEngine.isValidJSONPath('@.user.name')).toBeTruthy();
14 | });
15 |
16 | it('should return false for invalid JSON path', () => {
17 | expect(JsonTemplateEngine.isValidJSONPath('userId')).toBeFalsy();
18 | });
19 |
20 | it('should return false for invalid template', () => {
21 | expect(JsonTemplateEngine.isValidJSONPath('a=')).toBeFalsy();
22 | });
23 |
24 | it('should return false for empty path', () => {
25 | expect(JsonTemplateEngine.isValidJSONPath('')).toBeFalsy();
26 | });
27 | });
28 | describe('validateMappings', () => {
29 | it('should validate mappings', () => {
30 | expect(() =>
31 | JsonTemplateEngine.validateMappings([
32 | {
33 | input: '$.userId',
34 | output: '$.user.id',
35 | },
36 | {
37 | input: '$.discount',
38 | output: '$.events[0].items[*].discount',
39 | },
40 | ]),
41 | ).not.toThrow();
42 | });
43 |
44 | it('should throw error for mappings which are not compatible with each other', () => {
45 | expect(() =>
46 | JsonTemplateEngine.validateMappings([
47 | {
48 | input: '$.a[0]',
49 | output: '$.b[0].name',
50 | },
51 | {
52 | input: '$.discount',
53 | output: '$.b[0].name[*].discount',
54 | },
55 | ]),
56 | ).toThrowError('Invalid mapping');
57 | });
58 |
59 | it('should throw error for mappings with invalid json paths', () => {
60 | expect(() =>
61 | JsonTemplateEngine.validateMappings([
62 | {
63 | input: 'events[0]',
64 | output: 'events[0].name',
65 | },
66 | ]),
67 | ).toThrowError('Invalid mapping');
68 | });
69 | });
70 | describe('isValidMapping', () => {
71 | it('should convert to JSON paths when options are used', () => {
72 | expect(
73 | JsonTemplateEngine.isValidMapping(
74 | {
75 | input: '$.a[0]',
76 | output: 'b[0].name',
77 | },
78 | { defaultPathType: PathType.JSON },
79 | ),
80 | ).toBeTruthy();
81 | });
82 | it('should throw error when output is not valid json path without engine options', () => {
83 | expect(
84 | JsonTemplateEngine.isValidMapping({
85 | input: '$.events[0]',
86 | output: 'events[0].name',
87 | }),
88 | ).toBeFalsy();
89 | });
90 | });
91 | });
92 |
--------------------------------------------------------------------------------
/src/engine.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable import/no-cycle */
2 | import { BINDINGS_PARAM_KEY, DATA_PARAM_KEY, EMPTY_EXPR } from './constants';
3 | import { JsonTemplateMappingError } from './errors/mapping';
4 | import { JsonTemplateLexer } from './lexer';
5 | import { JsonTemplateParser } from './parser';
6 | import { JsonTemplateReverseTranslator } from './reverse_translator';
7 | import { JsonTemplateTranslator } from './translator';
8 | import {
9 | EngineOptions,
10 | Expression,
11 | FlatMappingAST,
12 | FlatMappingPaths,
13 | PathType,
14 | SyntaxType,
15 | TemplateInput,
16 | } from './types';
17 | import { CreateAsyncFunction, convertToObjectMapping, isExpression } from './utils';
18 |
19 | export class JsonTemplateEngine {
20 | private readonly fn: Function;
21 |
22 | private constructor(fn: Function) {
23 | this.fn = fn;
24 | }
25 |
26 | private static compileAsSync(template: TemplateInput, options?: EngineOptions): Function {
27 | // eslint-disable-next-line @typescript-eslint/no-implied-eval
28 | return Function(
29 | DATA_PARAM_KEY,
30 | BINDINGS_PARAM_KEY,
31 | JsonTemplateEngine.translate(template, options),
32 | );
33 | }
34 |
35 | private static compileAsAsync(templateOrExpr: TemplateInput, options?: EngineOptions): Function {
36 | return CreateAsyncFunction(
37 | DATA_PARAM_KEY,
38 | BINDINGS_PARAM_KEY,
39 | JsonTemplateEngine.translate(templateOrExpr, options),
40 | );
41 | }
42 |
43 | private static translateExpression(expr: Expression): string {
44 | const translator = new JsonTemplateTranslator(expr);
45 | return translator.translate();
46 | }
47 |
48 | static isValidJSONPath(path: string = ''): boolean {
49 | try {
50 | const expression = JsonTemplateEngine.parse(path, { defaultPathType: PathType.JSON });
51 | const statement = expression.statements?.[0];
52 | return (
53 | statement &&
54 | statement.type === SyntaxType.PATH &&
55 | (!statement.root || statement.root === DATA_PARAM_KEY)
56 | );
57 | } catch (e) {
58 | return false;
59 | }
60 | }
61 |
62 | private static toJsonPath(input: string): string {
63 | if (input.startsWith('$')) {
64 | return input;
65 | }
66 |
67 | // Check if it's wrapped in quotes
68 | // or is a simple identifier
69 | // or contains dots
70 | if (
71 | /^'.*'$/.test(input) ||
72 | /^".*"$/.test(input) ||
73 | /^\w+$/.test(input) ||
74 | input.includes('.')
75 | ) {
76 | return `$.${input}`;
77 | }
78 |
79 | // If input contains special characters
80 | return `$.'${input}'`;
81 | }
82 |
83 | private static convertToJSONPath(path?: string, options?: EngineOptions): string | undefined {
84 | if (!path) {
85 | return path;
86 | }
87 | if (options?.defaultPathType === PathType.JSON) {
88 | return JsonTemplateEngine.toJsonPath(path);
89 | }
90 | return path;
91 | }
92 |
93 | private static prepareMapping(mapping: FlatMappingPaths, options?: EngineOptions) {
94 | return {
95 | ...mapping,
96 | input: mapping.input ?? mapping.from,
97 | output: JsonTemplateEngine.convertToJSONPath(mapping.output ?? mapping.to, options),
98 | };
99 | }
100 |
101 | private static prepareMappings(
102 | mappings: FlatMappingPaths[],
103 | options?: EngineOptions,
104 | ): FlatMappingPaths[] {
105 | return mappings
106 | .map((mapping) => JsonTemplateEngine.prepareMapping(mapping, options))
107 | .filter((mapping) => mapping.input && mapping.output);
108 | }
109 |
110 | static validateMappings(mappings: FlatMappingPaths[], options?: EngineOptions) {
111 | JsonTemplateEngine.prepareMappings(mappings, options).forEach((mapping) => {
112 | if (!JsonTemplateEngine.isValidJSONPath(mapping.output)) {
113 | throw new JsonTemplateMappingError(
114 | 'Invalid mapping: invalid JSON path',
115 | mapping.input as string,
116 | mapping.output as string,
117 | );
118 | }
119 | });
120 | JsonTemplateEngine.parseMappingPaths(mappings, options);
121 | }
122 |
123 | static isValidMapping(mapping: FlatMappingPaths, options?: EngineOptions) {
124 | try {
125 | JsonTemplateEngine.validateMappings([mapping], options);
126 | return true;
127 | } catch (e) {
128 | return false;
129 | }
130 | }
131 |
132 | private static createFlatMappingsAST(
133 | mappings: FlatMappingPaths[],
134 | options?: EngineOptions,
135 | ): FlatMappingAST[] {
136 | const newOptions = { ...options, mappings: true };
137 | return JsonTemplateEngine.prepareMappings(mappings, options)
138 | .filter((mapping) => mapping.input && mapping.output)
139 | .map((mapping) => ({
140 | ...mapping,
141 | inputExpr: JsonTemplateEngine.parse(mapping.input, newOptions).statements[0],
142 | outputExpr: JsonTemplateEngine.parse(mapping.output, newOptions).statements[0],
143 | }));
144 | }
145 |
146 | static parseMappingPaths(mappings: FlatMappingPaths[], options?: EngineOptions): Expression {
147 | return convertToObjectMapping(JsonTemplateEngine.createFlatMappingsAST(mappings, options));
148 | }
149 |
150 | static create(templateOrExpr: TemplateInput, options?: EngineOptions): JsonTemplateEngine {
151 | return new JsonTemplateEngine(JsonTemplateEngine.compileAsAsync(templateOrExpr, options));
152 | }
153 |
154 | static createAsSync(template: TemplateInput, options?: EngineOptions): JsonTemplateEngine {
155 | return new JsonTemplateEngine(JsonTemplateEngine.compileAsSync(template, options));
156 | }
157 |
158 | static parse(template: TemplateInput, options?: EngineOptions): Expression {
159 | if (!template) {
160 | return EMPTY_EXPR;
161 | }
162 | if (isExpression(template)) {
163 | return template as Expression;
164 | }
165 | if (typeof template === 'string') {
166 | const lexer = new JsonTemplateLexer(template);
167 | const parser = new JsonTemplateParser(lexer, options);
168 | return parser.parse();
169 | }
170 | return JsonTemplateEngine.parseMappingPaths(template as FlatMappingPaths[], options);
171 | }
172 |
173 | static translate(template: TemplateInput, options?: EngineOptions): string {
174 | return JsonTemplateEngine.translateExpression(JsonTemplateEngine.parse(template, options));
175 | }
176 |
177 | static reverseTranslate(expr: Expression | FlatMappingPaths[], options?: EngineOptions): string {
178 | const translator = new JsonTemplateReverseTranslator(options);
179 | let newExpr = expr;
180 | if (Array.isArray(expr)) {
181 | newExpr = JsonTemplateEngine.parseMappingPaths(expr, options);
182 | }
183 | return translator.translate(newExpr as Expression);
184 | }
185 |
186 | static convertMappingsToTemplate(mappings: FlatMappingPaths[], options?: EngineOptions): string {
187 | return JsonTemplateEngine.reverseTranslate(
188 | JsonTemplateEngine.parse(mappings, options),
189 | options,
190 | );
191 | }
192 |
193 | static evaluateAsSync(
194 | template: TemplateInput,
195 | options: EngineOptions = {},
196 | data: unknown = {},
197 | bindings: Record = {},
198 | ): unknown {
199 | return JsonTemplateEngine.createAsSync(template, options).evaluate(data, bindings);
200 | }
201 |
202 | static evaluate(
203 | template: TemplateInput,
204 | options: EngineOptions = {},
205 | data: unknown = {},
206 | bindings: Record = {},
207 | ): unknown {
208 | return JsonTemplateEngine.create(template, options).evaluate(data, bindings);
209 | }
210 |
211 | evaluate(data: unknown = {}, bindings: Record = {}): unknown {
212 | return this.fn(data, bindings);
213 | }
214 | }
215 |
--------------------------------------------------------------------------------
/src/errors/index.ts:
--------------------------------------------------------------------------------
1 | export * from './lexer';
2 | export * from './parser';
3 | export * from './translator';
4 |
--------------------------------------------------------------------------------
/src/errors/lexer.ts:
--------------------------------------------------------------------------------
1 | export class JsonTemplateLexerError extends Error {}
2 |
--------------------------------------------------------------------------------
/src/errors/mapping.ts:
--------------------------------------------------------------------------------
1 | export class JsonTemplateMappingError extends Error {
2 | inputMapping: string;
3 |
4 | outputMapping: string;
5 |
6 | constructor(message: string, inputMapping: string, outputMapping: string) {
7 | super(`${message}. Input: ${inputMapping}, Output: ${outputMapping}`);
8 | this.inputMapping = inputMapping;
9 | this.outputMapping = outputMapping;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/errors/parser.ts:
--------------------------------------------------------------------------------
1 | export class JsonTemplateParserError extends Error {}
2 |
--------------------------------------------------------------------------------
/src/errors/translator.ts:
--------------------------------------------------------------------------------
1 | export class JsonTemplateTranslatorError extends Error {}
2 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './constants';
2 | export * from './engine';
3 | export * from './errors';
4 | export * from './lexer';
5 | export * from './operators';
6 | export * from './parser';
7 | export * from './translator';
8 | export * from './types';
9 | export * from './utils';
10 |
--------------------------------------------------------------------------------
/src/operator.test.ts:
--------------------------------------------------------------------------------
1 | import { isStandardFunction, standardFunctions } from './operators';
2 |
3 | describe('Operators tests', () => {
4 | describe('isStandardFunction', () => {
5 | it('should return true for standard functions', () => {
6 | expect(Object.keys(standardFunctions).every(isStandardFunction)).toBeTruthy();
7 | });
8 | it('should return false for non standard function', () => {
9 | const nonStandardFunctions = [
10 | 'toString',
11 | 'valueOf',
12 | 'toLocaleString',
13 | 'hasOwnProperty',
14 | 'isPrototypeOf',
15 | 'propertyIsEnumerable',
16 | 'constructor',
17 | ];
18 | expect(Object.keys(nonStandardFunctions).every(isStandardFunction)).toBeFalsy();
19 | });
20 | });
21 | });
22 |
--------------------------------------------------------------------------------
/src/operators.ts:
--------------------------------------------------------------------------------
1 | import { VARS_PREFIX } from './constants';
2 |
3 | function startsWithStrict(val1, val2): string {
4 | return `(typeof ${val1} === 'string' && ${val1}.startsWith(${val2}))`;
5 | }
6 |
7 | function startsWith(val1, val2): string {
8 | const code: string[] = [];
9 | code.push(`(typeof ${val1} === 'string' && `);
10 | code.push(`typeof ${val2} === 'string' && `);
11 | code.push(`${val1}.toLowerCase().startsWith(${val2}.toLowerCase()))`);
12 | return code.join('');
13 | }
14 |
15 | function endsWithStrict(val1, val2): string {
16 | return `(typeof ${val1} === 'string' && ${val1}.endsWith(${val2}))`;
17 | }
18 |
19 | function endsWith(val1, val2): string {
20 | const code: string[] = [];
21 | code.push(`(typeof ${val1} === 'string' && `);
22 | code.push(`typeof ${val2} === 'string' && `);
23 | code.push(`${val1}.toLowerCase().endsWith(${val2}.toLowerCase()))`);
24 | return code.join('');
25 | }
26 |
27 | function containsStrict(val1, val2): string {
28 | return `((typeof ${val1} === 'string' || Array.isArray(${val1})) && ${val1}.includes(${val2}))`;
29 | }
30 |
31 | function contains(val1, val2): string {
32 | const code: string[] = [];
33 | code.push(`(typeof ${val1} === 'string' && typeof ${val2} === 'string') ?`);
34 | code.push(`(${val1}.toLowerCase().includes(${val2}.toLowerCase()))`);
35 | code.push(':');
36 | code.push(`(Array.isArray(${val1}) && (${val1}.includes(${val2})`);
37 | code.push(`|| (typeof ${val2} === 'string' && ${val1}.includes(${val2}.toLowerCase()))))`);
38 | return code.join('');
39 | }
40 |
41 | export const binaryOperators = {
42 | '===': (val1, val2): string => `${val1}===${val2}`,
43 |
44 | '==': (val1, val2): string => {
45 | const code: string[] = [];
46 | code.push(`((typeof ${val1} == 'string' && `);
47 | code.push(`typeof ${val2} == 'string' && `);
48 | code.push(`${val1}.toLowerCase() == ${val2}.toLowerCase()) || `);
49 | code.push(`${val1} == ${val2})`);
50 | return code.join('');
51 | },
52 |
53 | '>=': (val1, val2): string => `${val1}>=${val2}`,
54 |
55 | '>': (val1, val2): string => `${val1}>${val2}`,
56 |
57 | '<=': (val1, val2): string => `${val1}<=${val2}`,
58 |
59 | '<': (val1, val2): string => `${val1}<${val2}`,
60 |
61 | '!==': (val1, val2): string => `${val1}!==${val2}`,
62 |
63 | '!=': (val1, val2): string => {
64 | const code: string[] = [];
65 | code.push(`(typeof ${val1} == 'string' && typeof ${val2} == 'string') ?`);
66 | code.push(`(${val1}.toLowerCase() != ${val2}.toLowerCase())`);
67 | code.push(':');
68 | code.push(`(${val1} != ${val2})`);
69 | return code.join('');
70 | },
71 |
72 | '^==': startsWithStrict,
73 |
74 | '==^': (val1, val2): string => startsWithStrict(val2, val1),
75 |
76 | '^=': startsWith,
77 |
78 | '=^': (val1, val2): string => startsWith(val2, val1),
79 |
80 | '$==': endsWithStrict,
81 |
82 | '==$': (val1, val2): string => endsWithStrict(val2, val1),
83 |
84 | '$=': endsWith,
85 |
86 | '=$': (val1, val2): string => endsWith(val2, val1),
87 |
88 | '=~': (val1, val2): string =>
89 | `(${val2} instanceof RegExp) ? (${val2}.test(${val1})) : (${val1}==${val2})`,
90 |
91 | contains,
92 |
93 | '==*': (val1, val2): string => containsStrict(val1, val2),
94 |
95 | '=*': (val1, val2): string => contains(val1, val2),
96 |
97 | size: (val1, val2): string => `${val1}.length === ${val2}`,
98 |
99 | empty: (val1, val2): string => `(${val1}.length === 0) === ${val2}`,
100 |
101 | subsetof: (val1, val2): string => `${val1}.every((el) => {return ${val2}.includes(el);})`,
102 |
103 | anyof: (val1, val2): string => `${val1}.some((el) => {return ${val2}.includes(el);})`,
104 |
105 | '+': (val1, val2): string => `${val1}+${val2}`,
106 |
107 | '-': (val1, val2): string => `${val1}-${val2}`,
108 |
109 | '*': (val1, val2): string => `${val1}*${val2}`,
110 |
111 | '/': (val1, val2): string => `${val1}/${val2}`,
112 |
113 | '%': (val1, val2): string => `${val1}%${val2}`,
114 |
115 | '>>': (val1, val2): string => `${val1}>>${val2}`,
116 |
117 | '<<': (val1, val2): string => `${val1}<<${val2}`,
118 |
119 | '**': (val1, val2): string => `${val1}**${val2}`,
120 | };
121 |
122 | function getSumFn(prefix: string = ''): string {
123 | return `function ${prefix}sum(arr) {
124 | if(!Array.isArray(arr)) {
125 | throw new Error('Expected an array');
126 | }
127 | return arr.reduce((a, b) => a + b, 0);
128 | }`;
129 | }
130 |
131 | function getAvgFn(prefix: string = ''): string {
132 | return `function ${prefix}avg(arr) {
133 | if(!Array.isArray(arr)) {
134 | throw new Error('Expected an array');
135 | }
136 | ${getSumFn()}
137 | return sum(arr) / arr.length;
138 | }`;
139 | }
140 |
141 | export const standardFunctions = {
142 | sum: getSumFn(VARS_PREFIX),
143 | max: `function ${VARS_PREFIX}max(arr) {
144 | if(!Array.isArray(arr)) {
145 | throw new Error('Expected an array');
146 | }
147 | return Math.max(...arr);
148 | }`,
149 | min: `function ${VARS_PREFIX}min(arr) {
150 | if(!Array.isArray(arr)) {
151 | throw new Error('Expected an array');
152 | }
153 | return Math.min(...arr);
154 | }`,
155 | avg: getAvgFn(VARS_PREFIX),
156 | length: `function ${VARS_PREFIX}length(arr) {
157 | if(!Array.isArray(arr) && typeof arr !== 'string') {
158 | throw new Error('Expected an array or string');
159 | }
160 | return arr.length;
161 | }`,
162 | stddev: `function ${VARS_PREFIX}stddev(arr) {
163 | if(!Array.isArray(arr)) {
164 | throw new Error('Expected an array');
165 | }
166 | ${getAvgFn()}
167 | const mu = avg(arr);
168 | const diffSq = arr.map((el) => (el - mu) ** 2);
169 | return Math.sqrt(avg(diffSq));
170 | }`,
171 | first: `function ${VARS_PREFIX}first(arr) {
172 | if(!Array.isArray(arr)) {
173 | throw new Error('Expected an array');
174 | }
175 | return arr[0];
176 | }`,
177 | last: `function ${VARS_PREFIX}last(arr) {
178 | if(!Array.isArray(arr)) {
179 | throw new Error('Expected an array');
180 | }
181 | return arr[arr.length - 1];
182 | }`,
183 | index: `function ${VARS_PREFIX}index(arr, i) {
184 | if(!Array.isArray(arr)) {
185 | throw new Error('Expected an array');
186 | }
187 | if (i < 0) {
188 | return arr[arr.length + i];
189 | }
190 | return arr[i];
191 | }`,
192 | keys: `function ${VARS_PREFIX}keys(obj) { return Object.keys(obj); }`,
193 | };
194 |
195 | export function isStandardFunction(name: string): boolean {
196 | return Object.prototype.hasOwnProperty.call(standardFunctions, name);
197 | }
198 |
--------------------------------------------------------------------------------
/src/reverse_translator.test.ts:
--------------------------------------------------------------------------------
1 | import { JsonTemplateEngine } from './engine';
2 | import { PathType } from './types';
3 |
4 | describe('reverse_translator', () => {
5 | it('should reverse translate with indentation', () => {
6 | const template = JsonTemplateEngine.reverseTranslate(
7 | JsonTemplateEngine.parse(`{a: {b: {c: 1}}}`),
8 | );
9 | expect(template).toEqual(
10 | '{\n "a": {\n "b": {\n "c": 1\n }\n }\n}',
11 | );
12 | });
13 |
14 | it('should reverse translate json mappings', () => {
15 | const template = JsonTemplateEngine.reverseTranslate(
16 | [
17 | {
18 | input: '$.userId',
19 | output: '$.user.id',
20 | },
21 | {
22 | input: '$.discount',
23 | output: '$.events[0].items[*].discount',
24 | },
25 | {
26 | input: '$.products[?(@.category)].id',
27 | output: '$.events[0].items[*].product_id',
28 | },
29 | {
30 | input: '$.events[0]',
31 | output: '$.events[0].name',
32 | },
33 | {
34 | input: '$.products[?(@.category)].variations[*].size',
35 | output: '$.events[0].items[*].options[*].s',
36 | },
37 | {
38 | input: '$.products[?(@.category)].(@.price * @.quantity * (1 - $.discount / 100))',
39 | output: '$.events[0].items[*].value',
40 | },
41 | {
42 | input: '$.products[?(@.category)].(@.price * @.quantity * (1 - $.discount / 100)).sum()',
43 | output: '$.events[0].revenue',
44 | },
45 | ],
46 | { defaultPathType: PathType.JSON },
47 | );
48 | expect(template).toEqual(
49 | '{\n "user": {\n "id": $.userId\n },\n "events": [{\n "items": $.products[?(@.category)].({\n "discount": $.discount,\n "product_id": @.id,\n "options": @.variations[*].({\n "s": @.size\n })[],\n "value": @.price * @.quantity * (1 - $.discount / 100)\n })[],\n "name": $.events[0],\n "revenue": $.products[?(@.category)].(@.price * @.quantity * (1 - $.discount / 100)).sum()\n }]\n}',
50 | );
51 | });
52 | });
53 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | export enum Keyword {
2 | FUNCTION = 'function',
3 | NEW = 'new',
4 | TYPEOF = 'typeof',
5 | LET = 'let',
6 | CONST = 'const',
7 | LAMBDA = 'lambda',
8 | AWAIT = 'await',
9 | ASYNC = 'async',
10 | IN = 'in',
11 | NOT_IN = 'nin',
12 | NOT = 'not',
13 | CONTAINS = 'contains',
14 | SUBSETOF = 'subsetof',
15 | ANYOF = 'anyof',
16 | NONEOF = 'noneof',
17 | EMPTY = 'empty',
18 | SIZE = 'size',
19 | RETURN = 'return',
20 | THROW = 'throw',
21 | CONTINUE = 'continue',
22 | BREAK = 'break',
23 | FOR = 'for',
24 | }
25 |
26 | export enum TokenType {
27 | UNKNOWN = 'unknown',
28 | ID = 'id',
29 | INT = 'int',
30 | FLOAT = 'float',
31 | TEMPLATE = 'template',
32 | STR = 'str',
33 | BOOL = 'bool',
34 | NULL = 'null',
35 | UNDEFINED = 'undefined',
36 | LAMBDA_ARG = 'lambda_arg',
37 | PUNCT = 'punct',
38 | THROW = 'throw',
39 | KEYWORD = 'keyword',
40 | EOT = 'eot',
41 | REGEXP = 'regexp',
42 | }
43 |
44 | // In the order of precedence
45 | export enum OperatorType {
46 | BASE = 'base',
47 | CONDITIONAL = 'conditional',
48 | ASSIGNMENT = 'assignment',
49 | COALESCING = 'coalescing',
50 | OR = 'or',
51 | AND = 'and',
52 | EQUALITY = 'equality',
53 | RELATIONAL = 'relational',
54 | SHIFT = 'shift',
55 | ADDITION = 'addition',
56 | MULTIPLICATION = 'multiplication',
57 | POWER = 'power',
58 | UNARY = 'unary',
59 | PREFIX_INCREMENT = 'prefix_increment',
60 | POSTFIX_INCREMENT = 'postfix_increment',
61 | }
62 |
63 | export enum SyntaxType {
64 | EMPTY = 'empty',
65 | PATH = 'path',
66 | PATH_OPTIONS = 'path_options',
67 | SELECTOR = 'selector',
68 | LAMBDA_ARG = 'lambda_arg',
69 | INCREMENT = 'increment',
70 | LITERAL = 'literal',
71 | LOGICAL_COALESCE_EXPR = 'logical_coalesce_expr',
72 | LOGICAL_OR_EXPR = 'logical_or_expr',
73 | LOGICAL_AND_EXPR = 'logical_and_expr',
74 | COMPARISON_EXPR = 'comparison_expr',
75 | IN_EXPR = 'in_expr',
76 | MATH_EXPR = 'math_expr',
77 | UNARY_EXPR = 'unary_expr',
78 | SPREAD_EXPR = 'spread_expr',
79 | CONDITIONAL_EXPR = 'conditional_expr',
80 | ARRAY_INDEX_FILTER_EXPR = 'array_index_filter_expr',
81 | ALL_FILTER_EXPR = 'all_filter_expr',
82 | OBJECT_INDEX_FILTER_EXPR = 'object_index_filter_expr',
83 | RANGE_FILTER_EXPR = 'range_filter_expr',
84 | OBJECT_FILTER_EXPR = 'object_filter_expr',
85 | ARRAY_FILTER_EXPR = 'array_filter_expr',
86 | DEFINITION_EXPR = 'definition_expr',
87 | ASSIGNMENT_EXPR = 'assignment_expr',
88 | OBJECT_PROP_EXPR = 'object_prop_expr',
89 | OBJECT_EXPR = 'object_expr',
90 | ARRAY_EXPR = 'array_expr',
91 | BLOCK_EXPR = 'block_expr',
92 | FUNCTION_EXPR = 'function_expr',
93 | FUNCTION_CALL_EXPR = 'function_call_expr',
94 | RETURN_EXPR = 'return_expr',
95 | THROW_EXPR = 'throw_expr',
96 | STATEMENTS_EXPR = 'statements_expr',
97 | LOOP_CONTROL_EXPR = 'loop_control_expr',
98 | LOOP_EXPR = 'loop_expr',
99 | TEMPLATE_EXPR = 'TEMPLATE_EXPR',
100 | }
101 |
102 | export enum PathType {
103 | SIMPLE = 'simple',
104 | RICH = 'rich',
105 | JSON = 'json',
106 | UNKNOWN = 'unknown',
107 | }
108 |
109 | export interface EngineOptions {
110 | compileTimeBindings?: Record;
111 | defaultPathType?: PathType;
112 | mappings?: boolean;
113 | }
114 |
115 | export type Token = {
116 | type: TokenType;
117 | value: any;
118 | range: [number, number];
119 | };
120 |
121 | export interface PathOptions {
122 | item?: string;
123 | index?: string;
124 | toArray?: boolean;
125 | }
126 |
127 | export interface Expression {
128 | type: SyntaxType;
129 | options?: PathOptions;
130 | [key: string]: any;
131 | }
132 |
133 | export interface PathOptionsExpression extends Expression {
134 | options: PathOptions;
135 | }
136 |
137 | export interface LambdaArgExpression extends Expression {
138 | index: number;
139 | }
140 |
141 | export interface FunctionExpression extends Expression {
142 | params?: string[];
143 | body: StatementsExpression;
144 | block?: boolean;
145 | async?: boolean;
146 | lambda?: boolean;
147 | }
148 |
149 | export interface BlockExpression extends Expression {
150 | statements: Expression[];
151 | }
152 |
153 | export interface ObjectPropExpression extends Expression {
154 | key?: Expression | string;
155 | value: Expression;
156 | contextVar?: string;
157 | }
158 |
159 | export interface ObjectExpression extends Expression {
160 | props: ObjectPropExpression[];
161 | }
162 |
163 | export interface ArrayExpression extends Expression {
164 | elements: Expression[];
165 | }
166 |
167 | export interface StatementsExpression extends Expression {
168 | statements: Expression[];
169 | }
170 |
171 | export interface UnaryExpression extends Expression {
172 | arg: Expression;
173 | op: string;
174 | }
175 |
176 | export interface BinaryExpression extends Expression {
177 | args: [Expression, Expression];
178 | op: string;
179 | }
180 |
181 | export interface TemplateExpression extends Expression {
182 | parts: Expression[];
183 | }
184 |
185 | export interface AssignmentExpression extends Expression {
186 | path: PathExpression;
187 | value: Expression;
188 | op: string;
189 | }
190 |
191 | export interface DefinitionExpression extends Expression {
192 | vars: string[];
193 | fromObject?: boolean;
194 | value: Expression;
195 | definition: string;
196 | }
197 |
198 | export interface RangeFilterExpression extends Expression {
199 | fromIdx?: Expression;
200 | toIdx?: Expression;
201 | }
202 |
203 | export interface IndexFilterExpression extends Expression {
204 | indexes: ArrayExpression;
205 | exclude?: boolean;
206 | }
207 |
208 | export interface AllFilterExpression extends Expression {}
209 |
210 | export interface ObjectFilterExpression extends Expression {
211 | filter: Expression;
212 | }
213 |
214 | export interface ArrayFilterExpression extends Expression {
215 | filter: RangeFilterExpression | IndexFilterExpression;
216 | }
217 |
218 | export type Literal = string | number | boolean | null | undefined;
219 | export interface LiteralExpression extends Expression {
220 | value: Literal;
221 | tokenType: TokenType;
222 | }
223 |
224 | export interface PathExpression extends Expression {
225 | parts: Expression[];
226 | root?: Expression | string;
227 | returnAsArray?: boolean;
228 | pathType: PathType;
229 | inferredPathType: PathType;
230 | }
231 |
232 | export interface IncrementExpression extends Expression {
233 | id: string;
234 | op: string;
235 | postfix?: boolean;
236 | }
237 |
238 | export interface SelectorExpression extends Expression {
239 | selector: string;
240 | prop?: Omit;
241 | }
242 | export interface SpreadExpression extends Expression {
243 | value: Expression;
244 | }
245 |
246 | export interface FunctionCallExpression extends Expression {
247 | args: Expression[];
248 | object?: Expression;
249 | id?: string;
250 | parent?: string;
251 | }
252 |
253 | export interface ConditionalExpression extends Expression {
254 | if: Expression;
255 | then: Expression;
256 | else?: Expression;
257 | }
258 |
259 | export type BlockExpressionOptions = {
260 | blockEnd?: string;
261 | parentType?: SyntaxType;
262 | };
263 |
264 | export interface ReturnExpression extends Expression {
265 | value?: Expression;
266 | }
267 |
268 | export interface LoopControlExpression extends Expression {
269 | control: string;
270 | }
271 | export interface LoopExpression extends Expression {
272 | init?: Expression;
273 | test?: Expression;
274 | update?: Expression;
275 | body: StatementsExpression;
276 | }
277 |
278 | export interface ThrowExpression extends Expression {
279 | value: Expression;
280 | }
281 |
282 | export type FlatMappingPaths = {
283 | description?: string;
284 | from?: string;
285 | to?: string;
286 | input?: string;
287 | output?: string;
288 | [key: string]: any;
289 | };
290 |
291 | export type FlatMappingAST = FlatMappingPaths & {
292 | inputExpr: Expression;
293 | outputExpr: PathExpression;
294 | };
295 |
296 | export type TemplateInput = string | Expression | FlatMappingPaths[] | undefined;
297 |
--------------------------------------------------------------------------------
/src/utils/common.test.ts:
--------------------------------------------------------------------------------
1 | import { EMPTY_EXPR } from '../constants';
2 | import { SyntaxType } from '../types';
3 | import * as CommonUtils from './common';
4 |
5 | describe('Common Utils tests', () => {
6 | describe('toArray', () => {
7 | it('should return array for non array', () => {
8 | expect(CommonUtils.toArray(1)).toEqual([1]);
9 | });
10 | it('should return array for array', () => {
11 | expect(CommonUtils.toArray([1])).toEqual([1]);
12 | });
13 | it('should return array for undefined', () => {
14 | expect(CommonUtils.toArray(undefined)).toBeUndefined();
15 | });
16 | });
17 | describe('getLastElement', () => {
18 | it('should return last element of non empty array', () => {
19 | expect(CommonUtils.getLastElement([1, 2])).toEqual(2);
20 | });
21 | it('should return undefined for empty array', () => {
22 | expect(CommonUtils.getLastElement([])).toBeUndefined();
23 | });
24 | });
25 | describe('convertToStatementsExpr', () => {
26 | it('should return statement expression for no expressions', () => {
27 | expect(CommonUtils.convertToStatementsExpr()).toEqual({
28 | type: SyntaxType.STATEMENTS_EXPR,
29 | statements: [],
30 | });
31 | });
32 | it('should return statement expression for single expression', () => {
33 | expect(CommonUtils.convertToStatementsExpr(EMPTY_EXPR)).toEqual({
34 | type: SyntaxType.STATEMENTS_EXPR,
35 | statements: [EMPTY_EXPR],
36 | });
37 | });
38 | it('should return statement expression for multiple expression', () => {
39 | expect(CommonUtils.convertToStatementsExpr(EMPTY_EXPR, EMPTY_EXPR)).toEqual({
40 | type: SyntaxType.STATEMENTS_EXPR,
41 | statements: [EMPTY_EXPR, EMPTY_EXPR],
42 | });
43 | });
44 | });
45 |
46 | describe('escapeStr', () => {
47 | it('should return emtpy string for non string input', () => {
48 | expect(CommonUtils.escapeStr(undefined)).toEqual('');
49 | });
50 | it('should return escaped string for simple string input', () => {
51 | expect(CommonUtils.escapeStr('aabc')).toEqual(`"aabc"`);
52 | });
53 |
54 | it('should return escaped string for string with escape characters', () => {
55 | expect(CommonUtils.escapeStr(`a\nb'"c`)).toEqual(`"a\nb'\\"c"`);
56 | });
57 | });
58 | describe('CreateAsyncFunction', () => {
59 | it('should return async function from code without args', async () => {
60 | expect(await CommonUtils.CreateAsyncFunction('return 1')()).toEqual(1);
61 | });
62 | it('should return async function from code with args', async () => {
63 | expect(await CommonUtils.CreateAsyncFunction('input', 'return input')(1)).toEqual(1);
64 | });
65 | });
66 | });
67 |
--------------------------------------------------------------------------------
/src/utils/common.ts:
--------------------------------------------------------------------------------
1 | import {
2 | type Expression,
3 | type StatementsExpression,
4 | SyntaxType,
5 | BlockExpression,
6 | FlatMappingPaths,
7 | } from '../types';
8 |
9 | export function toArray(val: T | T[] | undefined): T[] | undefined {
10 | if (val === undefined || val === null) {
11 | return undefined;
12 | }
13 | return Array.isArray(val) ? val : [val];
14 | }
15 |
16 | export function getLastElement(arr: T[]): T | undefined {
17 | if (!arr.length) {
18 | return undefined;
19 | }
20 | return arr[arr.length - 1];
21 | }
22 |
23 | export function createBlockExpression(expr: Expression): BlockExpression {
24 | return {
25 | type: SyntaxType.BLOCK_EXPR,
26 | statements: [expr],
27 | };
28 | }
29 |
30 | export function convertToStatementsExpr(...expressions: Expression[]): StatementsExpression {
31 | return {
32 | type: SyntaxType.STATEMENTS_EXPR,
33 | statements: expressions,
34 | };
35 | }
36 |
37 | export function CreateAsyncFunction(...args) {
38 | // eslint-disable-next-line @typescript-eslint/no-empty-function, func-names
39 | return async function () {}.constructor(...args);
40 | }
41 |
42 | export function isExpression(val: string | Expression | FlatMappingPaths[]): boolean {
43 | return (
44 | typeof val === 'object' && !Array.isArray(val) && Object.values(SyntaxType).includes(val.type)
45 | );
46 | }
47 |
48 | export function escapeStr(s?: string): string {
49 | if (typeof s !== 'string') {
50 | return '';
51 | }
52 | return `"${s.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
53 | }
54 |
--------------------------------------------------------------------------------
/src/utils/converter.test.ts:
--------------------------------------------------------------------------------
1 | import { SyntaxType } from '../types';
2 | import { convertToObjectMapping } from './converter';
3 |
4 | describe('Converter:', () => {
5 | describe('convertToObjectMapping', () => {
6 | it('should validate mappings', () => {
7 | expect(() =>
8 | convertToObjectMapping([
9 | { inputExpr: { type: SyntaxType.EMPTY }, outputExpr: { type: SyntaxType.EMPTY } },
10 | ] as any),
11 | ).toThrowError(/Invalid mapping/);
12 | });
13 | });
14 | });
15 |
--------------------------------------------------------------------------------
/src/utils/index.ts:
--------------------------------------------------------------------------------
1 | export * from './common';
2 | export * from './converter';
3 |
--------------------------------------------------------------------------------
/src/utils/translator.ts:
--------------------------------------------------------------------------------
1 | import { TokenType, Literal } from '../types';
2 | import { escapeStr } from './common';
3 |
4 | export function translateLiteral(type: TokenType, val: Literal): string {
5 | if (type === TokenType.STR) {
6 | return escapeStr(String(val));
7 | }
8 |
9 | return String(val);
10 | }
11 |
--------------------------------------------------------------------------------
/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/stryker.conf.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/@stryker-mutator/core/schema/stryker-schema.json",
3 | "_comment": "This config was generated using 'stryker init'. Please take a look at: https://stryker-mutator.io/docs/stryker-js/configuration/ for more information.",
4 | "packageManager": "npm",
5 | "reporters": ["html", "clear-text", "progress"],
6 | "testRunner": "command",
7 | "testRunner_comment": "Take a look at (missing 'homepage' URL in package.json) for information about the command plugin.",
8 | "coverageAnalysis": "all",
9 | "buildCommand": "npm run build",
10 | "mutate": [
11 | "{src,lib}/**/!(*.+(s|S)pec|*.+(t|T)est).+(cjs|mjs|js|ts|jsx|tsx|html|vue)",
12 | "!{src,lib}/**/__tests__/**/*.+(cjs|mjs|js|ts|jsx|tsx|html|vue)"
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/test/e2e.test.ts:
--------------------------------------------------------------------------------
1 | import { glob } from 'glob';
2 | import path, { join } from 'path';
3 | import { Command } from 'commander';
4 | import * as ScenarioUtils from './utils';
5 | import { Scenario } from './types';
6 |
7 | const rootDirName = 'scenarios';
8 | const command = new Command();
9 | command.allowUnknownOption().option('--scenarios ', 'Enter Scenario Names', 'all').parse();
10 |
11 | const opts = command.opts();
12 | let scenarios = opts.scenarios.split(/[, ]/);
13 |
14 | if (scenarios[0] === 'all') {
15 | scenarios = glob.sync(join(__dirname, rootDirName, '**/data.ts'));
16 | }
17 |
18 | describe('Scenarios tests', () => {
19 | scenarios.forEach((scenarioFileName) => {
20 | const scenarioDir = path.dirname(scenarioFileName);
21 | const scenarioName = path.basename(scenarioDir);
22 | describe(`${scenarioName}`, () => {
23 | const scenarios = ScenarioUtils.extractScenarios(scenarioDir);
24 | scenarios.forEach((scenario, index) => {
25 | it(`Scenario ${index}: ${Scenario.getTemplatePath(scenario)}`, async () => {
26 | try {
27 | const result = await ScenarioUtils.evaluateScenario(scenarioDir, scenario);
28 | expect(result).toEqual(scenario.output);
29 | } catch (error: any) {
30 | expect(error.message).toContain(scenario.error);
31 | }
32 | });
33 | });
34 | });
35 | });
36 | });
37 |
--------------------------------------------------------------------------------
/test/scenario.test.ts:
--------------------------------------------------------------------------------
1 | import { join } from 'path';
2 | import { Command } from 'commander';
3 | import { Scenario } from './types';
4 | import * as ScenarioUtils from './utils';
5 |
6 | // Run: npm run test:scenario -- --scenario=arrays --index=1
7 | const command = new Command();
8 | command
9 | .allowUnknownOption()
10 | .option('-s, --scenario ', 'Enter Scenario Name')
11 | .option('-i, --index ', 'Enter Test case index')
12 | .parse();
13 |
14 | const opts = command.opts();
15 | const scenarioName = opts.scenario || 'arrays';
16 | const index = +(opts.index || 0);
17 |
18 | describe(`${scenarioName}:`, () => {
19 | const scenarioDir = join(__dirname, 'scenarios', scenarioName);
20 | const scenarios = ScenarioUtils.extractScenarios(scenarioDir);
21 | const scenario: Scenario = scenarios[index] || scenarios[0];
22 | const templatePath = Scenario.getTemplatePath(scenario);
23 | it(`Scenario ${index}: ${templatePath}`, async () => {
24 | let result;
25 | try {
26 | console.log(`Executing scenario: ${scenarioName}, test: ${index}, template: ${templatePath}`);
27 | result = await ScenarioUtils.evaluateScenario(scenarioDir, scenario);
28 | expect(result).toEqual(scenario.output);
29 | } catch (error: any) {
30 | console.error(error);
31 | console.log('Actual result', JSON.stringify(result, null, 2));
32 | console.log('Expected result', JSON.stringify(scenario.output, null, 2));
33 | expect(error.message).toContain(scenario.error);
34 | }
35 | });
36 | });
37 |
--------------------------------------------------------------------------------
/test/scenarios/arrays/data.ts:
--------------------------------------------------------------------------------
1 | import { Scenario } from '../../types';
2 |
3 | export const data: Scenario[] = [
4 | {
5 | output: [1, 2, 3, ['string1', 20.02], ['string2', 'string3', 'aa"a', true, false], 2],
6 | },
7 | ];
8 |
--------------------------------------------------------------------------------
/test/scenarios/arrays/template.jt:
--------------------------------------------------------------------------------
1 | let a = [
2 | "string1", `string2`, 'string3', "aa\"a", true, false,
3 | undefined, null, 20.02, .22, [1., 2, 3], {"b": [1, 2]}
4 | ];
5 | [...~r a[-2], a[0,8], a[1:6],~r a[-1].b[1]]
6 |
7 |
--------------------------------------------------------------------------------
/test/scenarios/assignments/data.ts:
--------------------------------------------------------------------------------
1 | import { Scenario } from '../../types';
2 |
3 | export const data: Scenario[] = [
4 | {
5 | bindings: {
6 | context: {},
7 | },
8 | output: {
9 | a: {
10 | b: [11, 13],
11 | 'c key': 4,
12 | },
13 | },
14 | },
15 | ];
16 |
--------------------------------------------------------------------------------
/test/scenarios/assignments/template.jt:
--------------------------------------------------------------------------------
1 | let a = 3;
2 | a*=3;
3 | --a;
4 | ++a;
5 | let b = -a + 30;
6 | b-=1
7 | b/=2
8 | b+=1;
9 | b--;
10 | b++;
11 | let cKey = "c key";
12 | let c = { a: { b: [a, b], [cKey]: 2 } }
13 | let {d, e, f} = {d: 2, e: 2, f: 1}
14 | // updating binding value
15 | $.context.f = 1
16 | c.a.b[0] = c.a.b[0] + d
17 | c.a.b[1] = c.a.b[1] + e
18 | c.a."c key" = c.a."c key" + f
19 | c.a[cKey] = c.a[cKey] + ~s $.context.f
20 | c
21 |
--------------------------------------------------------------------------------
/test/scenarios/bad_templates/bad_array_coalese_expr.jt:
--------------------------------------------------------------------------------
1 | ??? []
--------------------------------------------------------------------------------
/test/scenarios/bad_templates/bad_async_usage.jt:
--------------------------------------------------------------------------------
1 | async abc
--------------------------------------------------------------------------------
/test/scenarios/bad_templates/bad_context_var.jt:
--------------------------------------------------------------------------------
1 | .a@1#2
--------------------------------------------------------------------------------
/test/scenarios/bad_templates/bad_function_params.jt:
--------------------------------------------------------------------------------
1 | function(1, 2, 3){}
--------------------------------------------------------------------------------
/test/scenarios/bad_templates/bad_function_rest_param.jt:
--------------------------------------------------------------------------------
1 | function(a, ...b, c){}
--------------------------------------------------------------------------------
/test/scenarios/bad_templates/bad_number.jt:
--------------------------------------------------------------------------------
1 | 2.2.3
--------------------------------------------------------------------------------
/test/scenarios/bad_templates/bad_regex.jt:
--------------------------------------------------------------------------------
1 | /?/
--------------------------------------------------------------------------------
/test/scenarios/bad_templates/bad_string.jt:
--------------------------------------------------------------------------------
1 | "aaaa
--------------------------------------------------------------------------------
/test/scenarios/bad_templates/data.ts:
--------------------------------------------------------------------------------
1 | import { Scenario } from '../../types';
2 |
3 | export const data: Scenario[] = [
4 | {
5 | templatePath: 'bad_array_coalese_expr.jt',
6 | error: 'expected at least 1 expression',
7 | },
8 | {
9 | templatePath: 'bad_async_usage.jt',
10 | error: 'Unexpected token',
11 | },
12 | {
13 | templatePath: 'bad_context_var.jt',
14 | error: 'Unexpected token',
15 | },
16 | {
17 | templatePath: 'bad_function_params.jt',
18 | error: 'Unexpected token',
19 | },
20 | {
21 | templatePath: 'bad_function_rest_param.jt',
22 | error: 'Unexpected token',
23 | },
24 | {
25 | templatePath: 'bad_number.jt',
26 | error: 'Unexpected token',
27 | },
28 | {
29 | templatePath: 'bad_regex.jt',
30 | error: 'invalid regular expression',
31 | },
32 | {
33 | templatePath: 'bad_string.jt',
34 | error: 'Unexpected end of template',
35 | },
36 | {
37 | templatePath: 'empty_block.jt',
38 | error: 'empty block is not allowed',
39 | },
40 | {
41 | templatePath: 'empty_object_vars_for_definition.jt',
42 | error: 'Empty object vars',
43 | },
44 | {
45 | templatePath: 'incomplete_statement.jt',
46 | error: 'Unexpected end of template',
47 | },
48 | {
49 | templatePath: 'invalid_new_function_call.jt',
50 | error: 'Unexpected token',
51 | },
52 | {
53 | templatePath: 'invalid_object_vars_for_definition.jt',
54 | error: 'Invalid object vars',
55 | },
56 | {
57 | templatePath: 'invalid_variable_assignment1.jt',
58 | error: 'Invalid assignment path',
59 | },
60 | {
61 | templatePath: 'invalid_variable_assignment2.jt',
62 | error: 'Invalid assignment path',
63 | },
64 | {
65 | templatePath: 'invalid_variable_assignment3.jt',
66 | error: 'Invalid assignment path',
67 | },
68 | {
69 | templatePath: 'invalid_variable_assignment4.jt',
70 | error: 'Invalid assignment path',
71 | },
72 | {
73 | templatePath: 'invalid_variable_assignment5.jt',
74 | error: 'Invalid assignment path',
75 | },
76 | {
77 | templatePath: 'invalid_variable_assignment6.jt',
78 | error: 'Invalid assignment path',
79 | },
80 | {
81 | templatePath: 'invalid_variable_assignment7.jt',
82 | error: 'Invalid assignment path',
83 | },
84 | {
85 | templatePath: 'invalid_variable_assignment8.jt',
86 | error: 'Invalid assignment path',
87 | },
88 | {
89 | templatePath: 'invalid_variable_assignment9.jt',
90 | error: 'Invalid assignment path',
91 | },
92 | {
93 | templatePath: 'invalid_variable_definition.jt',
94 | error: 'Invalid normal vars',
95 | },
96 | {
97 | templatePath: 'invalid_token_after_function_def.jt',
98 | error: 'Unexpected token',
99 | },
100 | {
101 | templatePath: 'object_with_invalid_closing.jt',
102 | error: 'Unexpected token',
103 | },
104 | {
105 | templatePath: 'object_with_invalid_key.jt',
106 | error: 'Unexpected token',
107 | },
108 | {
109 | templatePath: 'reserved_id.jt',
110 | error: 'Reserved ID pattern',
111 | },
112 | {
113 | templatePath: 'unknown_token.jt',
114 | error: 'Unknown token',
115 | },
116 | {
117 | templatePath: 'unsupported_assignment.jt',
118 | error: 'Unexpected token',
119 | },
120 | ];
121 |
--------------------------------------------------------------------------------
/test/scenarios/bad_templates/empty_block.jt:
--------------------------------------------------------------------------------
1 | ()
--------------------------------------------------------------------------------
/test/scenarios/bad_templates/empty_object_vars_for_definition.jt:
--------------------------------------------------------------------------------
1 | let {} = {a: 1}
--------------------------------------------------------------------------------
/test/scenarios/bad_templates/incomplete_statement.jt:
--------------------------------------------------------------------------------
1 | 2 **
--------------------------------------------------------------------------------
/test/scenarios/bad_templates/invalid_new_function_call.jt:
--------------------------------------------------------------------------------
1 | new .a()
--------------------------------------------------------------------------------
/test/scenarios/bad_templates/invalid_object_vars_for_definition.jt:
--------------------------------------------------------------------------------
1 | let {"a"} = {a: 1}
--------------------------------------------------------------------------------
/test/scenarios/bad_templates/invalid_token_after_function_def.jt:
--------------------------------------------------------------------------------
1 | function(){}[]
--------------------------------------------------------------------------------
/test/scenarios/bad_templates/invalid_variable_assignment1.jt:
--------------------------------------------------------------------------------
1 | let a = [{a: 1, b: 2}];
2 | a{.a===1}.b = 3;
--------------------------------------------------------------------------------
/test/scenarios/bad_templates/invalid_variable_assignment2.jt:
--------------------------------------------------------------------------------
1 | let a = [{a: 1, b: 2}];
2 | a[1, 2].b = 3;
--------------------------------------------------------------------------------
/test/scenarios/bad_templates/invalid_variable_assignment3.jt:
--------------------------------------------------------------------------------
1 | let a = [{a: 1, b: 2}];
2 | a..b = 3;
--------------------------------------------------------------------------------
/test/scenarios/bad_templates/invalid_variable_assignment4.jt:
--------------------------------------------------------------------------------
1 | let a = [{a: [1,2,3,4], b: 2}];
2 | a[1:3].b = 3;
--------------------------------------------------------------------------------
/test/scenarios/bad_templates/invalid_variable_assignment5.jt:
--------------------------------------------------------------------------------
1 | [].length = 1
--------------------------------------------------------------------------------
/test/scenarios/bad_templates/invalid_variable_assignment6.jt:
--------------------------------------------------------------------------------
1 | .length = 1
--------------------------------------------------------------------------------
/test/scenarios/bad_templates/invalid_variable_assignment7.jt:
--------------------------------------------------------------------------------
1 | .a.().b = 1
--------------------------------------------------------------------------------
/test/scenarios/bad_templates/invalid_variable_assignment8.jt:
--------------------------------------------------------------------------------
1 | ^.a.b = 1
--------------------------------------------------------------------------------
/test/scenarios/bad_templates/invalid_variable_assignment9.jt:
--------------------------------------------------------------------------------
1 | $.a.b = 1
--------------------------------------------------------------------------------
/test/scenarios/bad_templates/invalid_variable_definition.jt:
--------------------------------------------------------------------------------
1 | let 1 = 2;
--------------------------------------------------------------------------------
/test/scenarios/bad_templates/object_with_invalid_closing.jt:
--------------------------------------------------------------------------------
1 | { "aa": 1]
--------------------------------------------------------------------------------
/test/scenarios/bad_templates/object_with_invalid_key.jt:
--------------------------------------------------------------------------------
1 | {/1/: 2}
--------------------------------------------------------------------------------
/test/scenarios/bad_templates/reserved_id.jt:
--------------------------------------------------------------------------------
1 | let ___a = 1;
--------------------------------------------------------------------------------
/test/scenarios/bad_templates/unknown_token.jt:
--------------------------------------------------------------------------------
1 | \
--------------------------------------------------------------------------------
/test/scenarios/bad_templates/unsupported_assignment.jt:
--------------------------------------------------------------------------------
1 | let a = 1
2 | var b = 1
3 |
--------------------------------------------------------------------------------
/test/scenarios/base/data.ts:
--------------------------------------------------------------------------------
1 | import { Scenario } from '../../types';
2 |
3 | export const data: Scenario[] = [
4 | {
5 | template: '',
6 | output: undefined,
7 | },
8 | ];
9 |
--------------------------------------------------------------------------------
/test/scenarios/bindings/async.jt:
--------------------------------------------------------------------------------
1 | const data = await Promise.all(.map(async lambda await $.square(?0)))
2 | Promise.all(data.map(async function(a){
3 | await $.sqrt(a)
4 | }))
5 |
--------------------------------------------------------------------------------
/test/scenarios/bindings/data.ts:
--------------------------------------------------------------------------------
1 | import { Scenario } from '../../types';
2 |
3 | class CustomError extends Error {}
4 |
5 | export const data: Scenario[] = [
6 | {
7 | templatePath: 'async.jt',
8 | bindings: {
9 | square: (a) => new Promise((resolve) => setTimeout(() => resolve(a * a), 5)),
10 | sqrt: (a) => new Promise((resolve) => setTimeout(() => resolve(Math.sqrt(a)), 5)),
11 | },
12 | input: [1, 2, 3],
13 | output: [1, 2, 3],
14 | },
15 | {
16 | templatePath: 'new_operator.jt',
17 | bindings: { CustomError },
18 | output: new CustomError('some error'),
19 | },
20 | {
21 | bindings: {
22 | a: 10,
23 | b: 2,
24 | c: (a, b) => a * b,
25 | },
26 | output: 20,
27 | },
28 | ];
29 |
--------------------------------------------------------------------------------
/test/scenarios/bindings/new_operator.jt:
--------------------------------------------------------------------------------
1 | new $.CustomError("some error")
--------------------------------------------------------------------------------
/test/scenarios/bindings/template.jt:
--------------------------------------------------------------------------------
1 | $.c($.a, $.b);
2 |
--------------------------------------------------------------------------------
/test/scenarios/block/data.ts:
--------------------------------------------------------------------------------
1 | import { Scenario } from '../../types';
2 |
3 | export const data: Scenario[] = [
4 | {
5 | output: 15,
6 | },
7 | ];
8 |
--------------------------------------------------------------------------------
/test/scenarios/block/template.jt:
--------------------------------------------------------------------------------
1 | let c = (
2 | let a = 1
3 | let b = 2
4 | a + b
5 | )
6 | /*
7 | redefining a and b is possible because
8 | we declared them in block previously
9 | */
10 | let a = 2
11 | let b = 3
12 | (a + b) * c
--------------------------------------------------------------------------------
/test/scenarios/comments/data.ts:
--------------------------------------------------------------------------------
1 | import { Scenario } from '../../types';
2 |
3 | export const data: Scenario[] = [
4 | {
5 | output: ['////', '/*** /// */'],
6 | },
7 | ];
8 |
--------------------------------------------------------------------------------
/test/scenarios/comments/template.jt:
--------------------------------------------------------------------------------
1 | // line comment
2 | /**
3 | ////////////////
4 | * block comment
5 | */
6 | ["////", "/*** /// */"]
--------------------------------------------------------------------------------
/test/scenarios/comparisons/anyof.jt:
--------------------------------------------------------------------------------
1 | {
2 | true: [1, 2] anyof [2, 3],
3 | false: [1, 2] anyof [3, 4]
4 | }
--------------------------------------------------------------------------------
/test/scenarios/comparisons/contains.jt:
--------------------------------------------------------------------------------
1 | {
2 | true: ["aBc" ==* "aB", "abc" contains "c", ["a", "b", "c"] contains "c"],
3 | false: ["aBc" ==* "ab", "abc" contains "d", ["a", "b", "c"] contains "d"]
4 | }
--------------------------------------------------------------------------------
/test/scenarios/comparisons/data.ts:
--------------------------------------------------------------------------------
1 | import { Scenario } from '../../types';
2 |
3 | export const data: Scenario[] = [
4 | {
5 | templatePath: 'anyof.jt',
6 | output: {
7 | true: true,
8 | false: false,
9 | },
10 | },
11 | {
12 | templatePath: 'contains.jt',
13 | output: {
14 | true: [true, true, true],
15 | false: [false, false, false],
16 | },
17 | },
18 | {
19 | templatePath: 'empty.jt',
20 | output: {
21 | true: [true, true],
22 | false: [false, false],
23 | },
24 | },
25 | {
26 | templatePath: 'string_contains_ignore_case.jt',
27 | output: {
28 | true: true,
29 | false: false,
30 | },
31 | },
32 | {
33 | templatePath: 'ends_with.jt',
34 | output: {
35 | true: [true, true],
36 | false: [false, false],
37 | },
38 | },
39 | {
40 | templatePath: 'ends_with_ignore_case.jt',
41 | output: {
42 | true: [true, true],
43 | false: [false, false],
44 | },
45 | },
46 | {
47 | templatePath: 'eq.jt',
48 | output: {
49 | true: true,
50 | false: false,
51 | },
52 | },
53 | {
54 | templatePath: 'ge.jt',
55 | output: {
56 | true: true,
57 | false: false,
58 | },
59 | },
60 | {
61 | templatePath: 'gte.jt',
62 | output: {
63 | true: true,
64 | false: false,
65 | },
66 | },
67 | {
68 | templatePath: 'in.jt',
69 | output: {
70 | true: [true, true],
71 | false: [false, false],
72 | },
73 | },
74 | {
75 | templatePath: 'le.jt',
76 | output: {
77 | true: true,
78 | false: false,
79 | },
80 | },
81 | {
82 | templatePath: 'lte.jt',
83 | output: {
84 | true: true,
85 | false: false,
86 | },
87 | },
88 | {
89 | templatePath: 'ne.jt',
90 | output: {
91 | true: true,
92 | false: false,
93 | },
94 | },
95 | {
96 | templatePath: 'noneof.jt',
97 | output: {
98 | true: true,
99 | false: false,
100 | },
101 | },
102 | {
103 | templatePath: 'not_in.jt',
104 | output: {
105 | true: [true, true],
106 | false: [false, false],
107 | },
108 | },
109 | {
110 | templatePath: 'regex.jt',
111 | output: {
112 | true: [true, true],
113 | false: [false, false],
114 | },
115 | },
116 | {
117 | templatePath: 'size.jt',
118 | output: {
119 | true: [true, true],
120 | false: [false, false],
121 | },
122 | },
123 | {
124 | templatePath: 'starts_with.jt',
125 | output: {
126 | true: [true, true],
127 | false: [false, false],
128 | },
129 | },
130 | {
131 | templatePath: 'starts_with_ignore_case.jt',
132 | output: {
133 | true: [true, true],
134 | false: [false, false],
135 | },
136 | },
137 | {
138 | templatePath: 'string_eq.jt',
139 | output: {
140 | true: true,
141 | false: false,
142 | },
143 | },
144 | {
145 | templatePath: 'string_ne.jt',
146 | output: {
147 | true: true,
148 | false: false,
149 | },
150 | },
151 | {
152 | templatePath: 'string_eq_ingore_case.jt',
153 | output: {
154 | true: true,
155 | false: false,
156 | },
157 | },
158 | {
159 | templatePath: 'string_ne.jt',
160 | output: {
161 | true: true,
162 | false: false,
163 | },
164 | },
165 | {
166 | templatePath: 'string_ne_ingore_case.jt',
167 | output: {
168 | true: true,
169 | false: false,
170 | },
171 | },
172 | {
173 | templatePath: 'subsetof.jt',
174 | output: {
175 | true: [true, true],
176 | false: [false, false],
177 | },
178 | },
179 | ];
180 |
--------------------------------------------------------------------------------
/test/scenarios/comparisons/empty.jt:
--------------------------------------------------------------------------------
1 | {
2 | true: ["" empty true, [] empty true],
3 | false: ["a" empty true, ["a"] empty true]
4 | }
--------------------------------------------------------------------------------
/test/scenarios/comparisons/ends_with.jt:
--------------------------------------------------------------------------------
1 | {
2 | true:["EndsWith" $== "With", "With" ==$ "EndsWith"],
3 | false: ["EndsWith" $== "NotWith", "NotWith" ==$ "EndsWith"]
4 | }
--------------------------------------------------------------------------------
/test/scenarios/comparisons/ends_with_ignore_case.jt:
--------------------------------------------------------------------------------
1 | {
2 | true:["EndsWith" $= "with", "with" =$ "EndsWith"],
3 | false: ["EndsWith" $= "NotWith", "NotWith" =$ "EndsWith"]
4 | }
--------------------------------------------------------------------------------
/test/scenarios/comparisons/eq.jt:
--------------------------------------------------------------------------------
1 | {
2 | true: 10 == 10,
3 | false: 10 == 2
4 | }
--------------------------------------------------------------------------------
/test/scenarios/comparisons/ge.jt:
--------------------------------------------------------------------------------
1 | {
2 | true: 10 > 2,
3 | false: 2 > 10
4 | }
--------------------------------------------------------------------------------
/test/scenarios/comparisons/gte.jt:
--------------------------------------------------------------------------------
1 | {
2 | true: 10 >= 10,
3 | false: 2 >= 10
4 | }
--------------------------------------------------------------------------------
/test/scenarios/comparisons/in.jt:
--------------------------------------------------------------------------------
1 | {
2 | true: ["a" in ["a", "b"], "a" in {"a": 1, "b": 2}],
3 | false: ["c" in ["a", "b"], "c" in {"a": 1, "b": 2}]
4 | }
--------------------------------------------------------------------------------
/test/scenarios/comparisons/le.jt:
--------------------------------------------------------------------------------
1 | {
2 | true: 2 < 10,
3 | false: 10 < 2
4 | }
--------------------------------------------------------------------------------
/test/scenarios/comparisons/lte.jt:
--------------------------------------------------------------------------------
1 | {
2 | true: 10 <= 10,
3 | false: 10 <= 2
4 | }
--------------------------------------------------------------------------------
/test/scenarios/comparisons/ne.jt:
--------------------------------------------------------------------------------
1 | {
2 | true: 10 != 2,
3 | false: 10 != 10
4 | }
--------------------------------------------------------------------------------
/test/scenarios/comparisons/noneof.jt:
--------------------------------------------------------------------------------
1 | {
2 | true: [1, 2] noneof [3, 4],
3 | false: [1, 2] noneof [2, 3],
4 | }
--------------------------------------------------------------------------------
/test/scenarios/comparisons/not_in.jt:
--------------------------------------------------------------------------------
1 | {
2 | true: ["c" nin ["a", "b"], "c" nin {"a": 1, "b": 2}],
3 | false: ["a" nin ["a", "b"], "a" nin {"a": 1, "b": 2}]
4 | }
--------------------------------------------------------------------------------
/test/scenarios/comparisons/regex.jt:
--------------------------------------------------------------------------------
1 | {
2 | true: ['abc' =~ /a.*c/, 'aBC' =~ /a.*c/i],
3 | false: ['abC' =~ /a.*c/, 'aBd' =~ /a.*c/i]
4 | }
--------------------------------------------------------------------------------
/test/scenarios/comparisons/size.jt:
--------------------------------------------------------------------------------
1 | {
2 | true: [["a", "b"] size 2, "ab" size 2],
3 | false: [[] size 1, "" size 1]
4 | }
--------------------------------------------------------------------------------
/test/scenarios/comparisons/starts_with.jt:
--------------------------------------------------------------------------------
1 | {
2 | true:["StartsWith" ^== "Starts", "Starts" ==^ "StartsWith"],
3 | false: ["StartsWith" ^= "NotStarts", "NotStarts" =^ "StartsWith"]
4 | }
--------------------------------------------------------------------------------
/test/scenarios/comparisons/starts_with_ignore_case.jt:
--------------------------------------------------------------------------------
1 | {
2 | true:["StartsWith" ^= "starts", "starts" =^ "StartsWith"],
3 | false: ["StartsWith" ^= "NotStarts", "NotStarts" =^ "StartsWith"]
4 | }
--------------------------------------------------------------------------------
/test/scenarios/comparisons/string_contains_ignore_case.jt:
--------------------------------------------------------------------------------
1 | {
2 | true: "aBc" =* "aB",
3 | false: "ac" =* "aB"
4 | }
--------------------------------------------------------------------------------
/test/scenarios/comparisons/string_eq.jt:
--------------------------------------------------------------------------------
1 | {
2 | true: "aBc" === "aBc",
3 | false: "abc" === "aBc"
4 | }
--------------------------------------------------------------------------------
/test/scenarios/comparisons/string_eq_ingore_case.jt:
--------------------------------------------------------------------------------
1 | {
2 | true: "abc" == "aBc",
3 | false: "adc" == "aBc"
4 | }
--------------------------------------------------------------------------------
/test/scenarios/comparisons/string_ne.jt:
--------------------------------------------------------------------------------
1 | {
2 | true: "abc" !== "aBc",
3 | false: "aBc" !== "aBc"
4 | }
--------------------------------------------------------------------------------
/test/scenarios/comparisons/string_ne_ingore_case.jt:
--------------------------------------------------------------------------------
1 | {
2 | true: "adc" != "aBc",
3 | false: "abc" != "aBc"
4 | }
--------------------------------------------------------------------------------
/test/scenarios/comparisons/subsetof.jt:
--------------------------------------------------------------------------------
1 | {
2 | true: [[1, 2] subsetof [1, 2, 3], [] subsetof [1]],
3 | false: [[1, 2] subsetof [1], [1] subsetof []],
4 | }
--------------------------------------------------------------------------------
/test/scenarios/compile_time_expressions/data.ts:
--------------------------------------------------------------------------------
1 | import { Scenario } from '../../types';
2 |
3 | export const data: Scenario[] = [
4 | {
5 | options: { compileTimeBindings: { a: 1, b: 'string', c: { c: 1.02 }, d: [null, true, false] } },
6 | output: [1, 'string', { c: 1.02 }, [null, true, false]],
7 | },
8 | {
9 | templatePath: 'two_level_path_processing.jt',
10 | options: {
11 | compileTimeBindings: {
12 | paths: ['a.non_existing', 'a.b.non_existing', 'a.c'],
13 | },
14 | },
15 | input: {
16 | a: {
17 | c: 9,
18 | },
19 | },
20 | output: 9,
21 | },
22 | ];
23 |
--------------------------------------------------------------------------------
/test/scenarios/compile_time_expressions/template.jt:
--------------------------------------------------------------------------------
1 | // this will be resolved at compile time
2 | {{ $.([.a, .b, .c, .d]) }};
3 |
--------------------------------------------------------------------------------
/test/scenarios/compile_time_expressions/two_level_path_processing.jt:
--------------------------------------------------------------------------------
1 | // Here we are generating a coalescing expression and recompiling it
2 | // We are generating a string inside and that needs to be recompiled so
3 | // we need to use {{ {{ }} }}
4 | {{ {{ $.paths.map(lambda '.' + ?0).join(' ?? ') }} }}
--------------------------------------------------------------------------------
/test/scenarios/conditions/data.ts:
--------------------------------------------------------------------------------
1 | import { Scenario } from '../../types';
2 |
3 | export const data: Scenario[] = [
4 | {
5 | templatePath: 'if_block.jt',
6 | input: {
7 | a: -5,
8 | },
9 | output: 'a <= 1',
10 | },
11 | {
12 | templatePath: 'if_block.jt',
13 | input: {
14 | a: 1,
15 | },
16 | output: 'a <= 1',
17 | },
18 | {
19 | templatePath: 'if_block.jt',
20 | input: {
21 | a: 2,
22 | },
23 | output: 'a > 1',
24 | },
25 | {
26 | templatePath: 'if_block.jt',
27 | input: {
28 | a: 3,
29 | },
30 | output: 'a > 2',
31 | },
32 | {
33 | templatePath: 'if_block.jt',
34 | input: {
35 | a: 10,
36 | },
37 | output: 'a > 3',
38 | },
39 | {
40 | templatePath: 'if_then.jt',
41 | input: {
42 | a: -5,
43 | },
44 | output: 0,
45 | },
46 | {
47 | templatePath: 'if_then.jt',
48 | input: {
49 | a: 5,
50 | },
51 | output: 5,
52 | },
53 | {
54 | templatePath: 'objects.jt',
55 | input: {
56 | a: 5,
57 | },
58 | output: { message: 'a > 1' },
59 | },
60 | {
61 | templatePath: 'objects.jt',
62 | input: {
63 | a: 0,
64 | },
65 | output: { message: 'a <= 1' },
66 | },
67 | {
68 | input: {
69 | a: 5,
70 | b: 10,
71 | c: 15,
72 | },
73 | output: 15,
74 | },
75 | {
76 | input: {
77 | a: 15,
78 | b: 5,
79 | c: 10,
80 | },
81 | output: 15,
82 | },
83 | {
84 | input: {
85 | a: 10,
86 | b: 15,
87 | c: 5,
88 | },
89 | output: 15,
90 | },
91 | {
92 | templatePath: 'undefined_arr_cond.jt',
93 | input: {
94 | products: [{ a: 1 }, { a: 2 }],
95 | },
96 | output: 'no',
97 | },
98 | {
99 | templatePath: 'undefined_arr_cond.jt',
100 | input: {
101 | products: [{ objectID: 1 }, { objectID: 2 }],
102 | },
103 | output: 'yes',
104 | },
105 | {
106 | templatePath: 'undefined_arr_cond.jt',
107 | input: {
108 | otherProperty: [{ objectID: 1 }, { objectID: 2 }],
109 | },
110 | output: 'no',
111 | },
112 | ];
113 |
--------------------------------------------------------------------------------
/test/scenarios/conditions/if_block.jt:
--------------------------------------------------------------------------------
1 | (.a > 1) ? {
2 | (.a > 2) ? {
3 | (.a > 3) ? {
4 | return "a > 3";
5 | }
6 | return "a > 2";
7 | }
8 | return "a > 1";
9 | }
10 | "a <= 1"
--------------------------------------------------------------------------------
/test/scenarios/conditions/if_then.jt:
--------------------------------------------------------------------------------
1 | let a = .a
2 | a < 0 ? a = 0
3 | a
4 |
--------------------------------------------------------------------------------
/test/scenarios/conditions/objects.jt:
--------------------------------------------------------------------------------
1 | .a > 1 ? { message: "a > 1" } : { message: "a <= 1" }
--------------------------------------------------------------------------------
/test/scenarios/conditions/template.jt:
--------------------------------------------------------------------------------
1 | let a = .a
2 | let b = .b
3 | let c = .c;
4 | a > b ? a > c ? a : c : b > c ? b : c
--------------------------------------------------------------------------------
/test/scenarios/conditions/undefined_arr_cond.jt:
--------------------------------------------------------------------------------
1 | let a = ~r .products.objectID;
2 | Array.isArray(a) ? "yes":"no";
--------------------------------------------------------------------------------
/test/scenarios/context_variables/data.ts:
--------------------------------------------------------------------------------
1 | import { Scenario } from '../../types';
2 |
3 | export const data: Scenario[] = [
4 | {
5 | template: '.a.b@b[]',
6 | description: 'context variable in last part',
7 | input: {
8 | a: {
9 | b: [
10 | {
11 | c: 1,
12 | },
13 | {
14 | c: 2,
15 | },
16 | ],
17 | },
18 | },
19 | output: [{ c: 1 }, { c: 2 }],
20 | },
21 | {
22 | templatePath: 'filter.jt',
23 | input: [[{ a: 1 }], [{ a: 2 }, { a: 3 }], [{ a: 4 }, { a: 5 }, { a: 6 }]],
24 | output: [
25 | {
26 | a: [2, 3],
27 | idx: 0,
28 | },
29 | {
30 | a: [4, 5, 6],
31 | idx: 1,
32 | },
33 | ],
34 | },
35 | {
36 | templatePath: 'function.jt',
37 | input: {
38 | a: [
39 | {
40 | b: [1, 2],
41 | },
42 | {
43 | b: [1, 2],
44 | },
45 | ],
46 | },
47 | output: [1, 2, 2, 3],
48 | },
49 | {
50 | templatePath: 'selector.jt',
51 | input: {
52 | a: 10,
53 | b: [
54 | {
55 | c: [
56 | {
57 | id: 1,
58 | },
59 | {
60 | id: 2,
61 | },
62 | ],
63 | id: 1,
64 | },
65 | {
66 | c: [
67 | {
68 | id: 3,
69 | },
70 | {
71 | id: 4,
72 | },
73 | ],
74 | id: 2,
75 | },
76 | ],
77 | },
78 | output: [
79 | {
80 | cid: 1,
81 | cidx: 0,
82 | bid: 1,
83 | bidx: 0,
84 | a: 10,
85 | },
86 | {
87 | cid: 2,
88 | cidx: 1,
89 | bid: 1,
90 | bidx: 0,
91 | a: 10,
92 | },
93 | {
94 | cid: 3,
95 | cidx: 0,
96 | bid: 2,
97 | bidx: 1,
98 | a: 10,
99 | },
100 | {
101 | cid: 4,
102 | cidx: 1,
103 | bid: 2,
104 | bidx: 1,
105 | a: 10,
106 | },
107 | ],
108 | },
109 | {
110 | input: {
111 | orders: [
112 | {
113 | id: 1,
114 | products: [
115 | {
116 | name: 'A',
117 | price: 10,
118 | },
119 | {
120 | name: 'B',
121 | price: 5,
122 | },
123 | ],
124 | },
125 | {
126 | id: 2,
127 | products: [
128 | {
129 | name: 'A',
130 | price: 10,
131 | },
132 | {
133 | name: 'C',
134 | price: 15,
135 | },
136 | ],
137 | },
138 | ],
139 | },
140 | output: [
141 | {
142 | name: 'A',
143 | price: 10,
144 | orderNum: 0,
145 | orderId: 1,
146 | },
147 | {
148 | name: 'B',
149 | price: 5,
150 | orderNum: 0,
151 | orderId: 1,
152 | },
153 | {
154 | name: 'A',
155 | price: 10,
156 | orderNum: 1,
157 | orderId: 2,
158 | },
159 | {
160 | name: 'C',
161 | price: 15,
162 | orderNum: 1,
163 | orderId: 2,
164 | },
165 | ],
166 | },
167 | ];
168 |
--------------------------------------------------------------------------------
/test/scenarios/context_variables/filter.jt:
--------------------------------------------------------------------------------
1 | /*
2 | .() breaks the path, so path before will evaluated first
3 | and then on the result rest of the path will be executed
4 | */
5 | .{.[].length > 1}.().@item#idx.({
6 | a: ~r item.a,
7 | idx: idx
8 | })
9 |
10 |
--------------------------------------------------------------------------------
/test/scenarios/context_variables/function.jt:
--------------------------------------------------------------------------------
1 | let add = function(a, b) { a + b }
2 | .a#i.b.(add(i, .))
--------------------------------------------------------------------------------
/test/scenarios/context_variables/selector.jt:
--------------------------------------------------------------------------------
1 | ..b@b#bi.c#ci.({
2 | cid: .id,
3 | cidx: ci,
4 | bid: b.id,
5 | bidx: bi,
6 | a: ^.a
7 | })
--------------------------------------------------------------------------------
/test/scenarios/context_variables/template.jt:
--------------------------------------------------------------------------------
1 | .orders@order#idx.products.({
2 | name: .name,
3 | price: .price,
4 | orderNum: idx,
5 | orderId: order.id
6 | })
--------------------------------------------------------------------------------
/test/scenarios/filters/array_filters.jt:
--------------------------------------------------------------------------------
1 | let a = [1, 2, 3, 4, 5, {"a": 1, "b": 2, "c": 3}];
2 | [
3 | a[2:], a[:3], a[3:5], a[...[1, 3]].[0, 1],
4 | ~r a[-2], a[1], a[:-2], a[-2:], a[-1]["a", "b"]
5 | ]
--------------------------------------------------------------------------------
/test/scenarios/filters/data.ts:
--------------------------------------------------------------------------------
1 | import { Scenario } from '../../types';
2 |
3 | export const data: Scenario[] = [
4 | {
5 | templatePath: 'array_filters.jt',
6 | output: [
7 | [
8 | 3,
9 | 4,
10 | 5,
11 | {
12 | a: 1,
13 | b: 2,
14 | c: 3,
15 | },
16 | ],
17 | [1, 2, 3],
18 | [4, 5],
19 | [2, 4],
20 | 5,
21 | 2,
22 | [1, 2, 3, 4],
23 | [
24 | 5,
25 | {
26 | a: 1,
27 | b: 2,
28 | c: 3,
29 | },
30 | ],
31 | [1, 2],
32 | ],
33 | },
34 | {
35 | templatePath: 'invalid_object_index_filters.jt',
36 | error: 'Unexpected token',
37 | },
38 | {
39 | templatePath: 'object_filters.jt',
40 | output: {
41 | a: [3, 4],
42 | b: 2,
43 | },
44 | },
45 | {
46 | templatePath: 'object_indexes.jt',
47 | output: [
48 | { a: 1, b: 2 },
49 | { c: 3, d: 4 },
50 | { a: 1, b: 2 },
51 | ],
52 | },
53 | ];
54 |
--------------------------------------------------------------------------------
/test/scenarios/filters/invalid_object_index_filters.jt:
--------------------------------------------------------------------------------
1 | obj{~a}
--------------------------------------------------------------------------------
/test/scenarios/filters/object_filters.jt:
--------------------------------------------------------------------------------
1 | [
2 | {a: [1, 2], b: "1"}, {a: [3, 4], b: "22"},
3 | {a: [5, 3], b: 3}, {a:[3], b: 4}, {b: 5},
4 | {a: [3, 4], b: 2}
5 | ]
6 | {.a[].length > 1}
7 | {3 in .a}.{.a[].includes(4)}
8 | {!(typeof .b === "string")}
9 |
10 |
--------------------------------------------------------------------------------
/test/scenarios/filters/object_indexes.jt:
--------------------------------------------------------------------------------
1 | let obj = {
2 | a: 1,
3 | b: 2,
4 | c: 3,
5 | d: 4
6 | };
7 | [obj{["a", "b"]}, obj{!["a", "b"]}, obj{~["c", "d"]}]
--------------------------------------------------------------------------------
/test/scenarios/functions/array_functions.jt:
--------------------------------------------------------------------------------
1 | {
2 | map: .map(lambda ?0 * 2),
3 | filter: .filter(lambda ?0 % 2 == 0)
4 | }
--------------------------------------------------------------------------------
/test/scenarios/functions/data.ts:
--------------------------------------------------------------------------------
1 | import { Scenario } from '../../types';
2 |
3 | export const data: Scenario[] = [
4 | {
5 | templatePath: 'array_functions.jt',
6 | input: [1, 2, 3, 4],
7 | output: {
8 | map: [2, 4, 6, 8],
9 | filter: [2, 4],
10 | },
11 | },
12 | {
13 | templatePath: 'function_calls.jt',
14 | output: ['abc', null, undefined],
15 | },
16 | {
17 | templatePath: 'js_date_function.jt',
18 | output: ['2022', 8, 19],
19 | },
20 | {
21 | templatePath: 'new_operator.jt',
22 | output: [
23 | {
24 | name: 'foo',
25 | grade: 1,
26 | },
27 | {
28 | name: 'bar',
29 | grade: 2,
30 | },
31 | ],
32 | },
33 | {
34 | templatePath: 'parent_scope_vars.jt',
35 | output: 90,
36 | },
37 | {
38 | templatePath: 'promise.jt',
39 | input: [1, 2],
40 | output: [1, 2],
41 | },
42 | {
43 | templatePath: 'promise.jt',
44 | input: { a: 1 },
45 | output: { a: 1 },
46 | },
47 | {
48 | output: 80,
49 | },
50 | ];
51 |
--------------------------------------------------------------------------------
/test/scenarios/functions/function_calls.jt:
--------------------------------------------------------------------------------
1 | let a = {a: "Abc", b: null, c: undefined};
2 | a.([.a.toLowerCase(), .b.toLowerCase(), .c.toLowerCase()])
--------------------------------------------------------------------------------
/test/scenarios/functions/js_date_function.jt:
--------------------------------------------------------------------------------
1 | const date = new Date('2022-08-19');
2 | [date.getFullYear().toString(), date.getMonth() + 1, date.getDate()];
3 |
--------------------------------------------------------------------------------
/test/scenarios/functions/new_operator.jt:
--------------------------------------------------------------------------------
1 | const Person = {
2 | Student: function(name, grade) {
3 | this.name = name;
4 | this.grade = grade;
5 | }
6 | }
7 |
8 | const foo = new Person.Student('foo', 1);
9 | const bar = new Person.Student('bar', 2);
10 | [
11 | {name: foo.name, grade: foo.grade},
12 | {...bar}
13 | ]
--------------------------------------------------------------------------------
/test/scenarios/functions/parent_scope_vars.jt:
--------------------------------------------------------------------------------
1 | let a = 1; let b = 2
2 | let fn = function(e){
3 | (a+b)*e
4 | }
5 | fn(10) + fn(20)
--------------------------------------------------------------------------------
/test/scenarios/functions/promise.jt:
--------------------------------------------------------------------------------
1 | await new Promise(function(resolve) {
2 | setTimeout(lambda resolve(^), 100)
3 | });
--------------------------------------------------------------------------------
/test/scenarios/functions/template.jt:
--------------------------------------------------------------------------------
1 | let normalFn = function(a) {
2 | a + 10
3 | };
4 | let lambdaFn = lambda ?0 + 10;
5 | let spreadFn = function(...a) {
6 | a.reduce(lambda ?0 + ?1, 0)
7 | };
8 | let fnArr = {spread: spreadFn, other: [normalFn, lambdaFn]};
9 | fnArr.spread(fnArr.other[0](10), fnArr.other[1](20), function(){30}())
--------------------------------------------------------------------------------
/test/scenarios/increment_statements/data.ts:
--------------------------------------------------------------------------------
1 | import { Scenario } from '../../types';
2 |
3 | export const data: Scenario[] = [
4 | {
5 | templatePath: 'postfix_decrement_on_literal.jt',
6 | error: 'Invalid postfix increment expression',
7 | },
8 | {
9 | templatePath: 'postfix_decrement_on_non_id.jt',
10 | error: 'Invalid postfix increment expression',
11 | },
12 | {
13 | templatePath: 'postfix_increment_on_literal.jt',
14 | error: 'Invalid postfix increment expression',
15 | },
16 | {
17 | templatePath: 'postfix_increment_on_non_id.jt',
18 | error: 'Invalid postfix increment expression',
19 | },
20 | {
21 | templatePath: 'prefix_decrement_on_literal.jt',
22 | error: 'Invalid prefix increment expression',
23 | },
24 | {
25 | templatePath: 'prefix_increment_on_literal.jt',
26 | error: 'Invalid prefix increment expression',
27 | },
28 | ];
29 |
--------------------------------------------------------------------------------
/test/scenarios/increment_statements/postfix_decrement_on_literal.jt:
--------------------------------------------------------------------------------
1 | 1--
--------------------------------------------------------------------------------
/test/scenarios/increment_statements/postfix_decrement_on_non_id.jt:
--------------------------------------------------------------------------------
1 | .a--
--------------------------------------------------------------------------------
/test/scenarios/increment_statements/postfix_increment_on_literal.jt:
--------------------------------------------------------------------------------
1 | 1++
--------------------------------------------------------------------------------
/test/scenarios/increment_statements/postfix_increment_on_non_id.jt:
--------------------------------------------------------------------------------
1 | .a++
--------------------------------------------------------------------------------
/test/scenarios/increment_statements/prefix_decrement_on_literal.jt:
--------------------------------------------------------------------------------
1 | --1
--------------------------------------------------------------------------------
/test/scenarios/increment_statements/prefix_increment_on_literal.jt:
--------------------------------------------------------------------------------
1 | ++1
--------------------------------------------------------------------------------
/test/scenarios/inputs/data.ts:
--------------------------------------------------------------------------------
1 | import { Scenario } from '../../types';
2 |
3 | export const data: Scenario[] = [
4 | {
5 | input: {
6 | a: 10,
7 | b: 2,
8 | c: {
9 | d: 30,
10 | },
11 | },
12 | output: 50,
13 | },
14 | ];
15 |
--------------------------------------------------------------------------------
/test/scenarios/inputs/template.jt:
--------------------------------------------------------------------------------
1 | .c.(.d + ^.a * ^.b);
2 |
--------------------------------------------------------------------------------
/test/scenarios/logics/data.ts:
--------------------------------------------------------------------------------
1 | import { Scenario } from '../../types';
2 |
3 | export const data: Scenario[] = [
4 | {
5 | output: [3, 0, 2, 3, 0, 3, true, true],
6 | },
7 | ];
8 |
--------------------------------------------------------------------------------
/test/scenarios/logics/template.jt:
--------------------------------------------------------------------------------
1 | [
2 | 2 && 3,
3 | 0 && 3,
4 | 2 || 3,
5 | 0 || 3,
6 | 0 ?? 3,
7 | ??? [null, undefined, 3],
8 | !false, !!true,
9 | ];
10 |
--------------------------------------------------------------------------------
/test/scenarios/loops/break_without_condition.jt:
--------------------------------------------------------------------------------
1 | for {
2 | break;
3 | }
4 |
--------------------------------------------------------------------------------
/test/scenarios/loops/break_without_loop.jt:
--------------------------------------------------------------------------------
1 |
2 | (.num < 0) ? break;
--------------------------------------------------------------------------------
/test/scenarios/loops/complex_loop.jt:
--------------------------------------------------------------------------------
1 | let count = 0;
2 | for (let i = 0; i < 5; i++) {
3 | for (let j=0; j < 5; j++) {
4 | i < j ? {
5 | j > 4 ? continue;
6 | count++;
7 | }
8 | }
9 | }
10 | count;
--------------------------------------------------------------------------------
/test/scenarios/loops/continue.jt:
--------------------------------------------------------------------------------
1 | let count = 0;
2 | for (let i=0;i <= ^.num;i++) {
3 | console.log(i);
4 | i % 2 === 0 ? continue;
5 | count+=i;
6 | }
7 | count
--------------------------------------------------------------------------------
/test/scenarios/loops/continue_without_condition.jt:
--------------------------------------------------------------------------------
1 | for {
2 | continue;
3 | }
--------------------------------------------------------------------------------
/test/scenarios/loops/continue_without_loop.jt:
--------------------------------------------------------------------------------
1 |
2 | (.num < 0) ? continue;
--------------------------------------------------------------------------------
/test/scenarios/loops/data.ts:
--------------------------------------------------------------------------------
1 | import { Scenario } from '../../types';
2 |
3 | export const data: Scenario[] = [
4 | {
5 | templatePath: 'break_without_condition.jt',
6 | error:
7 | 'return, throw, continue and break statements are only allowed as last statements in conditional expressions',
8 | },
9 | {
10 | templatePath: 'break_without_loop.jt',
11 | error: 'encounted loop control outside loop',
12 | },
13 | {
14 | templatePath: 'complex_loop.jt',
15 | output: 10,
16 | },
17 | {
18 | templatePath: 'continue_without_condition.jt',
19 | error:
20 | 'return, throw, continue and break statements are only allowed as last statements in conditional expressions',
21 | },
22 | {
23 | templatePath: 'continue_without_loop.jt',
24 | error: 'encounted loop control outside loop',
25 | },
26 | {
27 | templatePath: 'continue.jt',
28 | input: {
29 | num: 10,
30 | },
31 | output: 25,
32 | },
33 | {
34 | templatePath: 'empty_loop.jt',
35 | error: 'Empty statements are not allowed in loop and condtional expressions',
36 | },
37 | {
38 | templatePath: 'just_for.jt',
39 | input: {
40 | num: 10,
41 | },
42 | output: 55,
43 | },
44 | {
45 | input: {
46 | num: 10,
47 | },
48 | output: 55,
49 | templatePath: 'no_init.jt',
50 | },
51 | {
52 | input: {
53 | num: 10,
54 | },
55 | output: 55,
56 | templatePath: 'no_test.jt',
57 | },
58 | {
59 | input: {
60 | num: 10,
61 | },
62 | output: 55,
63 | templatePath: 'no_update.jt',
64 | },
65 | {
66 | templatePath: 'statement_after_break.jt',
67 | error:
68 | 'return, throw, continue and break statements are only allowed as last statements in conditional expressions',
69 | },
70 | {
71 | templatePath: 'statement_after_continue.jt',
72 | error:
73 | 'return, throw, continue and break statements are only allowed as last statements in conditional expressions',
74 | },
75 | {
76 | input: {
77 | num: 10,
78 | },
79 | output: 55,
80 | },
81 | ];
82 |
--------------------------------------------------------------------------------
/test/scenarios/loops/empty_loop.jt:
--------------------------------------------------------------------------------
1 | for {}
--------------------------------------------------------------------------------
/test/scenarios/loops/just_for.jt:
--------------------------------------------------------------------------------
1 | let count = 0;
2 | let i = 0;
3 | for {
4 | i > ^.num ? break;
5 | count+=i;
6 | i++;
7 | }
8 | count
--------------------------------------------------------------------------------
/test/scenarios/loops/no_init.jt:
--------------------------------------------------------------------------------
1 | let count = 0;
2 | let i = 0;
3 | for (;i <= ^.num;i++) {
4 | count+=i;
5 | }
6 | count
--------------------------------------------------------------------------------
/test/scenarios/loops/no_test.jt:
--------------------------------------------------------------------------------
1 | let count = 0;
2 | for (let i=0;;i++) {
3 | i > ^.num ? break;
4 | count+=i;
5 | }
6 | count
--------------------------------------------------------------------------------
/test/scenarios/loops/no_update.jt:
--------------------------------------------------------------------------------
1 | let count = 0;
2 | for (let i=0;i <= ^.num;) {
3 | count+=i;
4 | i++;
5 | }
6 | count
--------------------------------------------------------------------------------
/test/scenarios/loops/statement_after_break.jt:
--------------------------------------------------------------------------------
1 | for {
2 | .num > 1 ? {
3 | break;
4 | let count = 0;
5 | }
6 | }
--------------------------------------------------------------------------------
/test/scenarios/loops/statement_after_continue.jt:
--------------------------------------------------------------------------------
1 | for {
2 | .num > 1 ? {
3 | continue;
4 | let count = 0;
5 | }
6 | }
--------------------------------------------------------------------------------
/test/scenarios/loops/template.jt:
--------------------------------------------------------------------------------
1 | let count = 0;
2 | for (let i=0;i <= ^.num;i++) {
3 | count+=i;
4 | }
5 | count
--------------------------------------------------------------------------------
/test/scenarios/mappings/all_features.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "input": "$.userId",
4 | "output": "$.user.id"
5 | },
6 | {
7 | "input": "$.discount",
8 | "output": "$.events[0].items[*].discount"
9 | },
10 | {
11 | "input": "$.products[?(@.category)].id",
12 | "output": "$.events[0].items[*].product_id"
13 | },
14 | {
15 | "input": "$.coupon",
16 | "output": "$.events[0].items[*].coupon_code"
17 | },
18 | {
19 | "input": "$.events[0]",
20 | "output": "$.events[0].name"
21 | },
22 | {
23 | "input": "$.products[*].name",
24 | "output": "$.events[0].items[*].product_name"
25 | },
26 | {
27 | "from": "$.products[*].category",
28 | "to": "$.events[0].items[*].product_category"
29 | },
30 | {
31 | "input": "$.products[*].variations[*].size",
32 | "output": "$.events[0].items[*].options[*].s"
33 | },
34 | {
35 | "input": "$.products[*].(@.price * @.quantity * (1 - $.discount / 100))",
36 | "output": "$.events[0].items[*].value"
37 | },
38 | {
39 | "input": "$.products[?(@.category)].(@.price * @.quantity * (1 - $.discount / 100)).sum()",
40 | "output": "$.events[0].revenue"
41 | },
42 | {
43 | "input": "$.products[*].variations[*].length",
44 | "output": "$.events[0].items[*].options[*].l"
45 | },
46 | {
47 | "input": "$.products[*].variations[*].width",
48 | "output": "$.events[0].items[*].options[*].w"
49 | },
50 | {
51 | "input": "$.products[*].variations[*].color",
52 | "output": "$.events[0].items[*].options[*].c"
53 | },
54 | {
55 | "input": "$.products[*].variations[*].height",
56 | "output": "$.events[0].items[*].options[*].h"
57 | }
58 | ]
59 |
--------------------------------------------------------------------------------
/test/scenarios/mappings/context_vars_mapping.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "from": "$.a[*].#index",
4 | "to": "$.b[*].#index"
5 | },
6 | {
7 | "from": "$.a[*].#index.foo",
8 | "to": "$.b[*].bar"
9 | }
10 | ]
11 |
--------------------------------------------------------------------------------
/test/scenarios/mappings/data.ts:
--------------------------------------------------------------------------------
1 | import type { Scenario } from '../../types';
2 |
3 | const input = {
4 | userId: 'u1',
5 | discount: 10,
6 | coupon: 'DISCOUNT',
7 | events: ['purchase', 'custom'],
8 | details: {
9 | name: 'Purchase',
10 | timestamp: 1630000000,
11 | },
12 | context: {
13 | traits: {
14 | email: 'dummy@example.com',
15 | first_name: 'John',
16 | last_name: 'Doe',
17 | phone: '1234567890',
18 | },
19 | },
20 | products: [
21 | {
22 | id: 1,
23 | name: 'p1',
24 | category: 'baby',
25 | price: 3,
26 | quantity: 2,
27 | variations: [
28 | {
29 | color: 'blue',
30 | size: 1,
31 | },
32 | {
33 | size: 2,
34 | },
35 | ],
36 | },
37 | {
38 | id: 2,
39 | name: 'p2',
40 | price: 5,
41 | quantity: 3,
42 | variations: [
43 | {
44 | length: 1,
45 | },
46 | {
47 | color: 'red',
48 | length: 2,
49 | },
50 | ],
51 | },
52 | {
53 | id: 3,
54 | name: 'p3',
55 | category: 'home',
56 | price: 10,
57 | quantity: 1,
58 | variations: [
59 | {
60 | width: 1,
61 | height: 2,
62 | length: 3,
63 | },
64 | ],
65 | },
66 | ],
67 | };
68 | export const data: Scenario[] = [
69 | {
70 | mappingsPath: 'all_features.json',
71 | input,
72 | output: {
73 | events: [
74 | {
75 | items: [
76 | {
77 | discount: 10,
78 | product_id: 1,
79 | coupon_code: 'DISCOUNT',
80 | product_name: 'p1',
81 | product_category: 'baby',
82 | options: [
83 | {
84 | s: 1,
85 | c: 'blue',
86 | },
87 | {
88 | s: 2,
89 | },
90 | ],
91 | value: 5.4,
92 | },
93 | {
94 | discount: 10,
95 | product_id: 3,
96 | coupon_code: 'DISCOUNT',
97 | product_name: 'p3',
98 | product_category: 'home',
99 | options: [
100 | {
101 | l: 3,
102 | w: 1,
103 | h: 2,
104 | },
105 | ],
106 | value: 9,
107 | },
108 | ],
109 | name: 'purchase',
110 | revenue: 14.4,
111 | },
112 | ],
113 | user: {
114 | id: 'u1',
115 | },
116 | },
117 | },
118 | {
119 | mappingsPath: 'context_vars_mapping.json',
120 | input: {
121 | a: [
122 | {
123 | foo: 1,
124 | },
125 | {
126 | foo: 2,
127 | },
128 | ],
129 | },
130 | output: {
131 | b: [
132 | { bar: 1, index: 0 },
133 | { bar: 2, index: 1 },
134 | ],
135 | },
136 | },
137 | {
138 | mappings: [
139 | {
140 | from: '$.a[*]',
141 | to: '$.b[*].#index',
142 | },
143 | ],
144 | error: 'Invalid mapping',
145 | },
146 | {
147 | mappings: [
148 | {
149 | from: '1',
150 | to: '$.b[*].#index',
151 | },
152 | ],
153 | error: 'Invalid mapping',
154 | },
155 | {
156 | mappingsPath: 'filters.json',
157 |
158 | input,
159 | output: {
160 | items: [
161 | {
162 | product_id: 1,
163 | product_name: 'p1',
164 | product_category: 'baby',
165 | },
166 | {
167 | product_id: 3,
168 | product_name: 'p3',
169 | product_category: 'home',
170 | },
171 | ],
172 | },
173 | },
174 | {
175 | mappingsPath: 'index_mappings.json',
176 | input,
177 | output: {
178 | events: [
179 | {
180 | name: 'purchase',
181 | type: 'identify',
182 | },
183 | {
184 | name: 'custom',
185 | type: 'track',
186 | },
187 | ],
188 | },
189 | },
190 | {
191 | mappingsPath: 'invalid_array_index_mappings.json',
192 | error: 'Invalid mapping',
193 | },
194 | {
195 | description: 'Index mappings in last part',
196 | mappings: [
197 | {
198 | from: '$.a[0]',
199 | to: '$.b[0]',
200 | },
201 | {
202 | from: '$.a[1]',
203 | to: '$.b[1]',
204 | },
205 | ],
206 | input: { a: [1, 2, 3] },
207 | output: { b: [1, 2] },
208 | },
209 | {
210 | mappingsPath: 'invalid_array_mappings.json',
211 | error: 'Invalid mapping',
212 | },
213 | {
214 | mappingsPath: 'invalid_object_mappings.json',
215 | error: 'Invalid mapping',
216 | },
217 | {
218 | mappingsPath: 'mappings_with_root_fields.json',
219 | input,
220 | output: {
221 | items: [
222 | {
223 | product_id: 1,
224 | product_name: 'p1',
225 | product_category: 'baby',
226 | discount: 10,
227 | coupon_code: 'DISCOUNT',
228 | },
229 | {
230 | product_id: 2,
231 | product_name: 'p2',
232 | discount: 10,
233 | coupon_code: 'DISCOUNT',
234 | },
235 | {
236 | product_id: 3,
237 | product_name: 'p3',
238 | product_category: 'home',
239 | discount: 10,
240 | coupon_code: 'DISCOUNT',
241 | },
242 | ],
243 | },
244 | },
245 | {
246 | mappingsPath: 'missing_array_index_mappings.json',
247 | error: 'Invalid mapping',
248 | },
249 | {
250 | mappingsPath: 'nested_mappings.json',
251 | input,
252 | output: {
253 | items: [
254 | {
255 | product_id: 1,
256 | product_name: 'p1',
257 | product_category: 'baby',
258 | options: [
259 | {
260 | s: 1,
261 | c: 'blue',
262 | },
263 | {
264 | s: 2,
265 | },
266 | ],
267 | },
268 | {
269 | product_id: 2,
270 | product_name: 'p2',
271 | options: [
272 | {
273 | l: 1,
274 | },
275 | {
276 | l: 2,
277 | c: 'red',
278 | },
279 | ],
280 | },
281 | {
282 | product_id: 3,
283 | product_name: 'p3',
284 | product_category: 'home',
285 | options: [
286 | {
287 | l: 3,
288 | w: 1,
289 | h: 2,
290 | },
291 | ],
292 | },
293 | ],
294 | },
295 | },
296 | {
297 | mappingsPath: 'non_path_output.json',
298 | output: {
299 | 'Content-Type': 'application/json',
300 | 'a.b.c': 3,
301 | bar: 1,
302 | c: { 'Content-Type': 'text/plain' },
303 | 'x-bar': 2,
304 | },
305 | },
306 | {
307 | mappingsPath: 'object_mappings.json',
308 | input: {
309 | user_id: 1,
310 | traits1: {
311 | name: 'John Doe',
312 | age: 30,
313 | },
314 | traits2: [
315 | {
316 | name: {
317 | value: 'John Doe',
318 | },
319 | },
320 | {
321 | age: {
322 | value: 30,
323 | },
324 | },
325 | ],
326 | traits3: {
327 | display_name: 'Rudderstack Inc.',
328 | category: 'Analytics',
329 | custom_properties: {
330 | bar: 1,
331 | },
332 | },
333 | },
334 | output: {
335 | user_id: {
336 | value: 1,
337 | },
338 | traits1: {
339 | value: {
340 | name: 'John Doe',
341 | age: 30,
342 | },
343 | },
344 | traits2: {
345 | value: [
346 | {
347 | name: {
348 | value: 'John Doe',
349 | },
350 | },
351 | {
352 | age: {
353 | value: 30,
354 | },
355 | },
356 | ],
357 | },
358 | traits3: {
359 | value: {
360 | display_name: 'Rudderstack Inc.',
361 | category: 'Analytics',
362 | custom_properties: {
363 | bar: 1,
364 | },
365 | },
366 | },
367 | properties1: {
368 | name: {
369 | value: 'John Doe',
370 | },
371 | age: {
372 | value: 30,
373 | },
374 | },
375 | properties2: [
376 | {
377 | name: 'John Doe',
378 | },
379 | {
380 | age: 30,
381 | },
382 | ],
383 | properties3: {
384 | display_name: 'Rudderstack Inc.',
385 | category: 'Analytics',
386 | custom_properties: {
387 | bar: 1,
388 | },
389 | name: 'Rudderstack Inc.',
390 | custom: {
391 | bar: 1,
392 | foo: 1,
393 | },
394 | },
395 | },
396 | },
397 | {
398 | mappingsPath: 'or_mappings.json',
399 | input: {
400 | context: {
401 | properties: {
402 | name: 'John',
403 | age: 30,
404 | },
405 | },
406 | },
407 | output: {
408 | user: {
409 | name: 'John',
410 | age: 30,
411 | },
412 | },
413 | },
414 | {
415 | mappingsPath: 'override_nested_arrays.json',
416 | input: {
417 | properties: {
418 | type: 'bar',
419 | products: [
420 | {
421 | name: 'a',
422 | },
423 | {
424 | name: 'b',
425 | },
426 | ],
427 | },
428 | },
429 | output: {
430 | properties: {
431 | products: [{ category: 'bar' }, { category: 'bar' }],
432 | type: 'bar',
433 | },
434 | },
435 | },
436 | {
437 | mappingsPath: 'or_mappings.json',
438 | input: {
439 | properties: {
440 | name: 'John Doe',
441 | age: 30,
442 | },
443 | context: {
444 | properties: {
445 | name: 'John',
446 | age: 30,
447 | },
448 | },
449 | },
450 | output: {
451 | user: {
452 | name: 'John Doe',
453 | age: 30,
454 | },
455 | },
456 | },
457 | {
458 | mappingsPath: 'or_mappings.json',
459 | input: {
460 | properties: {
461 | name: 'John Doe',
462 | age: 30,
463 | },
464 | },
465 | output: {
466 | user: {
467 | name: 'John Doe',
468 | age: 30,
469 | },
470 | },
471 | },
472 | {
473 | mappingsPath: 'root_array_mappings.json',
474 | input: [
475 | {
476 | user_id: 1,
477 | user_name: 'John Doe',
478 | },
479 | {
480 | user_id: 2,
481 | user_name: 'Jane Doe',
482 | },
483 | ],
484 | output: [
485 | {
486 | user: {
487 | id: 1,
488 | name: 'John Doe',
489 | },
490 | },
491 | {
492 | user: {
493 | id: 2,
494 | name: 'Jane Doe',
495 | },
496 | },
497 | ],
498 | },
499 | {
500 | mappingsPath: 'root_context_vars_mapping.json',
501 | input: [
502 | {
503 | foo: 1,
504 | },
505 | {
506 | foo: 2,
507 | },
508 | ],
509 | output: [
510 | {
511 | bar: 1,
512 | index: 0,
513 | },
514 | {
515 | bar: 2,
516 | index: 1,
517 | },
518 | ],
519 | },
520 | {
521 | mappingsPath: 'root_index_mappings.json',
522 | input: {
523 | id: 1,
524 | name: 'John Doe',
525 | },
526 | output: [
527 | {
528 | user_id: 1,
529 | user_name: 'John Doe',
530 | },
531 | ],
532 | },
533 | {
534 | mappingsPath: 'root_mappings.json',
535 | input,
536 | output: {
537 | traits: {
538 | email: 'dummy@example.com',
539 | first_name: 'John',
540 | last_name: 'Doe',
541 | phone: '1234567890',
542 | },
543 | event_names: ['purchase', 'custom'],
544 | name: 'Purchase',
545 | timestamp: 1630000000,
546 | },
547 | },
548 | {
549 | mappingsPath: 'root_nested_mappings.json',
550 | input: [
551 | {
552 | user_id: 1,
553 | user_name: 'John Doe',
554 | },
555 | {
556 | user_id: 2,
557 | user_name: 'Jane Doe',
558 | },
559 | ],
560 | output: [
561 | {
562 | user_id: {
563 | value: 1,
564 | },
565 | user_name: {
566 | value: 'John Doe',
567 | },
568 | },
569 | {
570 | user_id: {
571 | value: 2,
572 | },
573 | user_name: {
574 | value: 'Jane Doe',
575 | },
576 | },
577 | ],
578 | },
579 | {
580 | mappingsPath: 'root_object_mappings.json',
581 | input: {
582 | user_id: 1,
583 | user_name: 'John Doe',
584 | },
585 | output: {
586 | user_id: {
587 | value: 1,
588 | },
589 | user_name: {
590 | value: 'John Doe',
591 | },
592 | },
593 | },
594 | {
595 | description: 'array mappings in last part',
596 | mappings: [
597 | {
598 | from: '$.a[*]',
599 | to: '$.b[*]',
600 | },
601 | ],
602 | input: { a: [1, 2, 3] },
603 | output: { b: [1, 2, 3] },
604 | },
605 | {
606 | description: 'array mappings to scalar value',
607 | mappings: [
608 | {
609 | from: '1',
610 | to: '$.a[*].b',
611 | },
612 | ],
613 | output: { a: [{ b: 1 }] },
614 | },
615 | {
616 | description: 'array mappings to scalar value',
617 | mappings: [
618 | {
619 | from: '1',
620 | to: '$.a[*]',
621 | },
622 | ],
623 | output: { a: [1] },
624 | },
625 | {
626 | mappingsPath: 'simple_array_mappings.json',
627 | input: {
628 | user_id: 1,
629 | user_name: 'John Doe',
630 | },
631 | output: {
632 | users: [
633 | {
634 | id: 1,
635 | name: 'John Doe',
636 | },
637 | ],
638 | },
639 | },
640 | {
641 | input: {
642 | a: [
643 | {
644 | a: 1,
645 | },
646 | {
647 | a: 2,
648 | },
649 | ],
650 | },
651 | output: 3,
652 | },
653 | {
654 | template: '~m[1, 2]',
655 | error: 'Invalid mapping',
656 | },
657 | {
658 | template: '~m[{}]',
659 | error: 'Invalid mapping',
660 | },
661 | {
662 | template: '~m[{input: 1, output: 2}]',
663 | error: 'Invalid mapping',
664 | },
665 | {
666 | mappingsPath: 'transformations.json',
667 | input,
668 | output: {
669 | items: [
670 | {
671 | product_id: 1,
672 | product_name: 'p1',
673 | product_category: 'baby',
674 | value: 5.4,
675 | },
676 | {
677 | product_id: 2,
678 | product_name: 'p2',
679 | value: 13.5,
680 | },
681 | {
682 | product_id: 3,
683 | product_name: 'p3',
684 | product_category: 'home',
685 | value: 9,
686 | },
687 | ],
688 | revenue: 27.9,
689 | },
690 | },
691 | ];
692 |
--------------------------------------------------------------------------------
/test/scenarios/mappings/filters.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "input": "$.products[?(@.category)].id",
4 | "output": "$.items[*].product_id"
5 | },
6 | {
7 | "input": "$.products[*].name",
8 | "output": "$.items[*].product_name"
9 | },
10 | {
11 | "input": "$.products[*].category",
12 | "output": "$.items[*].product_category"
13 | }
14 | ]
15 |
--------------------------------------------------------------------------------
/test/scenarios/mappings/index_mappings.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "input": "$.events[0]",
4 | "output": "$.events[0].name"
5 | },
6 | {
7 | "input": "'identify'",
8 | "output": "$.events[0].type"
9 | },
10 | {
11 | "input": "$.events[1]",
12 | "output": "$.events[1].name"
13 | },
14 | {
15 | "input": "'track'",
16 | "output": "$.events[1].type"
17 | }
18 | ]
19 |
--------------------------------------------------------------------------------
/test/scenarios/mappings/invalid_array_index_mappings.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "from": "$.products[*].a",
4 | "to": "$.items[*].a"
5 | },
6 | {
7 | "from": "$.products[*].b",
8 | "to": "$.items[0].b"
9 | }
10 | ]
11 |
--------------------------------------------------------------------------------
/test/scenarios/mappings/invalid_array_mappings.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "input": "$.events[0]",
4 | "output": "$.events[0].name"
5 | },
6 | {
7 | "input": "$.discount",
8 | "output": "$.events[0].name[*].discount"
9 | }
10 | ]
11 |
--------------------------------------------------------------------------------
/test/scenarios/mappings/invalid_object_mappings.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "input": "$.traits1",
4 | "output": "$.properties1.*.value"
5 | }
6 | ]
7 |
--------------------------------------------------------------------------------
/test/scenarios/mappings/mappings_with_root_fields.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "input": "$.discount",
4 | "output": "$.items[*].discount"
5 | },
6 | {
7 | "input": "$.products[*].id",
8 | "output": "$.items[*].product_id"
9 | },
10 | {
11 | "input": "$.products[*].name",
12 | "output": "$.items[*].product_name"
13 | },
14 | {
15 | "input": "$.coupon",
16 | "output": "$.items[*].coupon_code"
17 | },
18 | {
19 | "input": "$.products[*].category",
20 | "output": "$.items[*].product_category"
21 | }
22 | ]
23 |
--------------------------------------------------------------------------------
/test/scenarios/mappings/missing_array_index_mappings.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "input": "$.events[1]",
4 | "output": "$.events[1].name"
5 | },
6 | {
7 | "input": "'track'",
8 | "output": "$.events[1].type"
9 | }
10 | ]
11 |
--------------------------------------------------------------------------------
/test/scenarios/mappings/nested_mappings.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "input": "$.products[*].id",
4 | "output": "$.items[*].product_id"
5 | },
6 | {
7 | "input": "$.products[*].name",
8 | "output": "$.items[*].product_name"
9 | },
10 | {
11 | "input": "$.products[*].category",
12 | "output": "$.items[*].product_category"
13 | },
14 | {
15 | "input": "$.products[*].variations[*].size",
16 | "output": "$.items[*].options[*].s"
17 | },
18 | {
19 | "input": "$.products[*].variations[*].length",
20 | "output": "$.items[*].options[*].l"
21 | },
22 | {
23 | "input": "$.products[*].variations[*].width",
24 | "output": "$.items[*].options[*].w"
25 | },
26 | {
27 | "input": "$.products[*].variations[*].color",
28 | "output": "$.items[*].options[*].c"
29 | },
30 | {
31 | "input": "$.products[*].variations[*].height",
32 | "output": "$.items[*].options[*].h"
33 | }
34 | ]
35 |
--------------------------------------------------------------------------------
/test/scenarios/mappings/non_path_output.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "input": "'application/json'",
4 | "output": "Content-Type"
5 | },
6 | {
7 | "input": "1",
8 | "output": "bar"
9 | },
10 | {
11 | "input": "2",
12 | "output": "x-bar"
13 | },
14 | {
15 | "input": "2",
16 | "description": "This mapping will be ignored because the output is not defined"
17 | },
18 | {
19 | "input": "3",
20 | "output": "'a.b.c'"
21 | },
22 | {
23 | "input": "'text/plain'",
24 | "output": "c.'Content-Type'"
25 | }
26 | ]
27 |
--------------------------------------------------------------------------------
/test/scenarios/mappings/object_mappings.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "input": "$.*",
4 | "output": "$.*.value"
5 | },
6 | {
7 | "input": "$.traits1.*",
8 | "output": "$.properties1.*.value"
9 | },
10 | {
11 | "input": "$.traits2[*].*.value",
12 | "output": "$.properties2[*].*"
13 | },
14 | {
15 | "input": "$.traits3",
16 | "output": "$.properties3"
17 | },
18 | {
19 | "input": "$.traits3.display_name",
20 | "output": "$.properties3.name"
21 | },
22 | {
23 | "input": "$.traits3.custom_properties",
24 | "output": "$.properties3['custom']"
25 | },
26 | {
27 | "input": "$.traits3.custom_properties.bar",
28 | "output": "$.properties3.custom.foo"
29 | }
30 | ]
31 |
--------------------------------------------------------------------------------
/test/scenarios/mappings/or_mappings.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "input": "$.properties.name",
4 | "output": "$.user.name"
5 | },
6 | {
7 | "input": "$.properties.age",
8 | "output": "$.user.age"
9 | },
10 | {
11 | "input": "$.context.properties.name",
12 | "output": "$.user.name"
13 | },
14 | {
15 | "input": "$.context.properties.age",
16 | "output": "$.user.age"
17 | }
18 | ]
19 |
--------------------------------------------------------------------------------
/test/scenarios/mappings/override_nested_arrays.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "from": "$.properties",
4 | "to": "$.properties"
5 | },
6 | {
7 | "to": "$.properties.products[0].category",
8 | "from": "$.properties.type"
9 | },
10 | {
11 | "to": "$.properties.products[1].category",
12 | "from": "$.properties.type"
13 | }
14 | ]
15 |
--------------------------------------------------------------------------------
/test/scenarios/mappings/root_array_mappings.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "input": "$[*].user_id",
4 | "output": "$[*].user.id"
5 | },
6 | {
7 | "input": "$[*].user_name",
8 | "output": "$[*].user.name"
9 | }
10 | ]
11 |
--------------------------------------------------------------------------------
/test/scenarios/mappings/root_context_vars_mapping.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "from": "$[*].#index",
4 | "to": "$[*].#index"
5 | },
6 | {
7 | "from": "$[*].foo",
8 | "to": "$[*].bar"
9 | }
10 | ]
11 |
--------------------------------------------------------------------------------
/test/scenarios/mappings/root_index_mappings.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "input": "$.id",
4 | "output": "$[0].user_id"
5 | },
6 | {
7 | "input": "$.name",
8 | "output": "$[0].user_name"
9 | }
10 | ]
11 |
--------------------------------------------------------------------------------
/test/scenarios/mappings/root_mappings.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "input": "$.context",
4 | "output": "$"
5 | },
6 | {
7 | "input": "$.events",
8 | "output": "$.event_names"
9 | },
10 | {
11 | "input": "$.details",
12 | "output": "$"
13 | }
14 | ]
15 |
--------------------------------------------------------------------------------
/test/scenarios/mappings/root_nested_mappings.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "input": "$[*].*",
4 | "output": "$[*].*.value"
5 | }
6 | ]
7 |
--------------------------------------------------------------------------------
/test/scenarios/mappings/root_object_mappings.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "input": "$.*",
4 | "output": "$.*.value"
5 | }
6 | ]
7 |
--------------------------------------------------------------------------------
/test/scenarios/mappings/simple_array_mappings.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "input": "$.user_id",
4 | "output": "$.users[*].id"
5 | },
6 | {
7 | "input": "$.user_name",
8 | "output": "$.users[*].name"
9 | }
10 | ]
11 |
--------------------------------------------------------------------------------
/test/scenarios/mappings/template.jt:
--------------------------------------------------------------------------------
1 | const temp = ~m[
2 | {
3 | input: '^.a[*].a',
4 | output: '^.b[*].b',
5 | }
6 | ];
7 |
8 | ~r temp.b.b.sum();
9 |
--------------------------------------------------------------------------------
/test/scenarios/mappings/transformations.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "input": "$.products[*].id",
4 | "output": "$.items[*].product_id"
5 | },
6 | {
7 | "input": "$.products[*].name",
8 | "output": "$.items[*].product_name"
9 | },
10 | {
11 | "input": "$.products[*].category",
12 | "output": "$.items[*].product_category"
13 | },
14 | {
15 | "input": "$.products[*].(@.price * @.quantity * (1 - $.discount / 100))",
16 | "output": "$.items[*].value"
17 | },
18 | {
19 | "input": "$.products[*].(@.price * @.quantity * (1 - $.discount / 100)).sum()",
20 | "output": "$.revenue"
21 | }
22 | ]
23 |
--------------------------------------------------------------------------------
/test/scenarios/math/data.ts:
--------------------------------------------------------------------------------
1 | import { Scenario } from '../../types';
2 |
3 | export const data: Scenario[] = [
4 | {
5 | output: [12, 8, 20, 5, 100, 0, 40, 2],
6 | },
7 | ];
8 |
--------------------------------------------------------------------------------
/test/scenarios/math/template.jt:
--------------------------------------------------------------------------------
1 | [10 + 2, 10 - 2, 10 * 2, 10 / 2, 10 ** 2, 10 % 2, 10 << 2, 10 >> 2];
2 |
--------------------------------------------------------------------------------
/test/scenarios/objects/context_props.jt:
--------------------------------------------------------------------------------
1 | {
2 | user: {
3 | props: .traits.({
4 | @e [e.key]: {
5 | value: e.value
6 | },
7 | someKey: {
8 | value: 'someValue'
9 | }
10 | }),
11 | events: .events.({
12 | @e [e.value]: e.key,
13 | someEventValue: 'someEventName'
14 | })
15 | }
16 | }
--------------------------------------------------------------------------------
/test/scenarios/objects/data.ts:
--------------------------------------------------------------------------------
1 | import { Scenario } from '../../types';
2 |
3 | export const data: Scenario[] = [
4 | {
5 | templatePath: 'context_props.jt',
6 | input: {
7 | traits: {
8 | name: 'John Doe',
9 | age: 30,
10 | },
11 | events: {
12 | foo: 'bar',
13 | something: 'something else',
14 | },
15 | },
16 | output: {
17 | user: {
18 | props: {
19 | name: {
20 | value: 'John Doe',
21 | },
22 | age: {
23 | value: 30,
24 | },
25 | someKey: {
26 | value: 'someValue',
27 | },
28 | },
29 | events: {
30 | bar: 'foo',
31 | 'something else': 'something',
32 | someEventValue: 'someEventName',
33 | },
34 | },
35 | },
36 | },
37 | {
38 | templatePath: 'invalid_context_prop.jt',
39 | error: 'Context prop should be used with a key expression',
40 | },
41 | {
42 | output: {
43 | a: 1,
44 | b: 2,
45 | d: 3,
46 | },
47 | },
48 | ];
49 |
--------------------------------------------------------------------------------
/test/scenarios/objects/invalid_context_prop.jt:
--------------------------------------------------------------------------------
1 | {
2 | user: {
3 | props: .traits.({
4 | @e key: {
5 | value: @bar
6 | }
7 | })
8 | }
9 | }
--------------------------------------------------------------------------------
/test/scenarios/objects/template.jt:
--------------------------------------------------------------------------------
1 | let c = "c key";
2 | let d = 3;
3 | let b = 2;
4 | let a = {
5 | "a b": 1,
6 | b,
7 | // [c] coverts to "c key"
8 | [c]: {
9 | // this coverts to d: 3
10 | d
11 | },
12 | };
13 | a.({
14 | a: ."a b",
15 | b: .b,
16 | ...(.'c key')
17 | })
--------------------------------------------------------------------------------
/test/scenarios/paths/block.jt:
--------------------------------------------------------------------------------
1 | [
2 | .({
3 | a: @.a + 1,
4 | b: @.b + 2
5 | }),
6 | .([.a + 1, .b + 2])
7 | ]
--------------------------------------------------------------------------------
/test/scenarios/paths/data.ts:
--------------------------------------------------------------------------------
1 | import { PathType } from '../../../src';
2 | import { Scenario } from '../../types';
3 |
4 | export const data: Scenario[] = [
5 | {
6 | templatePath: 'block.jt',
7 | input: {
8 | a: 1,
9 | b: 1,
10 | },
11 | output: [{ a: 2, b: 3 }, [2, 3]],
12 | },
13 | {
14 | templatePath: 'options.jt',
15 | options: {
16 | defaultPathType: PathType.RICH,
17 | },
18 | input: {
19 | a: {
20 | b: [{ c: [{ d: 1 }, { d: 2 }] }],
21 | },
22 | },
23 | output: [
24 | [
25 | {
26 | d: 1,
27 | },
28 | {
29 | d: 2,
30 | },
31 | ],
32 | [
33 | {
34 | d: 1,
35 | },
36 | {
37 | d: 2,
38 | },
39 | ],
40 | undefined,
41 | {
42 | d: 2,
43 | },
44 | ],
45 | },
46 | {
47 | templatePath: 'options.jt',
48 | options: {
49 | defaultPathType: PathType.SIMPLE,
50 | },
51 | input: {
52 | a: {
53 | b: [{ c: [{ d: 1 }, { d: 2 }] }],
54 | },
55 | },
56 | output: [
57 | undefined,
58 | [
59 | {
60 | d: 1,
61 | },
62 | {
63 | d: 2,
64 | },
65 | ],
66 | undefined,
67 | {
68 | d: 2,
69 | },
70 | ],
71 | },
72 | {
73 | templatePath: 'json_path.jt',
74 | input: {
75 | foo: 'bar',
76 | size: 1,
77 | items: [
78 | {
79 | a: 1,
80 | b: 1,
81 | },
82 | {
83 | a: 2,
84 | b: 2,
85 | },
86 | {
87 | a: 3,
88 | b: 3,
89 | },
90 | ],
91 | },
92 | output: [
93 | 'bar',
94 | [
95 | {
96 | a: 1,
97 | b: 1,
98 | },
99 | {
100 | a: 2,
101 | b: 2,
102 | },
103 | {
104 | a: 3,
105 | b: 3,
106 | },
107 | ],
108 | [
109 | {
110 | a: 2,
111 | b: 2,
112 | },
113 | {
114 | a: 3,
115 | b: 3,
116 | },
117 | ],
118 | [2, 4, 6],
119 | ],
120 | },
121 | {
122 | templatePath: 'options.jt',
123 | options: {
124 | defaultPathType: PathType.SIMPLE,
125 | },
126 | input: {
127 | a: {
128 | b: {
129 | c: [{ d: 1 }, { d: 2 }],
130 | },
131 | },
132 | },
133 | output: [
134 | [
135 | {
136 | d: 1,
137 | },
138 | {
139 | d: 2,
140 | },
141 | ],
142 | [
143 | {
144 | d: 1,
145 | },
146 | {
147 | d: 2,
148 | },
149 | ],
150 | [
151 | {
152 | d: 1,
153 | },
154 | {
155 | d: 2,
156 | },
157 | ],
158 | {
159 | d: 2,
160 | },
161 | ],
162 | },
163 | {
164 | templatePath: 'rich_path.jt',
165 | input: [
166 | {
167 | a: { e: 1 },
168 | b: [{ e: 2 }, { e: [3, 2] }],
169 | c: { c: { e: 4 } },
170 | d: [
171 | [1, 2],
172 | [3, 4],
173 | ],
174 | },
175 | ],
176 | output: [1, [4], 2],
177 | },
178 | {
179 | templatePath: 'simple_path.jt',
180 | input: [
181 | {
182 | a: { e: 1 },
183 | b: [{ e: 2 }, { e: [3, 2] }],
184 | c: { c: { e: 4 } },
185 | d: [
186 | [1, 2],
187 | [3, 4],
188 | ],
189 | },
190 | ],
191 | output: [1, 1, [4], 2, 1],
192 | },
193 | {
194 | input: [
195 | {
196 | a: { e: 1 },
197 | b: [{ e: 2 }, { e: [3, 2] }],
198 | c: { c: { e: 4 } },
199 | d: [
200 | [1, 2],
201 | [3, 4],
202 | ],
203 | },
204 | ],
205 | output: [
206 | [3],
207 | 'aa',
208 | {
209 | e: 1,
210 | },
211 | 4,
212 | 3,
213 | 4,
214 | 1,
215 | ],
216 | options: {
217 | defaultPathType: PathType.RICH,
218 | },
219 | },
220 | ];
221 |
--------------------------------------------------------------------------------
/test/scenarios/paths/json_path.jt:
--------------------------------------------------------------------------------
1 | [
2 | ~j $.foo,
3 | ~j $.items[*],
4 | ~j $.items[?(@.a>$.size)],
5 | ~j $.items.(@.a + @.b)
6 | ]
--------------------------------------------------------------------------------
/test/scenarios/paths/options.jt:
--------------------------------------------------------------------------------
1 | [
2 | // Based path options it will be treat as either simple or rich path
3 | .a.b.c,
4 | // this is always treated as rich path
5 | ~r .a.b.c,
6 | // this is always treated as simple path
7 | ~s .a.b.c,
8 | // this is always treated as rich path as it uses rich features like
9 | .a.b.c{.d > 1}
10 | ]
--------------------------------------------------------------------------------
/test/scenarios/paths/rich_path.jt:
--------------------------------------------------------------------------------
1 | [
2 | ~r .0.a."e",
3 | ~r .0.d.1.1[],
4 | ~r .[0].b.1.e.1
5 | ]
--------------------------------------------------------------------------------
/test/scenarios/paths/simple_path.jt:
--------------------------------------------------------------------------------
1 | [
2 | ~s ^.0.a."e",
3 | ~s ^.0.a["e"],
4 | ~s .0.d.1.1[],
5 | .[0].b.()~s .1.e.1,
6 | {a: {b : 1}}.a.b
7 | ]
--------------------------------------------------------------------------------
/test/scenarios/paths/template.jt:
--------------------------------------------------------------------------------
1 | [
2 | 3[],
3 | "aa"[0][][0][][0],
4 | ^.e ?? ^.a,
5 | ^.c.e ?? .c.c.e,
6 | .b.1.e.0 ?? .b.0.e,
7 | .d.1.1,
8 | ^[].length
9 | ]
--------------------------------------------------------------------------------
/test/scenarios/return/data.ts:
--------------------------------------------------------------------------------
1 | import { Scenario } from '../../types';
2 |
3 | export const data: Scenario[] = [
4 | {
5 | templatePath: 'return_without_condition.jt',
6 | error:
7 | 'return, throw, continue and break statements are only allowed as last statements in conditional expressions',
8 | },
9 | {
10 | templatePath: 'statement_after_return.jt',
11 | error:
12 | 'return, throw, continue and break statements are only allowed as last statements in conditional expressions',
13 | },
14 | {
15 | templatePath: 'return_no_value.jt',
16 | input: 2,
17 | },
18 | {
19 | templatePath: 'return_value.jt',
20 | input: 3,
21 | output: 1,
22 | },
23 | {
24 | templatePath: 'return_value.jt',
25 | input: 2,
26 | output: 1,
27 | },
28 | ];
29 |
--------------------------------------------------------------------------------
/test/scenarios/return/return_no_value.jt:
--------------------------------------------------------------------------------
1 | (. % 2 === 0) ? {
2 | return;
3 | }
4 | (. - 1)/2;
--------------------------------------------------------------------------------
/test/scenarios/return/return_value.jt:
--------------------------------------------------------------------------------
1 | (. % 2 === 0) ? {
2 | return ./2;
3 | }
4 | (. - 1)/2;
--------------------------------------------------------------------------------
/test/scenarios/return/return_without_condition.jt:
--------------------------------------------------------------------------------
1 | return;
--------------------------------------------------------------------------------
/test/scenarios/return/statement_after_return.jt:
--------------------------------------------------------------------------------
1 | .num > 1 ? {
2 | return;
3 | let count = 0;
4 | }
--------------------------------------------------------------------------------
/test/scenarios/selectors/data.ts:
--------------------------------------------------------------------------------
1 | import { Scenario } from '../../types';
2 |
3 | export const data: Scenario[] = [
4 | {
5 | input: {
6 | a: 10,
7 | b: 2,
8 | c: {
9 | d: 30,
10 | },
11 | },
12 | output: 50,
13 | },
14 | {
15 | templatePath: 'wild_cards.jt',
16 | input: {
17 | a: { d: 1 },
18 | b: [{ d: 2 }, { d: 3 }],
19 | c: { c: { d: 4 } },
20 | },
21 | output: [
22 | [1, 2, 3],
23 | [1, 2, 3, 4],
24 | ],
25 | },
26 | ];
27 |
--------------------------------------------------------------------------------
/test/scenarios/selectors/template.jt:
--------------------------------------------------------------------------------
1 | ..d[0] + .a * .b
--------------------------------------------------------------------------------
/test/scenarios/selectors/wild_cards.jt:
--------------------------------------------------------------------------------
1 | [.*.d, ..*.d]..
2 |
--------------------------------------------------------------------------------
/test/scenarios/standard_functions/data.ts:
--------------------------------------------------------------------------------
1 | import { Scenario } from '../../types';
2 |
3 | const input = {
4 | arr: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
5 | obj: {
6 | foo: 1,
7 | bar: 2,
8 | baz: 3,
9 | quux: 4,
10 | },
11 | };
12 |
13 | export const data: Scenario[] = [
14 | {
15 | template: '.arr.avg()',
16 | input,
17 | output: 5.5,
18 | },
19 | {
20 | template: '.arr.first()',
21 | input,
22 | output: 1,
23 | },
24 | {
25 | template: '.arr.index(0)',
26 | input,
27 | output: 1,
28 | },
29 | {
30 | template: '.arr.index(9)',
31 | input,
32 | output: 10,
33 | },
34 | {
35 | template: '.arr.last()',
36 | input,
37 | output: 10,
38 | },
39 | {
40 | template: '.arr.length()',
41 | input,
42 | output: 10,
43 | },
44 | {
45 | template: '.arr.min()',
46 | input,
47 | output: 1,
48 | },
49 | {
50 | template: '.arr.max()',
51 | input,
52 | output: 10,
53 | },
54 | {
55 | template: '.arr.stddev()',
56 | input,
57 | output: 2.8722813232690143,
58 | },
59 | {
60 | template: '.arr.sum()',
61 | input,
62 | output: 55,
63 | },
64 | {
65 | input,
66 | output: {
67 | sum: 55,
68 | sum2: 55,
69 | avg: 5.5,
70 | min: 1,
71 | max: 10,
72 | stddev: 2.8722813232690143,
73 | length: 10,
74 | first: 1,
75 | last: 10,
76 | keys: ['foo', 'bar', 'baz', 'quux'],
77 | },
78 | },
79 | ];
80 |
--------------------------------------------------------------------------------
/test/scenarios/standard_functions/template.jt:
--------------------------------------------------------------------------------
1 | const arr = .arr;
2 | const obj = .obj;
3 | const keys = obj.keys();
4 | {
5 | sum: .arr.sum(),
6 | sum2: (arr.index(0) + arr.index(-1)) * arr.length() / 2,
7 | avg: arr.avg(),
8 | min: arr.min(),
9 | max: arr.max(),
10 | stddev: arr.stddev(),
11 | length: arr.length(),
12 | first: arr.first(),
13 | last: arr.last(),
14 | keys,
15 | }
--------------------------------------------------------------------------------
/test/scenarios/statements/data.ts:
--------------------------------------------------------------------------------
1 | import { Scenario } from '../../types';
2 |
3 | export const data: Scenario[] = [
4 | {
5 | output: [15],
6 | },
7 | ];
8 |
--------------------------------------------------------------------------------
/test/scenarios/statements/template.jt:
--------------------------------------------------------------------------------
1 | // To statements in one line
2 | let a = 1; let b = 2
3 | // empty statements
4 | ;;;
5 |
6 | let c = [3]
7 | // block statement
8 | (
9 | let d = 4;
10 | let fn = function(a){
11 | 5*a
12 | }
13 | [a + b + c[0] + d + fn(1)]
14 | )
15 |
--------------------------------------------------------------------------------
/test/scenarios/template_strings/data.ts:
--------------------------------------------------------------------------------
1 | import { Scenario } from '../../types';
2 |
3 | export const data: Scenario[] = [
4 | {
5 | input: {
6 | a: 'foo',
7 | },
8 | bindings: {
9 | b: 'bar',
10 | },
11 | output: 'Input a=foo, Binding b=bar',
12 | },
13 | {
14 | template: '`unclosed template ${`',
15 | error: 'Invalid template expression',
16 | },
17 | {
18 | template: '`invalid template expression ${.a + }`',
19 | error: 'Invalid template expression',
20 | },
21 | ];
22 |
--------------------------------------------------------------------------------
/test/scenarios/template_strings/template.jt:
--------------------------------------------------------------------------------
1 | let a = `Input a=${.a}`;
2 | let b = `Binding b=${$.b}`;
3 | `${a}, ${b}`;
--------------------------------------------------------------------------------
/test/scenarios/throw/data.ts:
--------------------------------------------------------------------------------
1 | import { Scenario } from '../../types';
2 |
3 | export const data: Scenario[] = [
4 | {
5 | templatePath: 'statement_after_throw.jt',
6 | error:
7 | 'return, throw, continue and break statements are only allowed as last statements in conditional expressions',
8 | },
9 | {
10 | input: 3,
11 | error: 'num must be even',
12 | },
13 | {
14 | input: 2,
15 | output: 1,
16 | },
17 | {
18 | templatePath: 'throw_without_condition.jt',
19 | error:
20 | 'return, throw, continue and break statements are only allowed as last statements in conditional expressions',
21 | },
22 | ];
23 |
--------------------------------------------------------------------------------
/test/scenarios/throw/statement_after_throw.jt:
--------------------------------------------------------------------------------
1 | .num % 2 !== 0 ? {
2 | throw new Error("num must be even");
3 | let count = 0;
4 | }
--------------------------------------------------------------------------------
/test/scenarios/throw/template.jt:
--------------------------------------------------------------------------------
1 | (. % 2 !== 0) ? throw new Error("num must be even") : ./2;
--------------------------------------------------------------------------------
/test/scenarios/throw/throw_without_condition.jt:
--------------------------------------------------------------------------------
1 | throw new Error("num must be even");
--------------------------------------------------------------------------------
/test/types.ts:
--------------------------------------------------------------------------------
1 | import type { EngineOptions, FlatMappingPaths } from '../src';
2 |
3 | export type Scenario = {
4 | description?: string;
5 | input?: unknown;
6 | templatePath?: string;
7 | mappings?: FlatMappingPaths[];
8 | mappingsPath?: string;
9 | template?: string;
10 | options?: EngineOptions;
11 | bindings?: Record | undefined;
12 | output?: unknown;
13 | error?: string;
14 | };
15 |
16 | export namespace Scenario {
17 | export function getTemplatePath(scenario: Scenario): string {
18 | return scenario.templatePath || scenario.mappingsPath || scenario.template || 'template.jt';
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/test/utils/index.ts:
--------------------------------------------------------------------------------
1 | export * from './scenario';
2 |
--------------------------------------------------------------------------------
/test/utils/scenario.ts:
--------------------------------------------------------------------------------
1 | import { readFileSync } from 'node:fs';
2 | import { join } from 'node:path';
3 | import { FlatMappingPaths, JsonTemplateEngine, PathType } from '../../src';
4 | import { Scenario } from '../types';
5 |
6 | function getTemplate(scenarioDir: string, scenario: Scenario): string {
7 | const templatePath = join(scenarioDir, Scenario.getTemplatePath(scenario));
8 | return readFileSync(templatePath, 'utf-8');
9 | }
10 |
11 | function getDefaultPathType(scenario: Scenario): PathType {
12 | return scenario.mappingsPath || scenario.mappings ? PathType.JSON : PathType.SIMPLE;
13 | }
14 |
15 | function initializeScenario(scenarioDir: string, scenario: Scenario) {
16 | scenario.options = scenario.options || {};
17 | scenario.options.defaultPathType =
18 | scenario.options.defaultPathType || getDefaultPathType(scenario);
19 |
20 | if (scenario.mappingsPath) {
21 | scenario.mappings = JSON.parse(getTemplate(scenarioDir, scenario)) as FlatMappingPaths[];
22 | }
23 | if (scenario.mappings) {
24 | scenario.template = JsonTemplateEngine.convertMappingsToTemplate(
25 | scenario.mappings as FlatMappingPaths[],
26 | scenario.options,
27 | );
28 | }
29 | if (scenario.template === undefined) {
30 | scenario.template = getTemplate(scenarioDir, scenario);
31 | }
32 | scenario.template = JsonTemplateEngine.reverseTranslate(
33 | JsonTemplateEngine.parse(scenario.template, scenario.options),
34 | scenario.options,
35 | );
36 | }
37 |
38 | export function evaluateScenario(scenarioDir: string, scenario: Scenario): any {
39 | initializeScenario(scenarioDir, scenario);
40 | if (scenario.mappingsPath) {
41 | return JsonTemplateEngine.evaluateAsSync(
42 | scenario.template,
43 | scenario.options,
44 | scenario.input,
45 | scenario.bindings,
46 | );
47 | }
48 | return JsonTemplateEngine.evaluate(
49 | scenario.template,
50 | scenario.options,
51 | scenario.input,
52 | scenario.bindings,
53 | );
54 | }
55 |
56 | export function extractScenarios(scenarioDir: string): Scenario[] {
57 | const { data } = require(join(scenarioDir, 'data.ts'));
58 | return data as Scenario[];
59 | }
60 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Visit https://aka.ms/tsconfig to read more about this file */
4 |
5 | /* Projects */
6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
12 |
13 | /* Language and Environment */
14 | "target": "es2021" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
16 | // "jsx": "preserve", /* Specify what JSX code is generated. */
17 | "experimentalDecorators": true /* Enable experimental support for TC39 stage 2 draft decorators. */,
18 | "emitDecoratorMetadata": true /* Emit design-type metadata for decorated declarations in source files. */,
19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
26 |
27 | /* Modules */
28 | "module": "commonjs" /* Specify what module code is generated. */,
29 | "rootDir": "./src" /* Specify the root folder within your source files. */,
30 | "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */,
31 | "baseUrl": "./" /* Specify the base directory to resolve non-relative module names. */,
32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
33 | // "rootDirs": [
34 | // "./src",
35 | // "./test"
36 | // ] /* Allow multiple folders to be treated as one when resolving modules. */,
37 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
38 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */
39 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
40 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
41 | "resolveJsonModule": true /* Enable importing .json files. */,
42 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */
43 |
44 | /* JavaScript Support */
45 | "allowJs": true /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */,
46 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
47 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
48 |
49 | /* Emit */
50 | "declaration": true /* Generate .d.ts files from TypeScript and JavaScript files in your project. */,
51 | "declarationMap": true /* Create sourcemaps for d.ts files. */,
52 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
53 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */
54 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
55 | "outDir": "./build" /* Specify an output folder for all emitted files. */,
56 | // "removeComments": true, /* Disable emitting comments. */
57 | // "noEmit": true, /* Disable emitting files from a compilation. */
58 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
59 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
60 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
61 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
62 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
63 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
64 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
65 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
66 | // "newLine": "crlf", /* Set the newline character for emitting files. */
67 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
68 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
69 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
70 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
71 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */
72 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
73 |
74 | /* Interop Constraints */
75 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
76 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
77 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,
78 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
79 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
80 |
81 | /* Type Checking */
82 | "strict": true /* Enable all strict type-checking options. */,
83 | "noImplicitAny": false /* Enable error reporting for expressions and declarations with an implied 'any' type. */,
84 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
85 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
86 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
87 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
88 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
89 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
90 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
91 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
92 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
93 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
94 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
95 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
96 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
97 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
98 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
99 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
100 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
101 |
102 | /* Completeness */
103 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
104 | "skipLibCheck": true /* Skip type checking all .d.ts files. */
105 | },
106 | "exclude": ["node_modules", "**/test/*"],
107 | "include": ["src/**/*.ts", "src/**/*.json"]
108 | }
109 |
--------------------------------------------------------------------------------
/vite.config.mts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite';
2 | import { resolve } from 'path';
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | build: {
7 | lib: {
8 | entry: resolve(__dirname, 'src/index.ts'),
9 | formats: ['es'],
10 | fileName: () => 'json-template.js',
11 | },
12 | outDir: 'build',
13 | },
14 | resolve: { alias: { src: resolve('src/') } },
15 | });
16 |
--------------------------------------------------------------------------------