├── .code-samples.meilisearch.yaml ├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ └── feature_request.md ├── dependabot.yml ├── release-draft-template.yml ├── scripts │ └── check-release.sh └── workflows │ ├── pre-release-tests.yml │ ├── publish.yml │ ├── release-drafter.yml │ └── tests.yml ├── .gitignore ├── .pubignore ├── .yamllint.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── bors.toml ├── docker-compose.yml ├── example ├── lib │ └── main.dart └── pubspec.yaml ├── lib ├── meilisearch.dart └── src │ ├── annotations.dart │ ├── client.dart │ ├── exception.dart │ ├── facade.dart │ ├── filter_builder │ ├── _exports.dart │ ├── attribute.dart │ ├── filter_builder_base.dart │ ├── operators.dart │ └── values.dart │ ├── http_request.dart │ ├── index.dart │ ├── query_parameters │ ├── _exports.dart │ ├── cancel_tasks_query.dart │ ├── delete_documents_query.dart │ ├── delete_tasks_query.dart │ ├── documents_query.dart │ ├── facet_search_query.dart │ ├── hybrid_search.dart │ ├── index_search_query.dart │ ├── indexes_query.dart │ ├── keys_query.dart │ ├── multi_search_query.dart │ ├── queryable.dart │ ├── search_query.dart │ ├── swap_index.dart │ └── tasks_query.dart │ ├── results │ ├── _exports.dart │ ├── all_stats.dart │ ├── document_container.dart │ ├── experimental_features.dart │ ├── experimental_features.g.dart │ ├── facet_hit.dart │ ├── facet_search_result.dart │ ├── facet_stat.dart │ ├── index_stats.dart │ ├── key.dart │ ├── match_position.dart │ ├── matching_strategy_enum.dart │ ├── multi_search_result.dart │ ├── paginated_search_result.dart │ ├── ranking_rules │ │ ├── _exports.dart │ │ ├── attribute.dart │ │ ├── base.dart │ │ ├── exactness.dart │ │ ├── proximity.dart │ │ ├── typo.dart │ │ └── words.dart │ ├── result.dart │ ├── search_result.dart │ ├── searchable.dart │ ├── searchable_helpers.dart │ ├── task.dart │ ├── task_error.dart │ └── tasks_results.dart │ ├── settings │ ├── _exports.dart │ ├── distribution.dart │ ├── embedder.dart │ ├── faceting.dart │ ├── index_settings.dart │ ├── min_word_size_for_typos.dart │ ├── pagination.dart │ └── typo_tolerance.dart │ ├── tenant_token.dart │ ├── tenant_token │ ├── exceptions.dart │ └── generator.dart │ └── version.dart ├── pubspec.yaml ├── screenshots └── logo.png ├── test ├── analytics_test.dart ├── code_samples.dart ├── custom_dio_test.dart ├── documents_test.dart ├── dump_test.dart ├── exceptions_test.dart ├── filter_builder_test.dart ├── get_client_stats_test.dart ├── get_keys_test.dart ├── get_version_test.dart ├── health_test.dart ├── indexes_test.dart ├── models │ ├── adapter.dart │ ├── adapter_browser.dart │ ├── adapter_io.dart │ └── test_client.dart ├── multi_index_search_test.dart ├── queryable_test.dart ├── search_test.dart ├── settings_test.dart ├── swaps_test.dart ├── tasks_test.dart ├── tenant_token_test.dart └── utils │ ├── books.dart │ ├── books_data.dart │ ├── client.dart │ └── wait_for.dart └── tool ├── .gitignore ├── analysis_options.yaml ├── bin └── meili.dart ├── lib └── src │ ├── command_base.dart │ ├── core.dart │ ├── main.dart │ ├── output_utils.dart │ ├── result.dart │ └── update_samples_command.dart └── pubspec.yaml /.editorconfig: -------------------------------------------------------------------------------- 1 | # https://editorconfig.org/ 2 | 3 | root = true 4 | 5 | [*] 6 | trim_trailing_whitespace = true 7 | insert_final_newline = true 8 | end_of_line = lf 9 | charset = utf-8 10 | indent_size = 2 11 | indent_style = space 12 | 13 | [*.dart] 14 | indent_size = 2 15 | indent_style = space 16 | 17 | [*.{yaml,yml}] 18 | indent_size = 2 19 | indent_style = space 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 🐞 3 | about: Create a report to help us improve. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | 10 | 11 | **Description** 12 | Description of what the bug is about. 13 | 14 | **Expected behavior** 15 | What you expected to happen. 16 | 17 | **Current behavior** 18 | What happened. 19 | 20 | **Screenshots or Logs** 21 | If applicable, add screenshots or logs to help explain your problem. 22 | 23 | **Environment (please complete the following information):** 24 | - OS: [e.g. Debian GNU/Linux] 25 | - Meilisearch version: [e.g. v.0.20.0] 26 | - meilisearch-dart version: [e.g v0.2.1] 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Support questions & other 4 | url: https://discord.meilisearch.com/ 5 | about: Support is not handled here but on our Discord 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request & Enhancement 💡 3 | about: Suggest a new idea for the project. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | 10 | 11 | **Description** 12 | Brief explanation of the feature. 13 | 14 | **Basic example** 15 | If the proposal involves something new or a change, include a basic example. How would you use the feature? In which context? 16 | 17 | **Other** 18 | Any other things you want to add. 19 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | enable-beta-ecosystems: true 3 | updates: 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "monthly" 8 | labels: 9 | - 'skip-changelog' 10 | - 'dependencies' 11 | rebase-strategy: disabled 12 | 13 | - package-ecosystem: "pub" 14 | directory: "/" 15 | schedule: 16 | interval: "monthly" 17 | -------------------------------------------------------------------------------- /.github/release-draft-template.yml: -------------------------------------------------------------------------------- 1 | name-template: 'v$RESOLVED_VERSION 🎯' 2 | tag-template: 'v$RESOLVED_VERSION' 3 | exclude-labels: 4 | - 'skip-changelog' 5 | version-resolver: 6 | minor: 7 | labels: 8 | - 'breaking-change' 9 | default: patch 10 | categories: 11 | - title: '⚠️ Breaking changes' 12 | label: 'breaking-change' 13 | - title: '🚀 Enhancements' 14 | label: 'enhancement' 15 | - title: '🐛 Bug Fixes' 16 | label: 'bug' 17 | - title: '🔒 Security' 18 | label: 'security' 19 | - title: '⚙️ Maintenance/misc' 20 | label: 21 | - 'maintenance' 22 | - 'documentation' 23 | template: | 24 | $CHANGES 25 | 26 | Thanks again to $CONTRIBUTORS! 🎉 27 | no-changes-template: 'Changes are coming soon 😎' 28 | sort-direction: 'ascending' 29 | replacers: 30 | - search: '/(?:and )?@dependabot-preview(?:\[bot\])?,?/g' 31 | replace: '' 32 | - search: '/(?:and )?@bors(?:\[bot\])?,?/g' 33 | replace: '' 34 | - search: '/(?:and )?@meili-bot,?/g' 35 | replace: '' 36 | -------------------------------------------------------------------------------- /.github/scripts/check-release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Checking if current tag matches the package version 4 | current_tag=$(echo $GITHUB_REF | cut -d '/' -f 3 | sed -r 's/^v//') 5 | 6 | file1='pubspec.yaml' 7 | file2='README.md' 8 | file3='lib/src/version.dart' 9 | changelog_file='CHANGELOG.md' 10 | ret=0 11 | 12 | file_tag1=$(grep '^version: ' $file1 | cut -d ':' -f 2 | tr -d ' ') 13 | file_tag2=$(grep 'meilisearch: ' $file2 | cut -d '^' -f2) 14 | file_tag3=$(grep -o "[0-9\.]" $file3 | tr -d '[:space:]') 15 | if [ "$current_tag" != "$file_tag1" ] || [ "$current_tag" != "$file_tag2" ] || [ "$current_tag" != "$file_tag3" ]; then 16 | echo "Error: the current tag does not match the version in package file(s)." 17 | echo "$file1: found $file_tag1 - expected $current_tag" 18 | echo "$file2: found $file_tag2 - expected $current_tag" 19 | echo "$file3: found $file_tag3 - expected $current_tag" 20 | ret=1 21 | fi 22 | 23 | # Checking the CHANGELOG file was updated 24 | grep -q "$current_tag" "$changelog_file" 25 | 26 | if [ "$?" -ne 0 ]; then 27 | echo "There is no description of the $current_tag release in $changelog_file" 28 | ret=1 29 | fi 30 | 31 | # Return 32 | if [ "$ret" -eq 0 ]; then 33 | echo 'OK' 34 | exit 0 35 | fi 36 | 37 | exit 1 38 | -------------------------------------------------------------------------------- /.github/workflows/pre-release-tests.yml: -------------------------------------------------------------------------------- 1 | # Testing the code base against the Meilisearch pre-releases 2 | name: Pre-Release Tests 3 | 4 | # Will only run for PRs and pushes to bump-meilisearch-v* 5 | on: 6 | push: 7 | branches: 8 | - bump-meilisearch-v* 9 | pull_request: 10 | branches: 11 | - bump-meilisearch-v* 12 | 13 | jobs: 14 | integration-tests: 15 | timeout-minutes: 10 16 | runs-on: ubuntu-latest 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | version: ['latest'] 21 | name: integration-tests-against-rc (dart ${{ matrix.version }}) 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@v4 25 | - name: Get the latest Meilisearch RC 26 | run: echo "MEILISEARCH_VERSION=$(curl https://raw.githubusercontent.com/meilisearch/integration-guides/main/scripts/get-latest-meilisearch-rc.sh | bash)" >> $GITHUB_ENV 27 | - name: Meilisearch (${{ env.MEILISEARCH_VERSION }}) setup with Docker 28 | run: docker run -d -p 7700:7700 getmeili/meilisearch:${{ env.MEILISEARCH_VERSION }} meilisearch --master-key=masterKey --no-analytics 29 | - name: Run integration tests 30 | run: docker run --net="host" -v $PWD:/package -w /package dart:${{ matrix.version }} /bin/sh -c 'dart pub get && dart pub get -C tool && dart run test' 31 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to Pub.dev 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | 8 | jobs: 9 | check_release: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v4 14 | - name: Check release validity 15 | run: sh .github/scripts/check-release.sh 16 | 17 | publishing: 18 | needs: check_release 19 | permissions: 20 | id-token: write # Required for authentication using OIDC 21 | uses: dart-lang/setup-dart/.github/workflows/publish.yml@v1 22 | -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name: Release Drafter 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | update_release_draft: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: release-drafter/release-drafter@v6 14 | with: 15 | config-name: release-draft-template.yml 16 | env: 17 | GITHUB_TOKEN: ${{ secrets.RELEASE_DRAFTER_TOKEN }} 18 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | pull_request: 5 | push: 6 | # trying and staging branches are for BORS config 7 | branches: 8 | - trying 9 | - staging 10 | - main 11 | 12 | env: 13 | MEILISEARCH_URL: http://localhost:7700 14 | 15 | jobs: 16 | integration-tests: 17 | # Will not run if the event is a PR to bump-meilisearch-v* (so a pre-release PR) 18 | # Will still run for each push to bump-meilisearch-v* 19 | if: github.event_name != 'pull_request' || !startsWith(github.base_ref, 'bump-meilisearch-v') 20 | timeout-minutes: 10 21 | runs-on: ubuntu-latest 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | version: ['latest'] 26 | name: integration-tests (dart ${{ matrix.version }}) 27 | services: 28 | meilisearch: 29 | image: getmeili/meilisearch:latest 30 | env: 31 | MEILI_MASTER_KEY: "masterKey" 32 | MEILI_NO_ANALYTICS: "true" 33 | ports: 34 | - 7700:7700 35 | 36 | steps: 37 | - uses: actions/checkout@v4 38 | - uses: dart-lang/setup-dart@v1 39 | with: 40 | sdk: ${{ matrix.version }} 41 | - name: Install dependencies 42 | run: | 43 | dart pub get 44 | dart pub get -C tool 45 | dart pub global activate coverage 46 | - name: Run integration tests 47 | run: dart test --concurrency=4 --reporter=github --coverage=./coverage/reports 48 | - name: Generate coverage reports 49 | run: dart pub global run coverage:format_coverage --report-on=./lib --lcov --in=./coverage/reports --out=coverage/lcov.info 50 | - name: Report to Codecov 51 | uses: codecov/codecov-action@v4 52 | with: 53 | file: coverage/lcov.info 54 | fail_ci_if_error: false 55 | 56 | 57 | linter: 58 | name: linter-check 59 | runs-on: ubuntu-latest 60 | container: 61 | image: dart:latest 62 | steps: 63 | - uses: actions/checkout@v4 64 | - name: Install dependencies 65 | run: | 66 | dart pub get 67 | dart pub get -C tool 68 | - name: Run linter 69 | run: dart analyze --fatal-infos && dart format . --set-exit-if-changed 70 | 71 | yaml-lint: 72 | name: Yaml linting check 73 | runs-on: ubuntu-latest 74 | steps: 75 | - uses: actions/checkout@v4 76 | - name: Yaml lint check 77 | uses: ibiqlik/action-yamllint@v3 78 | with: 79 | config_file: .yamllint.yml 80 | 81 | check-code-samples: 82 | name: check .code-samples.meilisearch.yaml 83 | runs-on: ubuntu-latest 84 | steps: 85 | - uses: actions/checkout@v4 86 | - uses: dart-lang/setup-dart@v1 87 | with: 88 | sdk: 'latest' 89 | - name: check if samples changed 90 | run: | 91 | dart pub get 92 | dart pub get -C tool 93 | dart run ./tool/bin/meili.dart update-samples --fail-on-change 94 | pana: 95 | runs-on: ubuntu-latest 96 | timeout-minutes: 10 97 | steps: 98 | - uses: actions/checkout@v4 99 | - uses: dart-lang/setup-dart@v1 100 | with: 101 | sdk: '3.0.0' 102 | - run: dart pub global activate pana 103 | - name: Run pana 104 | id: pana-run 105 | run: | 106 | echo "PANA_OUTPUT<> $GITHUB_ENV 107 | dart pub global run pana --no-warning --exit-code-threshold 130 >> $GITHUB_ENV 108 | echo "EOF" >> $GITHUB_ENV 109 | - name: Analyze pana output 110 | run: | 111 | echo "$PANA_OUTPUT" | grep "Supports 6 of 6 possible platforms" 112 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | .vscode/ 12 | pubspec.lock 13 | example/pubspec.lock 14 | 15 | # IntelliJ related 16 | *.iml 17 | *.ipr 18 | *.iws 19 | .idea/ 20 | 21 | # The .vscode folder contains launch configuration and tasks you configure in 22 | # VS Code which you may wish to be included in version control, so this line 23 | # is commented out by default. 24 | #.vscode/ 25 | 26 | # Flutter/Dart/Pub related 27 | **/doc/api/ 28 | .dart_tool/ 29 | .flutter-plugins 30 | .flutter-plugins-dependencies 31 | .packages 32 | .pub-cache/ 33 | .pub/ 34 | build/ 35 | 36 | # Android related 37 | **/android/**/gradle-wrapper.jar 38 | **/android/.gradle 39 | **/android/captures/ 40 | **/android/gradlew 41 | **/android/gradlew.bat 42 | **/android/local.properties 43 | **/android/**/GeneratedPluginRegistrant.java 44 | 45 | # iOS/XCode related 46 | **/ios/**/*.mode1v3 47 | **/ios/**/*.mode2v3 48 | **/ios/**/*.moved-aside 49 | **/ios/**/*.pbxuser 50 | **/ios/**/*.perspectivev3 51 | **/ios/**/*sync/ 52 | **/ios/**/.sconsign.dblite 53 | **/ios/**/.tags* 54 | **/ios/**/.vagrant/ 55 | **/ios/**/DerivedData/ 56 | **/ios/**/Icon? 57 | **/ios/**/Pods/ 58 | **/ios/**/.symlinks/ 59 | **/ios/**/profile 60 | **/ios/**/xcuserdata 61 | **/ios/.generated/ 62 | **/ios/Flutter/App.framework 63 | **/ios/Flutter/Flutter.framework 64 | **/ios/Flutter/Flutter.podspec 65 | **/ios/Flutter/Generated.xcconfig 66 | **/ios/Flutter/app.flx 67 | **/ios/Flutter/app.zip 68 | **/ios/Flutter/flutter_assets/ 69 | **/ios/Flutter/flutter_export_environment.sh 70 | **/ios/ServiceDefinitions.json 71 | **/ios/Runner/GeneratedPluginRegistrant.* 72 | 73 | # Exceptions to above rules. 74 | !**/ios/**/default.mode1v3 75 | !**/ios/**/default.mode2v3 76 | !**/ios/**/default.pbxuser 77 | !**/ios/**/default.perspectivev3 78 | -------------------------------------------------------------------------------- /.pubignore: -------------------------------------------------------------------------------- 1 | tool 2 | -------------------------------------------------------------------------------- /.yamllint.yml: -------------------------------------------------------------------------------- 1 | extends: default 2 | ignore: | 3 | node_modules 4 | rules: 5 | comments-indentation: disable 6 | line-length: disable 7 | document-start: disable 8 | brackets: disable 9 | truthy: disable 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-2025 Meili SAS 4 | Copyright (c) 2020 Misir Jafarov 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:lints/recommended.yaml 2 | analyzer: 3 | language: 4 | strict-casts: true 5 | strict-inference: true 6 | strict-raw-types: true 7 | -------------------------------------------------------------------------------- /bors.toml: -------------------------------------------------------------------------------- 1 | status = [ 2 | 'integration-tests (dart latest)', 3 | 'linter-check', 4 | 'pana' 5 | ] 6 | # 1 hour timeout 7 | timeout-sec = 3600 8 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | 3 | volumes: 4 | pub: 5 | 6 | services: 7 | package: 8 | image: dart:latest 9 | tty: true 10 | stdin_open: true 11 | working_dir: /home/package 12 | environment: 13 | - MEILISEARCH_URL=http://meilisearch:7700 14 | - PUB_CACHE=/vendor/pub-cache 15 | depends_on: 16 | - meilisearch 17 | links: 18 | - meilisearch 19 | volumes: 20 | - pub:/vendor/pub-cache 21 | - ./:/home/package 22 | 23 | meilisearch: 24 | image: getmeili/meilisearch:latest 25 | ports: 26 | - "7700" 27 | environment: 28 | - MEILI_MASTER_KEY=masterKey 29 | - MEILI_NO_ANALYTICS=true 30 | -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:meilisearch/meilisearch.dart'; 2 | 3 | void main() async { 4 | var client = MeiliSearchClient('http://127.0.0.1:7700', 'masterKey'); 5 | 6 | // An index where books are stored. 7 | await client.createIndex('books'); 8 | var index = await client.getIndex('books'); 9 | 10 | var documents = [ 11 | {'book_id': 123, 'title': 'Pride and Prejudice'}, 12 | {'book_id': 456, 'title': 'Le Petit Prince'}, 13 | {'book_id': 1, 'title': 'Alice In Wonderland'}, 14 | {'book_id': 1344, 'title': 'The Hobbit'}, 15 | {'book_id': 4, 'title': 'Harry Potter and the Half-Blood Prince'}, 16 | {'book_id': 42, 'title': 'The Hitchhiker\'s Guide to the Galaxy'} 17 | ]; 18 | 19 | // Add documents into index we just created. 20 | await index.addDocuments(documents); 21 | 22 | // Search 23 | var result = await index.search('prience'); 24 | print(result.hits); 25 | } 26 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: example 2 | publish_to: "none" 3 | version: 0.1.0 4 | 5 | environment: 6 | sdk: ">=3.0.0 <4.0.0" 7 | 8 | dependencies: 9 | meilisearch: 10 | path: ../ 11 | -------------------------------------------------------------------------------- /lib/meilisearch.dart: -------------------------------------------------------------------------------- 1 | library meilisearch; 2 | 3 | export 'src/settings/_exports.dart'; 4 | export 'src/results/_exports.dart'; 5 | export 'src/query_parameters/_exports.dart'; 6 | export 'src/filter_builder/_exports.dart'; 7 | export 'src/client.dart'; 8 | export 'src/index.dart'; 9 | export 'src/exception.dart'; 10 | export 'src/facade.dart'; 11 | export 'src/settings/distribution.dart'; 12 | export 'src/settings/embedder.dart'; 13 | -------------------------------------------------------------------------------- /lib/src/annotations.dart: -------------------------------------------------------------------------------- 1 | class RequiredMeiliServerVersion { 2 | const RequiredMeiliServerVersion(this.version); 3 | 4 | final String version; 5 | } 6 | -------------------------------------------------------------------------------- /lib/src/client.dart: -------------------------------------------------------------------------------- 1 | import 'package:meilisearch/meilisearch.dart'; 2 | import 'package:dio/dio.dart'; 3 | import 'annotations.dart'; 4 | import 'tenant_token.dart'; 5 | import 'http_request.dart'; 6 | 7 | class MeiliSearchClient { 8 | MeiliSearchClient( 9 | this.serverUrl, [ 10 | this.apiKey, 11 | this.connectTimeout, 12 | HttpClientAdapter? adapter, 13 | List? interceptors, 14 | ]) : http = HttpRequest( 15 | serverUrl, 16 | apiKey, 17 | connectTimeout, 18 | adapter, 19 | interceptors, 20 | ); 21 | 22 | factory MeiliSearchClient.withCustomDio( 23 | String serverUrl, { 24 | String? apiKey, 25 | Duration? connectTimeout, 26 | HttpClientAdapter? adapter, 27 | List? interceptors, 28 | }) => 29 | MeiliSearchClient( 30 | serverUrl, 31 | apiKey, 32 | connectTimeout, 33 | adapter, 34 | interceptors, 35 | ); 36 | 37 | /// Meilisearch server URL. 38 | final String serverUrl; 39 | 40 | /// Master key for authenticating with meilisearch server. 41 | final String? apiKey; 42 | 43 | /// Timeout in milliseconds for opening a url. 44 | final Duration? connectTimeout; 45 | 46 | /// Http client instance. 47 | final HttpRequest http; 48 | 49 | /// Create an index object by given [uid]. 50 | MeiliSearchIndex index(String uid) { 51 | return MeiliSearchIndex(this, uid); 52 | } 53 | 54 | Future _update(Future>> future) async { 55 | final response = await future; 56 | 57 | return Task.fromMap(response.data!); 58 | } 59 | 60 | /// Create a new index by given [uid] and optional [primaryKey] parameter. 61 | /// Throws an error if index is already exists. 62 | Future createIndex(String uid, {String? primaryKey}) async { 63 | final data = { 64 | 'uid': uid, 65 | if (primaryKey != null) 'primaryKey': primaryKey, 66 | }; 67 | 68 | return await _update( 69 | http.postMethod>('/indexes', data: data)); 70 | } 71 | 72 | /// Find index by matching [uid]. Throws error if index is not exists. 73 | Future getIndex(String uid) async { 74 | final response = await _getIndex(uid); 75 | 76 | return MeiliSearchIndex.fromMap(this, response.data!); 77 | } 78 | 79 | /// Find index by matching [uid] and responds with raw information from API. 80 | /// Throws error if index does not exist. 81 | Future> getRawIndex(String uid) async { 82 | final response = await _getIndex(uid); 83 | 84 | return response.data!; 85 | } 86 | 87 | Future>> _getIndex(String uid) { 88 | return http.getMethod>('/indexes/$uid'); 89 | } 90 | 91 | /// Return list of all existing indexes. 92 | Future> getIndexes({IndexesQuery? params}) async { 93 | final response = await http.getMethod>( 94 | '/indexes', 95 | queryParameters: params?.toQuery(), 96 | ); 97 | 98 | return Result.fromMapWithType( 99 | response.data!, 100 | (item) => MeiliSearchIndex.fromMap(this, item), 101 | ); 102 | } 103 | 104 | /// Delete the index by matching [uid]. 105 | Future deleteIndex(String uid) async { 106 | final index = this.index(uid); 107 | 108 | return await index.delete(); 109 | } 110 | 111 | /// Update the primary Key of the index by matching [uid]. 112 | Future updateIndex(String uid, String primaryKey) async { 113 | final index = this.index(uid); 114 | 115 | return index.update(primaryKey: primaryKey); 116 | } 117 | 118 | /// Swap indexes 119 | Future swapIndexes(List param) async { 120 | var query = param.map((e) => e.toQuery()).toList(); 121 | 122 | final response = await http 123 | .postMethod>('/swap-indexes', data: query); 124 | 125 | return Task.fromMap(response.data!); 126 | } 127 | 128 | /// Return health of the Meilisearch server. 129 | /// Throws an error if containing details if Meilisearch can't process your request. 130 | Future> health() async { 131 | final response = await http.getMethod>('/health'); 132 | 133 | return response.data!; 134 | } 135 | 136 | /// Get health of the Meilisearch server. 137 | /// Return true or false. 138 | Future isHealthy() async { 139 | try { 140 | await health(); 141 | } on Exception catch (_) { 142 | return false; 143 | } 144 | return true; 145 | } 146 | 147 | /// Trigger a dump creation process. 148 | 149 | Future createDump() async { 150 | final response = await http.postMethod>('/dumps'); 151 | 152 | return Task.fromMap(response.data!); 153 | } 154 | 155 | /// Get the public and private keys. 156 | Future> getKeys({KeysQuery? params}) async { 157 | final response = await http.getMethod>('/keys', 158 | queryParameters: params?.toQuery()); 159 | 160 | return Result.fromMapWithType( 161 | response.data!, (model) => Key.fromJson(model)); 162 | } 163 | 164 | /// Get a specific key by key or uid. 165 | Future getKey(String keyOrUid) async { 166 | final response = 167 | await http.getMethod>('/keys/$keyOrUid'); 168 | 169 | return Key.fromJson(response.data!); 170 | } 171 | 172 | /// Get the Meilisearch version 173 | Future> getVersion() async { 174 | final response = await http.getMethod>('/version'); 175 | return response.data!.map((k, v) => MapEntry(k, v.toString())); 176 | } 177 | 178 | /// Get all index stats. 179 | Future getStats() async { 180 | final response = await http.getMethod>('/stats'); 181 | 182 | return AllStats.fromMap(response.data!); 183 | } 184 | 185 | /// Create a new key. 186 | Future createKey({ 187 | DateTime? expiresAt, 188 | String? description, 189 | String? uid, 190 | required List indexes, 191 | required List actions, 192 | }) async { 193 | final data = { 194 | if (uid != null) 'uid': uid, 195 | 'expiresAt': expiresAt?.toIso8601String().split('.').first, 196 | if (description != null) 'description': description, 197 | 'indexes': indexes, 198 | 'actions': actions, 199 | }; 200 | 201 | final response = 202 | await http.postMethod>('/keys', data: data); 203 | 204 | return Key.fromJson(response.data!); 205 | } 206 | 207 | /// Update a key. 208 | Future updateKey( 209 | String key, { 210 | String? name, 211 | String? description, 212 | }) async { 213 | final data = { 214 | if (description != null) 'description': description, 215 | if (name != null) 'name': name, 216 | }; 217 | 218 | final response = 219 | await http.patchMethod>('/keys/$key', data: data); 220 | 221 | return Key.fromJson(response.data!); 222 | } 223 | 224 | /// Delete a key 225 | Future deleteKey(String key) async { 226 | final response = await http.deleteMethod('/keys/$key'); 227 | 228 | return response.statusCode == 204; 229 | } 230 | 231 | /// Generates a tenant token. 232 | String generateTenantToken( 233 | String uid, 234 | Object? searchRules, { 235 | String? apiKey, 236 | DateTime? expiresAt, 237 | }) { 238 | return generateToken( 239 | uid, 240 | searchRules, 241 | apiKey ?? this.apiKey ?? '', 242 | expiresAt: expiresAt, 243 | ); 244 | } 245 | 246 | // 247 | // Tasks endpoints 248 | // 249 | 250 | /// Get a list of tasks from the client. 251 | Future getTasks({TasksQuery? params}) async { 252 | final response = await http.getMethod>( 253 | '/tasks', 254 | queryParameters: params?.toQuery(), 255 | ); 256 | 257 | return TasksResults.fromMap(response.data!); 258 | } 259 | 260 | /// Cancel tasks based on the input query params 261 | Future cancelTasks({CancelTasksQuery? params}) async { 262 | final response = await http.postMethod>( 263 | '/tasks/cancel', 264 | queryParameters: params?.toQuery(), 265 | ); 266 | 267 | return Task.fromMap(response.data!); 268 | } 269 | 270 | /// Delete old processed tasks based on the input query params 271 | Future deleteTasks({DeleteTasksQuery? params}) async { 272 | final response = await http.deleteMethod>('/tasks', 273 | queryParameters: params?.toQuery()); 274 | 275 | return Task.fromMap(response.data!); 276 | } 277 | 278 | /// Get a task from an index specified by uid with the specified uid. 279 | Future getTask(int uid) async { 280 | final response = 281 | await http.getMethod>(('/tasks/$uid')); 282 | 283 | return Task.fromMap(response.data!); 284 | } 285 | 286 | /// does a Multi-index search 287 | @RequiredMeiliServerVersion('1.1.0') 288 | Future multiSearch(MultiSearchQuery query) async { 289 | final response = await http.postMethod>( 290 | '/multi-search', 291 | data: query.toSparseMap(), 292 | ); 293 | 294 | return MultiSearchResult.fromMap(response.data!); 295 | } 296 | } 297 | -------------------------------------------------------------------------------- /lib/src/exception.dart: -------------------------------------------------------------------------------- 1 | class MeiliSearchApiException implements Exception { 2 | MeiliSearchApiException( 3 | this.message, { 4 | this.code, 5 | this.link, 6 | this.type, 7 | }); 8 | 9 | factory MeiliSearchApiException.fromHttpBody( 10 | String message, 11 | Object? httpBody, 12 | ) { 13 | if (httpBody != null && 14 | httpBody is Map && 15 | httpBody.containsKey('message') && 16 | httpBody.containsKey('code') && 17 | httpBody.containsKey('link') && 18 | httpBody.containsKey('type')) { 19 | return MeiliSearchApiException( 20 | httpBody['message'] as String? ?? "", 21 | code: httpBody['code'] as String?, 22 | link: httpBody['link'] as String?, 23 | type: httpBody['type'] as String?, 24 | ); 25 | } else { 26 | return MeiliSearchApiException(message); 27 | } 28 | } 29 | 30 | final String message; 31 | final String? code; 32 | final String? link; 33 | final String? type; 34 | 35 | @override 36 | String toString() { 37 | var output = 'MeiliSearchApiError - message: $message'; 38 | if (code != null && link != null && type != null) { 39 | output += ' - code: $code - type: $type - link: $link'; 40 | } 41 | return output; 42 | } 43 | } 44 | 45 | class CommunicationException implements Exception { 46 | CommunicationException(this.message); 47 | 48 | final String message; 49 | 50 | @override 51 | String toString() { 52 | return 'An error occurred while trying to connect to the Meilisearch instance: $message'; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /lib/src/facade.dart: -------------------------------------------------------------------------------- 1 | import 'annotations.dart'; 2 | import 'filter_builder/_exports.dart'; 3 | 4 | /// Convenience class to access all Meilisearch filter expressions 5 | 6 | class Meili { 7 | Meili._(); 8 | 9 | /// Creates an attribute from string 10 | static MeiliAttributeExpression attr(String path) => 11 | MeiliAttributeExpression(path); 12 | 13 | /// Creates an attribute from parts to represent object nesting 14 | /// 15 | /// `Meili.attrFromParts(['contact','phone'])` is equivalent to `Meili.attr('contact.phone')` 16 | static MeiliAttributeExpression attrFromParts(List parts) => 17 | MeiliAttributeExpression.fromParts(parts); 18 | 19 | /// Creates an `ATTR = VALUE` filter operator 20 | static MeiliEqualsOperatorExpression eq( 21 | MeiliAttributeExpression path, 22 | MeiliValueExpressionBase value, 23 | ) => 24 | MeiliEqualsOperatorExpression(property: path, value: value); 25 | 26 | static MeiliGreaterThanOperatorExpression gt( 27 | MeiliAttributeExpression path, 28 | MeiliValueExpressionBase value, 29 | ) => 30 | MeiliGreaterThanOperatorExpression(property: path, value: value); 31 | 32 | static MeiliGreaterThanEqualsOperatorExpression gte( 33 | MeiliAttributeExpression path, 34 | MeiliValueExpressionBase value, 35 | ) => 36 | MeiliGreaterThanEqualsOperatorExpression(property: path, value: value); 37 | 38 | static MeiliLessThanEqualsOperatorExpression lte( 39 | MeiliAttributeExpression path, 40 | MeiliValueExpressionBase value, 41 | ) => 42 | MeiliLessThanEqualsOperatorExpression(property: path, value: value); 43 | 44 | static MeiliLessThanOperatorExpression lt( 45 | MeiliAttributeExpression path, 46 | MeiliValueExpressionBase value, 47 | ) => 48 | MeiliLessThanOperatorExpression(property: path, value: value); 49 | 50 | static MeiliNotEqualsOperatorExpression notEq( 51 | MeiliAttributeExpression path, 52 | MeiliValueExpressionBase value, 53 | ) => 54 | MeiliNotEqualsOperatorExpression(property: path, value: value); 55 | 56 | static MeiliToOperatorExpression to( 57 | MeiliAttributeExpression attribute, 58 | MeiliValueExpressionBase min, 59 | MeiliValueExpressionBase max, 60 | ) => 61 | MeiliToOperatorExpression( 62 | min: min, 63 | max: max, 64 | attribute: attribute, 65 | ); 66 | 67 | static MeiliGeoRadiusOperatorExpression geoRadius( 68 | MeiliPoint point, 69 | double distanceInMeters, 70 | ) => 71 | MeiliGeoRadiusOperatorExpression( 72 | (lat: point.lat, lng: point.lng), 73 | distanceInMeters, 74 | ); 75 | 76 | @RequiredMeiliServerVersion('1.1.0') 77 | static MeiliGeoBoundingBoxOperatorExpression geoBoundingBox( 78 | MeiliPoint p1, 79 | MeiliPoint p2, 80 | ) => 81 | MeiliGeoBoundingBoxOperatorExpression(p1, p2); 82 | 83 | static MeiliExistsOperatorExpression exists( 84 | MeiliAttributeExpression attribute) => 85 | MeiliExistsOperatorExpression(attribute); 86 | static MeiliNotExistsOperatorExpression notExists( 87 | MeiliAttributeExpression attribute) => 88 | MeiliNotExistsOperatorExpression(attribute); 89 | 90 | @RequiredMeiliServerVersion('1.2.0') 91 | static MeiliIsNullOperatorExpression isNull( 92 | MeiliAttributeExpression attribute, 93 | ) => 94 | MeiliIsNullOperatorExpression(attribute); 95 | 96 | @RequiredMeiliServerVersion('1.2.0') 97 | static MeiliIsNotNullOperatorExpression isNotNull( 98 | MeiliAttributeExpression attribute, 99 | ) => 100 | MeiliIsNotNullOperatorExpression(attribute); 101 | 102 | @RequiredMeiliServerVersion('1.2.0') 103 | static MeiliIsEmptyOperatorExpression isEmpty( 104 | MeiliAttributeExpression attribute, 105 | ) => 106 | MeiliIsEmptyOperatorExpression(attribute); 107 | 108 | @RequiredMeiliServerVersion('1.2.0') 109 | static MeiliIsNotEmptyOperatorExpression isNotEmpty( 110 | MeiliAttributeExpression attribute, 111 | ) => 112 | MeiliIsNotEmptyOperatorExpression(attribute); 113 | 114 | static MeiliNotOperatorExpression not(MeiliOperatorExpressionBase operator) => 115 | MeiliNotOperatorExpression(operator); 116 | static MeiliInOperatorExpression $in(MeiliAttributeExpression attribute, 117 | List values) => 118 | MeiliInOperatorExpression( 119 | attribute: attribute, 120 | values: values, 121 | ); 122 | 123 | static List values(Iterable v) { 124 | return v.map(value).toList(); 125 | } 126 | 127 | static MeiliValueExpressionBase value(Object v) { 128 | if (v is String) { 129 | return MeiliStringValueExpression(v); 130 | } 131 | if (v is num) { 132 | return MeiliNumberValueExpression(v); 133 | } 134 | if (v is bool) { 135 | return MeiliBooleanValueExpression(v); 136 | } 137 | if (v is DateTime) { 138 | return MeiliDateTimeValueExpression(v); 139 | } 140 | // fallback to a string value 141 | return MeiliStringValueExpression(v.toString()); 142 | } 143 | 144 | static MeiliOrOperatorExpression or( 145 | List operands, 146 | ) => 147 | MeiliOrOperatorExpression.fromList(operands); 148 | 149 | static MeiliAndOperatorExpression and( 150 | List operands, 151 | ) => 152 | MeiliAndOperatorExpression.fromList(operands); 153 | 154 | static MeiliEmptyExpression empty() => MeiliEmptyExpression(); 155 | } 156 | 157 | extension MeiliFiltersOperatorsExt on MeiliOperatorExpressionBase { 158 | MeiliOrOperatorExpression or(MeiliOperatorExpressionBase other) => 159 | MeiliOrOperatorExpression(first: this, second: other); 160 | MeiliOrOperatorExpression orList(List others) => 161 | MeiliOrOperatorExpression.fromList([this, ...others]); 162 | 163 | MeiliAndOperatorExpression and(MeiliOperatorExpressionBase other) => 164 | MeiliAndOperatorExpression(first: this, second: other); 165 | MeiliAndOperatorExpression andList( 166 | List others) => 167 | MeiliAndOperatorExpression.fromList([this, ...others]); 168 | 169 | MeiliNotOperatorExpression not() => MeiliNotOperatorExpression(this); 170 | } 171 | 172 | extension MeiliAttributesExt on MeiliAttributeExpression { 173 | MeiliEqualsOperatorExpression eq(MeiliValueExpressionBase value) => 174 | Meili.eq(this, value); 175 | 176 | MeiliNotEqualsOperatorExpression notEq(MeiliValueExpressionBase value) => 177 | Meili.notEq(this, value); 178 | 179 | MeiliGreaterThanOperatorExpression gt(MeiliValueExpressionBase value) => 180 | Meili.gt(this, value); 181 | 182 | MeiliGreaterThanEqualsOperatorExpression gte( 183 | MeiliValueExpressionBase value, 184 | ) => 185 | Meili.gte(this, value); 186 | 187 | MeiliLessThanOperatorExpression lt(MeiliValueExpressionBase value) => 188 | Meili.lt(this, value); 189 | 190 | MeiliLessThanEqualsOperatorExpression lte(MeiliValueExpressionBase value) => 191 | Meili.lte(this, value); 192 | 193 | MeiliToOperatorExpression to( 194 | MeiliValueExpressionBase min, 195 | MeiliValueExpressionBase max, 196 | ) => 197 | Meili.to(this, min, max); 198 | 199 | MeiliExistsOperatorExpression exists() => Meili.exists(this); 200 | 201 | MeiliNotExistsOperatorExpression notExists() => Meili.notExists(this); 202 | 203 | @RequiredMeiliServerVersion('1.2.0') 204 | MeiliIsNullOperatorExpression isNull() => Meili.isNull(this); 205 | 206 | @RequiredMeiliServerVersion('1.2.0') 207 | MeiliIsNotNullOperatorExpression isNotNull() => Meili.isNotNull(this); 208 | 209 | @RequiredMeiliServerVersion('1.2.0') 210 | MeiliIsEmptyOperatorExpression isEmpty() => Meili.isEmpty(this); 211 | 212 | @RequiredMeiliServerVersion('1.2.0') 213 | MeiliIsNotEmptyOperatorExpression isNotEmpty() => Meili.isNotEmpty(this); 214 | 215 | MeiliInOperatorExpression $in(List values) => 216 | Meili.$in(this, values); 217 | } 218 | 219 | extension StrMeiliValueExt on String { 220 | MeiliStringValueExpression toMeiliValue() => MeiliStringValueExpression(this); 221 | MeiliAttributeExpression toMeiliAttribute() => MeiliAttributeExpression(this); 222 | } 223 | 224 | extension NumMeiliValueExt on num { 225 | MeiliNumberValueExpression toMeiliValue() => MeiliNumberValueExpression(this); 226 | } 227 | 228 | extension DateMeiliValueExt on DateTime { 229 | MeiliDateTimeValueExpression toMeiliValue() => 230 | MeiliDateTimeValueExpression(this); 231 | } 232 | 233 | extension BoolMeiliValueExt on bool { 234 | MeiliBooleanValueExpression toMeiliValue() => 235 | MeiliBooleanValueExpression(this); 236 | } 237 | -------------------------------------------------------------------------------- /lib/src/filter_builder/_exports.dart: -------------------------------------------------------------------------------- 1 | export 'filter_builder_base.dart'; 2 | export 'operators.dart'; 3 | export 'values.dart'; 4 | export 'attribute.dart'; 5 | -------------------------------------------------------------------------------- /lib/src/filter_builder/attribute.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:collection/collection.dart'; 4 | 5 | import 'filter_builder_base.dart'; 6 | import 'operators.dart'; 7 | 8 | const _eq = DeepCollectionEquality(); 9 | 10 | /// Represents an attribute path in a filter expression 11 | class MeiliAttributeExpression extends MeiliExpressionBase { 12 | final List parts; 13 | 14 | MeiliAttributeExpression(String path) 15 | : parts = _normalizeParts(path.split('.')); 16 | MeiliAttributeExpression.fromParts(List parts) 17 | : parts = _normalizeParts(parts); 18 | 19 | static List _normalizeParts(List parts) { 20 | return parts 21 | .map((e) => e.trim()) 22 | .where((element) => element.isNotEmpty) 23 | .toList(); 24 | } 25 | 26 | @override 27 | String transform() { 28 | return jsonEncode(parts.join('.')); 29 | } 30 | 31 | @override 32 | bool operator ==(Object other) { 33 | if (identical(this, other)) return true; 34 | 35 | return other is MeiliAttributeExpression && _eq.equals(other.parts, parts); 36 | } 37 | 38 | @override 39 | int get hashCode => _eq.hash(parts); 40 | 41 | MeiliLessThanOperatorExpression operator <(MeiliValueExpressionBase value) { 42 | return MeiliLessThanOperatorExpression(property: this, value: value); 43 | } 44 | 45 | MeiliGreaterThanOperatorExpression operator >( 46 | MeiliValueExpressionBase value) { 47 | return MeiliGreaterThanOperatorExpression(property: this, value: value); 48 | } 49 | 50 | MeiliGreaterThanEqualsOperatorExpression operator >=( 51 | MeiliValueExpressionBase value) { 52 | return MeiliGreaterThanEqualsOperatorExpression( 53 | property: this, value: value); 54 | } 55 | 56 | MeiliLessThanEqualsOperatorExpression operator <=( 57 | MeiliValueExpressionBase value) { 58 | return MeiliLessThanEqualsOperatorExpression(property: this, value: value); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /lib/src/filter_builder/filter_builder_base.dart: -------------------------------------------------------------------------------- 1 | import 'operators.dart'; 2 | 3 | /// Represents an arbitrary filter expression 4 | abstract class MeiliExpressionBase { 5 | const MeiliExpressionBase(); 6 | 7 | String transform(); 8 | 9 | @override 10 | String toString() => transform(); 11 | 12 | /// Fallback equality and hashing, override these when possible to optimize performance 13 | @override 14 | bool operator ==(Object other) { 15 | if (identical(this, other)) return true; 16 | 17 | return other is MeiliExpressionBase && other.transform() == transform(); 18 | } 19 | 20 | @override 21 | int get hashCode => transform().hashCode; 22 | } 23 | 24 | /// Represents a filter operator with either at least one operand 25 | abstract class MeiliOperatorExpressionBase extends MeiliExpressionBase { 26 | const MeiliOperatorExpressionBase(); 27 | 28 | MeiliAndOperatorExpression operator &(MeiliOperatorExpressionBase other) { 29 | return MeiliAndOperatorExpression(first: this, second: other); 30 | } 31 | 32 | MeiliOrOperatorExpression operator |(MeiliOperatorExpressionBase other) { 33 | return MeiliOrOperatorExpression(first: this, second: other); 34 | } 35 | 36 | MeiliNotOperatorExpression operator ~() { 37 | return MeiliNotOperatorExpression(this); 38 | } 39 | } 40 | 41 | /// Represents a value in a filter expression 42 | abstract class MeiliValueExpressionBase extends MeiliExpressionBase { 43 | const MeiliValueExpressionBase(); 44 | } 45 | -------------------------------------------------------------------------------- /lib/src/filter_builder/operators.dart: -------------------------------------------------------------------------------- 1 | import 'package:collection/collection.dart'; 2 | import 'package:meilisearch/meilisearch.dart'; 3 | 4 | import '../annotations.dart'; 5 | 6 | const _eqUnordered = DeepCollectionEquality.unordered(); 7 | 8 | typedef MeiliPoint = ({num lat, num lng}); 9 | 10 | /// Represents an empty filter 11 | /// 12 | /// works as a starting point for filter builders 13 | class MeiliEmptyExpression extends MeiliOperatorExpressionBase { 14 | const MeiliEmptyExpression(); 15 | 16 | @override 17 | String transform() => ""; 18 | } 19 | 20 | class MeiliAndOperatorExpression extends MeiliOperatorExpressionBase { 21 | final List operands; 22 | 23 | MeiliAndOperatorExpression({ 24 | required MeiliOperatorExpressionBase first, 25 | required MeiliOperatorExpressionBase second, 26 | }) : this.fromList([first, second]); 27 | 28 | const MeiliAndOperatorExpression.fromList(this.operands); 29 | 30 | @override 31 | String transform() { 32 | //space is mandatory 33 | final filteredOperands = operands 34 | .map((e) => e.transform()) 35 | .where((element) => element.isNotEmpty); 36 | if (filteredOperands.isEmpty) { 37 | return ''; 38 | } else if (filteredOperands.length == 1) { 39 | return filteredOperands.first; 40 | } else { 41 | return filteredOperands.map((e) => '($e)').join(" AND "); 42 | } 43 | } 44 | 45 | @override 46 | bool operator ==(Object other) { 47 | if (identical(this, other)) return true; 48 | 49 | return other is MeiliAndOperatorExpression && 50 | _eqUnordered.equals(other.operands, operands); 51 | } 52 | 53 | @override 54 | int get hashCode => Object.hash("AND", _eqUnordered.hash(operands)); 55 | } 56 | 57 | class MeiliOrOperatorExpression extends MeiliOperatorExpressionBase { 58 | final List operands; 59 | 60 | MeiliOrOperatorExpression({ 61 | required MeiliOperatorExpressionBase first, 62 | required MeiliOperatorExpressionBase second, 63 | }) : this.fromList([first, second]); 64 | 65 | const MeiliOrOperatorExpression.fromList(this.operands); 66 | 67 | @override 68 | String transform() { 69 | final filteredOperands = operands 70 | .map((e) => e.transform()) 71 | .where((element) => element.isNotEmpty); 72 | if (filteredOperands.isEmpty) { 73 | return ''; 74 | } else if (filteredOperands.length == 1) { 75 | return filteredOperands.first; 76 | } else { 77 | return filteredOperands.map((e) => '($e)').join(" OR "); 78 | } 79 | } 80 | 81 | @override 82 | bool operator ==(Object other) { 83 | if (identical(this, other)) return true; 84 | 85 | return other is MeiliOrOperatorExpression && 86 | _eqUnordered.equals(other.operands, operands); 87 | } 88 | 89 | @override 90 | int get hashCode => Object.hash("OR", _eqUnordered.hash(operands)); 91 | } 92 | 93 | class MeiliToOperatorExpression extends MeiliOperatorExpressionBase { 94 | final MeiliAttributeExpression attribute; 95 | final MeiliValueExpressionBase min; 96 | final MeiliValueExpressionBase max; 97 | 98 | const MeiliToOperatorExpression({ 99 | required this.min, 100 | required this.max, 101 | required this.attribute, 102 | }); 103 | 104 | @override 105 | String transform() { 106 | final attributeTransformed = attribute.transform(); 107 | return "$attributeTransformed ${min.transform()} TO $attributeTransformed ${max.transform()}"; 108 | } 109 | } 110 | 111 | class MeiliGeoRadiusOperatorExpression extends MeiliOperatorExpressionBase { 112 | final MeiliPoint point; 113 | final double distanceInMeters; 114 | 115 | const MeiliGeoRadiusOperatorExpression( 116 | this.point, 117 | this.distanceInMeters, 118 | ); 119 | 120 | @override 121 | String transform() { 122 | return '_geoRadius(${point.lat},${point.lng},$distanceInMeters)'; 123 | } 124 | } 125 | 126 | @RequiredMeiliServerVersion('1.1.0') 127 | class MeiliGeoBoundingBoxOperatorExpression 128 | extends MeiliOperatorExpressionBase { 129 | final MeiliPoint point1; 130 | final MeiliPoint point2; 131 | 132 | const MeiliGeoBoundingBoxOperatorExpression( 133 | this.point1, 134 | this.point2, 135 | ); 136 | 137 | @override 138 | String transform() { 139 | return '_geoBoundingBox([${point1.lat},${point1.lng}],[${point2.lat},${point2.lng}])'; 140 | } 141 | } 142 | 143 | class MeiliExistsOperatorExpression extends MeiliOperatorExpressionBase { 144 | final MeiliAttributeExpression attribute; 145 | 146 | const MeiliExistsOperatorExpression(this.attribute); 147 | 148 | @override 149 | String transform() { 150 | return "${attribute.transform()} EXISTS"; 151 | } 152 | } 153 | 154 | class MeiliNotExistsOperatorExpression extends MeiliOperatorExpressionBase { 155 | final MeiliAttributeExpression attribute; 156 | 157 | const MeiliNotExistsOperatorExpression(this.attribute); 158 | 159 | @override 160 | String transform() { 161 | return "${attribute.transform()} NOT EXISTS"; 162 | } 163 | } 164 | 165 | @RequiredMeiliServerVersion('1.2.0') 166 | class MeiliIsNullOperatorExpression extends MeiliOperatorExpressionBase { 167 | final MeiliAttributeExpression attribute; 168 | 169 | const MeiliIsNullOperatorExpression(this.attribute); 170 | 171 | @override 172 | String transform() { 173 | return "${attribute.transform()} IS NULL"; 174 | } 175 | } 176 | 177 | @RequiredMeiliServerVersion('1.2.0') 178 | class MeiliIsNotNullOperatorExpression extends MeiliOperatorExpressionBase { 179 | final MeiliAttributeExpression attribute; 180 | 181 | const MeiliIsNotNullOperatorExpression(this.attribute); 182 | 183 | @override 184 | String transform() { 185 | return "${attribute.transform()} IS NOT NULL"; 186 | } 187 | } 188 | 189 | @RequiredMeiliServerVersion('1.2.0') 190 | class MeiliIsEmptyOperatorExpression extends MeiliOperatorExpressionBase { 191 | final MeiliAttributeExpression attribute; 192 | 193 | const MeiliIsEmptyOperatorExpression(this.attribute); 194 | 195 | @override 196 | String transform() { 197 | return "${attribute.transform()} IS EMPTY"; 198 | } 199 | } 200 | 201 | @RequiredMeiliServerVersion('1.2.0') 202 | class MeiliIsNotEmptyOperatorExpression extends MeiliOperatorExpressionBase { 203 | final MeiliAttributeExpression attribute; 204 | 205 | const MeiliIsNotEmptyOperatorExpression(this.attribute); 206 | 207 | @override 208 | String transform() { 209 | return "${attribute.transform()} IS NOT EMPTY"; 210 | } 211 | } 212 | 213 | class MeiliNotOperatorExpression extends MeiliOperatorExpressionBase { 214 | final MeiliOperatorExpressionBase operator; 215 | 216 | const MeiliNotOperatorExpression(this.operator) 217 | : assert(operator is! MeiliEmptyExpression, 218 | "Cannot negate (NOT) an empty operator"); 219 | 220 | @override 221 | String transform() { 222 | return "NOT ${operator.transform()}"; 223 | } 224 | } 225 | 226 | class MeiliInOperatorExpression extends MeiliOperatorExpressionBase { 227 | final MeiliAttributeExpression attribute; 228 | final List values; 229 | 230 | const MeiliInOperatorExpression({ 231 | required this.attribute, 232 | required this.values, 233 | }); 234 | 235 | @override 236 | String transform() { 237 | //TODO(ahmednfwela): escape commas in values ? 238 | return "${attribute.transform()} IN [${values.map((e) => e.transform()).join(',')}]"; 239 | } 240 | } 241 | 242 | /// Represents an operator that has a value as an operand 243 | abstract class MeiliValueOperandOperatorExpressionBase 244 | extends MeiliOperatorExpressionBase { 245 | final MeiliAttributeExpression property; 246 | final MeiliValueExpressionBase value; 247 | 248 | const MeiliValueOperandOperatorExpressionBase({ 249 | required this.property, 250 | required this.value, 251 | }); 252 | 253 | String get operator; 254 | 255 | @override 256 | String transform() { 257 | return '${property.transform()} $operator ${value.transform()}'; 258 | } 259 | } 260 | 261 | class MeiliEqualsOperatorExpression 262 | extends MeiliValueOperandOperatorExpressionBase { 263 | const MeiliEqualsOperatorExpression({ 264 | required super.property, 265 | required super.value, 266 | }); 267 | 268 | @override 269 | final String operator = "="; 270 | } 271 | 272 | class MeiliNotEqualsOperatorExpression 273 | extends MeiliValueOperandOperatorExpressionBase { 274 | const MeiliNotEqualsOperatorExpression({ 275 | required super.property, 276 | required super.value, 277 | }); 278 | 279 | @override 280 | final String operator = "!="; 281 | } 282 | 283 | class MeiliGreaterThanOperatorExpression 284 | extends MeiliValueOperandOperatorExpressionBase { 285 | const MeiliGreaterThanOperatorExpression({ 286 | required super.property, 287 | required super.value, 288 | }); 289 | 290 | @override 291 | final String operator = ">"; 292 | } 293 | 294 | class MeiliGreaterThanEqualsOperatorExpression 295 | extends MeiliValueOperandOperatorExpressionBase { 296 | const MeiliGreaterThanEqualsOperatorExpression({ 297 | required super.property, 298 | required super.value, 299 | }); 300 | 301 | @override 302 | final String operator = ">="; 303 | } 304 | 305 | class MeiliLessThanOperatorExpression 306 | extends MeiliValueOperandOperatorExpressionBase { 307 | const MeiliLessThanOperatorExpression({ 308 | required super.property, 309 | required super.value, 310 | }); 311 | 312 | @override 313 | final String operator = "<"; 314 | } 315 | 316 | class MeiliLessThanEqualsOperatorExpression 317 | extends MeiliValueOperandOperatorExpressionBase { 318 | const MeiliLessThanEqualsOperatorExpression({ 319 | required super.property, 320 | required super.value, 321 | }); 322 | 323 | @override 324 | final String operator = "<="; 325 | } 326 | -------------------------------------------------------------------------------- /lib/src/filter_builder/values.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'filter_builder_base.dart'; 4 | 5 | class MeiliNumberValueExpression extends MeiliValueExpressionBase { 6 | final num value; 7 | 8 | const MeiliNumberValueExpression(this.value); 9 | 10 | @override 11 | String transform() => value.toString(); 12 | 13 | @override 14 | bool operator ==(Object other) { 15 | if (identical(this, other)) return true; 16 | 17 | return other is MeiliNumberValueExpression && other.value == value; 18 | } 19 | 20 | @override 21 | int get hashCode => value.hashCode; 22 | } 23 | 24 | class MeiliDateTimeValueExpression extends MeiliValueExpressionBase { 25 | final DateTime value; 26 | MeiliDateTimeValueExpression(this.value) 27 | : assert( 28 | value.isUtc, 29 | "DateTime passed to Meili must be in UTC to avoid inconsistency accross multiple devices", 30 | ); 31 | 32 | /// Unix epoch time is seconds since epoch 33 | @override 34 | String transform() => 35 | (value.millisecondsSinceEpoch / 1000).floor().toString(); 36 | 37 | @override 38 | bool operator ==(Object other) { 39 | if (identical(this, other)) return true; 40 | 41 | return other is MeiliDateTimeValueExpression && other.value == value; 42 | } 43 | 44 | @override 45 | int get hashCode => value.hashCode; 46 | } 47 | 48 | class MeiliBooleanValueExpression extends MeiliValueExpressionBase { 49 | final bool value; 50 | 51 | const MeiliBooleanValueExpression(this.value); 52 | 53 | @override 54 | String transform() { 55 | return value.toString(); 56 | } 57 | 58 | @override 59 | bool operator ==(Object other) { 60 | if (identical(this, other)) return true; 61 | 62 | return other is MeiliBooleanValueExpression && other.value == value; 63 | } 64 | 65 | @override 66 | int get hashCode => value.hashCode; 67 | } 68 | 69 | class MeiliStringValueExpression extends MeiliValueExpressionBase { 70 | final String value; 71 | 72 | const MeiliStringValueExpression(this.value); 73 | 74 | @override 75 | String transform() { 76 | return jsonEncode(value); 77 | } 78 | 79 | @override 80 | bool operator ==(Object other) { 81 | if (identical(this, other)) return true; 82 | 83 | return other is MeiliStringValueExpression && other.value == value; 84 | } 85 | 86 | @override 87 | int get hashCode => value.hashCode; 88 | } 89 | -------------------------------------------------------------------------------- /lib/src/http_request.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'version.dart'; 3 | 4 | import 'exception.dart'; 5 | 6 | const bool _kIsWeb = bool.fromEnvironment('dart.library.js_util'); 7 | 8 | class HttpRequest { 9 | HttpRequest( 10 | this.serverUrl, 11 | this.apiKey, [ 12 | this.connectTimeout, 13 | HttpClientAdapter? adapter, 14 | List? interceptors, 15 | ]) : dio = Dio( 16 | BaseOptions( 17 | baseUrl: serverUrl, 18 | headers: { 19 | if (apiKey != null) 'Authorization': 'Bearer $apiKey', 20 | 'X-Meilisearch-Client': [ 21 | Version.qualifiedVersion, 22 | if (_kIsWeb) Version.qualifiedVersionWeb 23 | ].join(',') 24 | }, 25 | responseType: ResponseType.json, 26 | connectTimeout: connectTimeout ?? Duration(seconds: 5), 27 | ), 28 | ) { 29 | if (adapter != null) { 30 | dio.httpClientAdapter = adapter; 31 | } 32 | 33 | dio.interceptors.removeImplyContentTypeInterceptor(); 34 | 35 | if (interceptors != null) { 36 | dio.interceptors.addAll(interceptors); 37 | } 38 | } 39 | 40 | /// Meilisearch server URL. 41 | final String serverUrl; 42 | 43 | /// API key for authenticating with Meilisearch server. 44 | final String? apiKey; 45 | 46 | /// Timeout for opening a url. 47 | final Duration? connectTimeout; 48 | 49 | final Dio dio; 50 | 51 | /// Retrieve all headers used when Http calls are made. 52 | Map headers() { 53 | return dio.options.headers; 54 | } 55 | 56 | /// GET method 57 | Future> getMethod( 58 | String path, { 59 | Object? data, 60 | Map? queryParameters, 61 | }) async { 62 | try { 63 | return await dio.get( 64 | path, 65 | queryParameters: queryParameters, 66 | data: data, 67 | ); 68 | } on DioException catch (e) { 69 | return _throwException(e); 70 | } 71 | } 72 | 73 | /// POST method 74 | Future> postMethod( 75 | String path, { 76 | Object? data, 77 | Map? queryParameters, 78 | String contentType = Headers.jsonContentType, 79 | }) async { 80 | try { 81 | return await dio.post( 82 | path, 83 | data: data, 84 | queryParameters: queryParameters, 85 | options: Options( 86 | contentType: contentType, 87 | ), 88 | ); 89 | } on DioException catch (e) { 90 | return _throwException(e); 91 | } 92 | } 93 | 94 | /// PATCH method 95 | Future> patchMethod( 96 | String path, { 97 | Object? data, 98 | Map? queryParameters, 99 | String contentType = Headers.jsonContentType, 100 | }) async { 101 | try { 102 | return await dio.patch( 103 | path, 104 | data: data, 105 | queryParameters: queryParameters, 106 | options: Options( 107 | contentType: contentType, 108 | ), 109 | ); 110 | } on DioException catch (e) { 111 | return _throwException(e); 112 | } 113 | } 114 | 115 | /// PUT method 116 | Future> putMethod( 117 | String path, { 118 | Object? data, 119 | Map? queryParameters, 120 | String contentType = Headers.jsonContentType, 121 | }) async { 122 | try { 123 | return await dio.put( 124 | path, 125 | data: data, 126 | queryParameters: queryParameters, 127 | options: Options( 128 | contentType: contentType, 129 | ), 130 | ); 131 | } on DioException catch (e) { 132 | return _throwException(e); 133 | } 134 | } 135 | 136 | /// DELETE method 137 | Future> deleteMethod( 138 | String path, { 139 | Object? data, 140 | Map? queryParameters, 141 | }) async { 142 | try { 143 | return await dio.delete( 144 | path, 145 | data: data, 146 | queryParameters: queryParameters, 147 | ); 148 | } on DioException catch (e) { 149 | return _throwException(e); 150 | } 151 | } 152 | 153 | Never _throwException(DioException e) { 154 | final message = e.message ?? ''; 155 | if (e.type == DioExceptionType.badResponse) { 156 | throw MeiliSearchApiException.fromHttpBody(message, e.response?.data); 157 | } else { 158 | throw CommunicationException(message); 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /lib/src/query_parameters/_exports.dart: -------------------------------------------------------------------------------- 1 | //Barrel file to export other files 2 | export 'cancel_tasks_query.dart'; 3 | export 'delete_tasks_query.dart'; 4 | export 'documents_query.dart'; 5 | export 'indexes_query.dart'; 6 | export 'keys_query.dart'; 7 | export 'tasks_query.dart'; 8 | export 'index_search_query.dart'; 9 | export 'search_query.dart'; 10 | export 'multi_search_query.dart'; 11 | export 'delete_documents_query.dart'; 12 | export 'facet_search_query.dart'; 13 | export 'swap_index.dart'; 14 | export 'hybrid_search.dart'; 15 | -------------------------------------------------------------------------------- /lib/src/query_parameters/cancel_tasks_query.dart: -------------------------------------------------------------------------------- 1 | import 'queryable.dart'; 2 | 3 | class CancelTasksQuery extends Queryable { 4 | final DateTime? beforeEnqueuedAt; 5 | final DateTime? afterEnqueuedAt; 6 | final DateTime? beforeStartedAt; 7 | final DateTime? afterStartedAt; 8 | final DateTime? beforeFinishedAt; 9 | final DateTime? afterFinishedAt; 10 | final List uids; 11 | final List statuses; 12 | final List types; 13 | final List indexUids; 14 | 15 | const CancelTasksQuery({ 16 | this.beforeEnqueuedAt, 17 | this.afterEnqueuedAt, 18 | this.beforeStartedAt, 19 | this.afterStartedAt, 20 | this.beforeFinishedAt, 21 | this.afterFinishedAt, 22 | this.uids = const [], 23 | this.indexUids = const [], 24 | this.statuses = const [], 25 | this.types = const [], 26 | }); 27 | 28 | @override 29 | Map buildMap() { 30 | return { 31 | 'beforeEnqueuedAt': beforeEnqueuedAt, 32 | 'afterEnqueuedAt': afterEnqueuedAt, 33 | 'beforeStartedAt': beforeStartedAt, 34 | 'afterStartedAt': afterStartedAt, 35 | 'beforeFinishedAt': beforeFinishedAt, 36 | 'afterFinishedAt': afterFinishedAt, 37 | 'uids': uids, 38 | 'statuses': statuses, 39 | 'types': types, 40 | 'indexUids': indexUids, 41 | }; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/src/query_parameters/delete_documents_query.dart: -------------------------------------------------------------------------------- 1 | import 'package:meilisearch/meilisearch.dart'; 2 | import '../annotations.dart'; 3 | import 'queryable.dart'; 4 | 5 | class DeleteDocumentsQuery extends Queryable { 6 | final List? ids; 7 | 8 | @RequiredMeiliServerVersion('1.2.0') 9 | final Object? filter; 10 | 11 | @RequiredMeiliServerVersion('1.2.0') 12 | final MeiliOperatorExpressionBase? filterExpression; 13 | 14 | bool get containsFilter => filter != null || filterExpression != null; 15 | 16 | DeleteDocumentsQuery({ 17 | this.ids, 18 | this.filter, 19 | this.filterExpression, 20 | }) : assert( 21 | (ids != null && ids.isNotEmpty) ^ 22 | (filter != null || filterExpression != null), 23 | 'DeleteDocumentsQuery must contain either [ids] or [filter]/[filterExpression]', 24 | ); 25 | 26 | @override 27 | Map buildMap() { 28 | return { 29 | 'filter': filter ?? filterExpression?.transform(), 30 | }; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/src/query_parameters/delete_tasks_query.dart: -------------------------------------------------------------------------------- 1 | import 'queryable.dart'; 2 | 3 | class DeleteTasksQuery extends Queryable { 4 | final DateTime? beforeEnqueuedAt; 5 | final DateTime? afterEnqueuedAt; 6 | final DateTime? beforeStartedAt; 7 | final DateTime? afterStartedAt; 8 | final DateTime? beforeFinishedAt; 9 | final DateTime? afterFinishedAt; 10 | final List canceledBy; 11 | final List uids; 12 | final List statuses; 13 | final List types; 14 | final List indexUids; 15 | 16 | const DeleteTasksQuery({ 17 | this.beforeEnqueuedAt, 18 | this.afterEnqueuedAt, 19 | this.beforeStartedAt, 20 | this.afterStartedAt, 21 | this.beforeFinishedAt, 22 | this.afterFinishedAt, 23 | this.canceledBy = const [], 24 | this.uids = const [], 25 | this.indexUids = const [], 26 | this.statuses = const [], 27 | this.types = const [], 28 | }); 29 | 30 | @override 31 | Map buildMap() { 32 | return { 33 | 'beforeEnqueuedAt': beforeEnqueuedAt, 34 | 'afterEnqueuedAt': afterEnqueuedAt, 35 | 'beforeStartedAt': beforeStartedAt, 36 | 'afterStartedAt': afterStartedAt, 37 | 'beforeFinishedAt': beforeFinishedAt, 38 | 'afterFinishedAt': afterFinishedAt, 39 | 'canceledBy': canceledBy, 40 | 'uids': uids, 41 | 'statuses': statuses, 42 | 'types': types, 43 | 'indexUids': indexUids, 44 | }; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/src/query_parameters/documents_query.dart: -------------------------------------------------------------------------------- 1 | import 'package:meilisearch/meilisearch.dart'; 2 | import 'queryable.dart'; 3 | import '../annotations.dart'; 4 | 5 | class DocumentsQuery extends Queryable { 6 | final int? offset; 7 | final int? limit; 8 | final List fields; 9 | 10 | @RequiredMeiliServerVersion('1.2.0') 11 | final Object? filter; 12 | 13 | @RequiredMeiliServerVersion('1.2.0') 14 | final MeiliOperatorExpressionBase? filterExpression; 15 | 16 | bool get containsFilter => filter != null || filterExpression != null; 17 | 18 | const DocumentsQuery({ 19 | this.limit, 20 | this.offset, 21 | this.fields = const [], 22 | this.filter, 23 | this.filterExpression, 24 | }); 25 | 26 | @override 27 | Map buildMap() { 28 | return { 29 | 'offset': offset, 30 | 'limit': limit, 31 | 'fields': fields, 32 | 'filter': filter ?? filterExpression?.transform(), 33 | }; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/src/query_parameters/facet_search_query.dart: -------------------------------------------------------------------------------- 1 | import '../annotations.dart'; 2 | import 'search_query.dart'; 3 | 4 | @RequiredMeiliServerVersion('1.3.0') 5 | class FacetSearchQuery extends SearchQuery { 6 | const FacetSearchQuery({ 7 | required this.facetName, 8 | this.facetQuery = '', 9 | this.q = '', 10 | //Per spec, only filter and matchingStrategy are shared with the parent SearchQuery 11 | 12 | /// Additional search parameter. 13 | /// 14 | /// If additional search parameters are set, the method will return facet values that both: 15 | /// - Match the face query 16 | /// - Are contained in the records matching the additional search parameters 17 | super.filter, 18 | super.filterExpression, 19 | super.matchingStrategy, 20 | }); 21 | 22 | final String facetName; 23 | 24 | final String facetQuery; 25 | 26 | /// Additional search parameter. 27 | /// 28 | /// If additional search parameters are set, the method will return facet values that both: 29 | /// - Match the face query 30 | /// - Are contained in the records matching the additional search parameters 31 | final String q; 32 | 33 | @override 34 | Map buildMap() { 35 | return { 36 | 'facetName': facetName, 37 | 'facetQuery': facetQuery, 38 | 'q': q, 39 | ...super.buildMap(), 40 | }; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/src/query_parameters/hybrid_search.dart: -------------------------------------------------------------------------------- 1 | class HybridSearch { 2 | final String embedder; 3 | final double semanticRatio; 4 | 5 | const HybridSearch({ 6 | required this.embedder, 7 | required this.semanticRatio, 8 | }) : assert( 9 | semanticRatio >= 0.0 && semanticRatio <= 1.0, 10 | "'semanticRatio' must be between 0.0 and 1.0", 11 | ); 12 | 13 | Map toMap() => { 14 | 'embedder': embedder, 15 | 'semanticRatio': semanticRatio, 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /lib/src/query_parameters/index_search_query.dart: -------------------------------------------------------------------------------- 1 | import '../annotations.dart'; 2 | import '../filter_builder/_exports.dart'; 3 | import '../results/matching_strategy_enum.dart'; 4 | import 'hybrid_search.dart'; 5 | import 'search_query.dart'; 6 | 7 | @RequiredMeiliServerVersion('1.1.0') 8 | class IndexSearchQuery extends SearchQuery { 9 | final String indexUid; 10 | final String? query; 11 | 12 | const IndexSearchQuery({ 13 | required this.indexUid, 14 | this.query, 15 | super.offset, 16 | super.limit, 17 | super.page, 18 | super.hitsPerPage, 19 | super.filter, 20 | super.filterExpression, 21 | super.sort, 22 | super.facets, 23 | super.attributesToRetrieve, 24 | super.attributesToCrop, 25 | super.cropLength, 26 | super.attributesToHighlight, 27 | super.showMatchesPosition, 28 | super.cropMarker, 29 | super.highlightPreTag, 30 | super.highlightPostTag, 31 | super.matchingStrategy, 32 | super.attributesToSearchOn, 33 | super.hybrid, 34 | super.showRankingScore, 35 | super.vector, 36 | super.showRankingScoreDetails, 37 | }); 38 | 39 | @override 40 | Map buildMap() { 41 | return { 42 | 'indexUid': indexUid, 43 | 'q': query, 44 | ...super.buildMap(), 45 | }; 46 | } 47 | 48 | @override 49 | IndexSearchQuery copyWith({ 50 | String? indexUid, 51 | String? query, 52 | int? offset, 53 | int? limit, 54 | int? page, 55 | int? hitsPerPage, 56 | Object? filter, 57 | MeiliOperatorExpressionBase? filterExpression, 58 | List? sort, 59 | List? facets, 60 | List? attributesToRetrieve, 61 | List? attributesToCrop, 62 | int? cropLength, 63 | List? attributesToHighlight, 64 | bool? showMatchesPosition, 65 | String? cropMarker, 66 | String? highlightPreTag, 67 | String? highlightPostTag, 68 | MatchingStrategy? matchingStrategy, 69 | List? attributesToSearchOn, 70 | HybridSearch? hybrid, 71 | bool? showRankingScore, 72 | List */ >? vector, 73 | bool? showRankingScoreDetails, 74 | }) => 75 | IndexSearchQuery( 76 | query: query ?? this.query, 77 | indexUid: indexUid ?? this.indexUid, 78 | offset: offset ?? this.offset, 79 | limit: limit ?? this.limit, 80 | page: page ?? this.page, 81 | hitsPerPage: hitsPerPage ?? this.hitsPerPage, 82 | filter: filter ?? this.filter, 83 | filterExpression: filterExpression ?? this.filterExpression, 84 | sort: sort ?? this.sort, 85 | facets: facets ?? this.facets, 86 | attributesToRetrieve: attributesToRetrieve ?? this.attributesToRetrieve, 87 | attributesToCrop: attributesToCrop ?? this.attributesToCrop, 88 | cropLength: cropLength ?? this.cropLength, 89 | attributesToHighlight: 90 | attributesToHighlight ?? this.attributesToHighlight, 91 | showMatchesPosition: showMatchesPosition ?? this.showMatchesPosition, 92 | cropMarker: cropMarker ?? this.cropMarker, 93 | highlightPreTag: highlightPreTag ?? this.highlightPreTag, 94 | highlightPostTag: highlightPostTag ?? this.highlightPostTag, 95 | matchingStrategy: matchingStrategy ?? this.matchingStrategy, 96 | attributesToSearchOn: attributesToSearchOn ?? this.attributesToSearchOn, 97 | hybrid: hybrid ?? this.hybrid, 98 | showRankingScore: showRankingScore ?? this.showRankingScore, 99 | vector: vector ?? this.vector, 100 | showRankingScoreDetails: 101 | showRankingScoreDetails ?? this.showRankingScoreDetails, 102 | ); 103 | } 104 | -------------------------------------------------------------------------------- /lib/src/query_parameters/indexes_query.dart: -------------------------------------------------------------------------------- 1 | import 'queryable.dart'; 2 | 3 | class IndexesQuery extends Queryable { 4 | final int? offset; 5 | final int? limit; 6 | 7 | const IndexesQuery({ 8 | this.limit, 9 | this.offset, 10 | }); 11 | 12 | @override 13 | Map buildMap() { 14 | return { 15 | 'offset': offset, 16 | 'limit': limit, 17 | }; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/src/query_parameters/keys_query.dart: -------------------------------------------------------------------------------- 1 | import 'queryable.dart'; 2 | 3 | class KeysQuery extends Queryable { 4 | final int? offset; 5 | final int? limit; 6 | 7 | const KeysQuery({ 8 | this.limit, 9 | this.offset, 10 | }); 11 | 12 | @override 13 | Map buildMap() { 14 | return { 15 | 'offset': offset, 16 | 'limit': limit, 17 | }; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/src/query_parameters/multi_search_query.dart: -------------------------------------------------------------------------------- 1 | import 'queryable.dart'; 2 | 3 | import '../annotations.dart'; 4 | import 'index_search_query.dart'; 5 | 6 | @RequiredMeiliServerVersion('1.1.0') 7 | class MultiSearchQuery extends Queryable { 8 | final List queries; 9 | 10 | const MultiSearchQuery({ 11 | required this.queries, 12 | }); 13 | 14 | @override 15 | Map buildMap() { 16 | return { 17 | 'queries': queries.map((e) => e.toSparseMap()).toList(), 18 | }; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/src/query_parameters/queryable.dart: -------------------------------------------------------------------------------- 1 | abstract class Queryable { 2 | const Queryable(); 3 | 4 | ///For use with POST methods that can handle JSON types 5 | Map buildMap(); 6 | 7 | ///For use with GET methods that require queryParameters 8 | Map toQuery() { 9 | return toSparseMap()..updateAll((key, value) => toURIString(value)); 10 | } 11 | 12 | ///Returns a map with only non-null and non-empty fields 13 | Map toSparseMap() { 14 | return removeEmptyOrNullsFromMap(buildMap()); 15 | } 16 | 17 | Object toURIString(Object value) { 18 | if (value is DateTime) { 19 | return value.toUtc().toIso8601String(); 20 | } else if (value is List) { 21 | return value.join(','); 22 | } else { 23 | return value; 24 | } 25 | } 26 | 27 | static Map removeEmptyOrNullsFromMap( 28 | Map map, 29 | ) { 30 | return (map..removeWhere(isEmptyOrNull)).cast(); 31 | } 32 | 33 | static bool isEmptyOrNull(String key, Object? value) { 34 | return value == null || (value is Iterable && value.isEmpty); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/src/query_parameters/search_query.dart: -------------------------------------------------------------------------------- 1 | import 'package:meilisearch/meilisearch.dart'; 2 | import 'package:meilisearch/src/annotations.dart'; 3 | import 'queryable.dart'; 4 | 5 | class SearchQuery extends Queryable { 6 | final int? offset; 7 | final int? limit; 8 | final int? page; 9 | final int? hitsPerPage; 10 | final Object? filter; 11 | final MeiliOperatorExpressionBase? filterExpression; 12 | final List? sort; 13 | final List? facets; 14 | final List? attributesToRetrieve; 15 | final List? attributesToCrop; 16 | final int? cropLength; 17 | final List? attributesToHighlight; 18 | final bool? showMatchesPosition; 19 | final String? cropMarker; 20 | final String? highlightPreTag; 21 | final String? highlightPostTag; 22 | final MatchingStrategy? matchingStrategy; 23 | final List? attributesToSearchOn; 24 | @RequiredMeiliServerVersion('1.6.0') 25 | final HybridSearch? hybrid; 26 | @RequiredMeiliServerVersion('1.3.0') 27 | final bool? showRankingScore; 28 | @RequiredMeiliServerVersion('1.3.0') 29 | final bool? showRankingScoreDetails; 30 | @RequiredMeiliServerVersion('1.3.0') 31 | final List */ >? vector; 32 | 33 | const SearchQuery({ 34 | this.offset, 35 | this.limit, 36 | this.page, 37 | this.hitsPerPage, 38 | this.filter, 39 | this.filterExpression, 40 | this.sort, 41 | this.facets, 42 | this.attributesToRetrieve, 43 | this.attributesToCrop, 44 | this.cropLength, 45 | this.attributesToHighlight, 46 | this.showMatchesPosition, 47 | this.cropMarker, 48 | this.highlightPreTag, 49 | this.highlightPostTag, 50 | this.matchingStrategy, 51 | this.attributesToSearchOn, 52 | this.hybrid, 53 | this.showRankingScore, 54 | this.showRankingScoreDetails, 55 | this.vector, 56 | }); 57 | 58 | @override 59 | Map buildMap() { 60 | return { 61 | 'offset': offset, 62 | 'limit': limit, 63 | 'page': page, 64 | 'hitsPerPage': hitsPerPage, 65 | 'filter': filter ?? filterExpression?.transform(), 66 | 'sort': sort, 67 | 'facets': facets, 68 | 'attributesToRetrieve': attributesToRetrieve, 69 | 'attributesToCrop': attributesToCrop, 70 | 'cropLength': cropLength, 71 | 'attributesToHighlight': attributesToHighlight, 72 | 'showMatchesPosition': showMatchesPosition, 73 | 'cropMarker': cropMarker, 74 | 'highlightPreTag': highlightPreTag, 75 | 'highlightPostTag': highlightPostTag, 76 | 'matchingStrategy': matchingStrategy?.name, 77 | 'attributesToSearchOn': attributesToSearchOn, 78 | 'hybrid': hybrid?.toMap(), 79 | 'showRankingScore': showRankingScore, 80 | 'showRankingScoreDetails': showRankingScoreDetails, 81 | 'vector': vector, 82 | }; 83 | } 84 | 85 | SearchQuery copyWith({ 86 | int? offset, 87 | int? limit, 88 | int? page, 89 | int? hitsPerPage, 90 | Object? filter, 91 | MeiliOperatorExpressionBase? filterExpression, 92 | List? sort, 93 | List? facets, 94 | List? attributesToRetrieve, 95 | List? attributesToCrop, 96 | int? cropLength, 97 | List? attributesToHighlight, 98 | bool? showMatchesPosition, 99 | String? cropMarker, 100 | String? highlightPreTag, 101 | String? highlightPostTag, 102 | MatchingStrategy? matchingStrategy, 103 | List? attributesToSearchOn, 104 | HybridSearch? hybrid, 105 | bool? showRankingScore, 106 | List? vector, 107 | bool? showRankingScoreDetails, 108 | }) => 109 | SearchQuery( 110 | offset: offset ?? this.offset, 111 | limit: limit ?? this.limit, 112 | page: page ?? this.page, 113 | hitsPerPage: hitsPerPage ?? this.hitsPerPage, 114 | filter: filter ?? this.filter, 115 | filterExpression: filterExpression ?? this.filterExpression, 116 | sort: sort ?? this.sort, 117 | facets: facets ?? this.facets, 118 | attributesToRetrieve: attributesToRetrieve ?? this.attributesToRetrieve, 119 | attributesToCrop: attributesToCrop ?? this.attributesToCrop, 120 | cropLength: cropLength ?? this.cropLength, 121 | attributesToHighlight: 122 | attributesToHighlight ?? this.attributesToHighlight, 123 | showMatchesPosition: showMatchesPosition ?? this.showMatchesPosition, 124 | cropMarker: cropMarker ?? this.cropMarker, 125 | highlightPreTag: highlightPreTag ?? this.highlightPreTag, 126 | highlightPostTag: highlightPostTag ?? this.highlightPostTag, 127 | matchingStrategy: matchingStrategy ?? this.matchingStrategy, 128 | attributesToSearchOn: attributesToSearchOn ?? this.attributesToSearchOn, 129 | hybrid: hybrid ?? this.hybrid, 130 | showRankingScore: showRankingScore ?? this.showRankingScore, 131 | vector: vector ?? this.vector, 132 | showRankingScoreDetails: 133 | showRankingScoreDetails ?? this.showRankingScoreDetails, 134 | ); 135 | } 136 | -------------------------------------------------------------------------------- /lib/src/query_parameters/swap_index.dart: -------------------------------------------------------------------------------- 1 | class SwapIndex { 2 | final List indexes; 3 | 4 | SwapIndex(this.indexes) 5 | : assert( 6 | indexes.isEmpty || indexes.length == 2, 7 | 'Indexes must be either empty or have exactly 2 items', 8 | ); 9 | 10 | Map toQuery() { 11 | return {'indexes': indexes}; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/src/query_parameters/tasks_query.dart: -------------------------------------------------------------------------------- 1 | import 'queryable.dart'; 2 | 3 | class TasksQuery extends Queryable { 4 | final int? from; 5 | final int? limit; 6 | final DateTime? beforeEnqueuedAt; 7 | final DateTime? afterEnqueuedAt; 8 | final DateTime? beforeStartedAt; 9 | final DateTime? afterStartedAt; 10 | final DateTime? beforeFinishedAt; 11 | final DateTime? afterFinishedAt; 12 | final List uids; 13 | final List canceledBy; 14 | final List statuses; 15 | final List types; 16 | final List indexUids; 17 | 18 | const TasksQuery({ 19 | this.limit, 20 | this.from, 21 | this.beforeEnqueuedAt, 22 | this.afterEnqueuedAt, 23 | this.beforeStartedAt, 24 | this.afterStartedAt, 25 | this.beforeFinishedAt, 26 | this.afterFinishedAt, 27 | this.canceledBy = const [], 28 | this.uids = const [], 29 | this.indexUids = const [], 30 | this.statuses = const [], 31 | this.types = const [], 32 | }); 33 | 34 | TasksQuery copyWith({ 35 | List? uids, 36 | List? canceledBy, 37 | List? statuses, 38 | List? types, 39 | List? indexUids, 40 | }) { 41 | return TasksQuery( 42 | from: from, 43 | limit: limit, 44 | beforeEnqueuedAt: beforeEnqueuedAt, 45 | afterEnqueuedAt: afterEnqueuedAt, 46 | beforeStartedAt: beforeStartedAt, 47 | afterStartedAt: afterStartedAt, 48 | beforeFinishedAt: beforeFinishedAt, 49 | afterFinishedAt: afterFinishedAt, 50 | uids: uids ?? this.uids, 51 | canceledBy: canceledBy ?? this.canceledBy, 52 | statuses: statuses ?? this.statuses, 53 | types: types ?? this.types, 54 | indexUids: indexUids ?? this.indexUids, 55 | ); 56 | } 57 | 58 | @override 59 | Map buildMap() { 60 | return { 61 | 'from': from, 62 | 'limit': limit, 63 | 'canceledBy': canceledBy, 64 | 'beforeEnqueuedAt': beforeEnqueuedAt, 65 | 'afterEnqueuedAt': afterEnqueuedAt, 66 | 'beforeStartedAt': beforeStartedAt, 67 | 'afterStartedAt': afterStartedAt, 68 | 'beforeFinishedAt': beforeFinishedAt, 69 | 'afterFinishedAt': afterFinishedAt, 70 | 'uids': uids, 71 | 'statuses': statuses, 72 | 'types': types, 73 | 'indexUids': indexUids, 74 | }; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /lib/src/results/_exports.dart: -------------------------------------------------------------------------------- 1 | export 'multi_search_result.dart'; 2 | export 'searchable.dart'; 3 | export 'tasks_results.dart'; 4 | export 'result.dart'; 5 | export 'facet_search_result.dart'; 6 | export 'facet_hit.dart'; 7 | export 'key.dart'; 8 | export 'task.dart'; 9 | export 'task_error.dart'; 10 | export 'match_position.dart'; 11 | export 'matching_strategy_enum.dart'; 12 | export 'index_stats.dart'; 13 | export 'all_stats.dart'; 14 | export 'facet_stat.dart'; 15 | export 'document_container.dart'; 16 | export 'ranking_rules/_exports.dart'; 17 | -------------------------------------------------------------------------------- /lib/src/results/all_stats.dart: -------------------------------------------------------------------------------- 1 | import 'index_stats.dart'; 2 | 3 | class AllStats { 4 | AllStats({ 5 | this.databaseSize, 6 | this.lastUpdate, 7 | this.indexes, 8 | }); 9 | 10 | final int? databaseSize; 11 | final DateTime? lastUpdate; 12 | final Map? indexes; 13 | 14 | factory AllStats.fromMap(Map json) { 15 | final lastUpdateRaw = json['lastUpdate']; 16 | final indexesRaw = json['indexes']; 17 | 18 | return AllStats( 19 | databaseSize: json['databaseSize'] as int?, 20 | lastUpdate: 21 | lastUpdateRaw is String ? DateTime.tryParse(lastUpdateRaw) : null, 22 | indexes: indexesRaw is Map 23 | ? indexesRaw 24 | .cast>() 25 | .map((k, v) => MapEntry(k, IndexStats.fromMap(v))) 26 | : null, 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/src/results/document_container.dart: -------------------------------------------------------------------------------- 1 | import 'package:meilisearch/src/annotations.dart'; 2 | 3 | import 'match_position.dart'; 4 | import 'ranking_rules/base.dart'; 5 | import 'searchable.dart'; 6 | 7 | /// A class that wraps around documents returned from meilisearch to provide useful information. 8 | final class MeiliDocumentContainer { 9 | const MeiliDocumentContainer._({ 10 | required this.rankingScoreDetails, 11 | required this.src, 12 | required this.parsed, 13 | required this.formatted, 14 | required this.vectors, 15 | required this.semanticScore, 16 | required this.rankingScore, 17 | required this.matchesPosition, 18 | }); 19 | 20 | final Map src; 21 | final T parsed; 22 | final Map? formatted; 23 | @RequiredMeiliServerVersion('1.3.0') 24 | final List */ >? vectors; 25 | @RequiredMeiliServerVersion('1.3.0') 26 | final double? semanticScore; 27 | @RequiredMeiliServerVersion('1.3.0') 28 | final double? rankingScore; 29 | @RequiredMeiliServerVersion('1.3.0') 30 | final MeiliRankingScoreDetails? rankingScoreDetails; 31 | 32 | /// Contains the location of each occurrence of queried terms across all fields 33 | final Map>? matchesPosition; 34 | 35 | dynamic operator [](String key) => src[key]; 36 | dynamic getFormatted(String key) => formatted?[key]; 37 | 38 | dynamic getFormattedOrSrc(String key) => getFormatted(key) ?? this[key]; 39 | 40 | static MeiliDocumentContainer> fromJson( 41 | Map src, 42 | ) { 43 | final rankingScoreDetails = 44 | src['_rankingScoreDetails'] as Map?; 45 | return MeiliDocumentContainer>._( 46 | src: src, 47 | parsed: src, 48 | formatted: src['_formatted'] as Map?, 49 | vectors: src['_vectors'] as List?, 50 | semanticScore: src['_semanticScore'] as double?, 51 | rankingScore: src['_rankingScore'] as double?, 52 | matchesPosition: _readMatchesPosition(src), 53 | rankingScoreDetails: rankingScoreDetails == null 54 | ? null 55 | : MeiliRankingScoreDetails.fromJson(rankingScoreDetails), 56 | ); 57 | } 58 | 59 | MeiliDocumentContainer map( 60 | MeilisearchDocumentMapper mapper, 61 | ) { 62 | return MeiliDocumentContainer._( 63 | src: src, 64 | parsed: mapper(parsed), 65 | formatted: formatted, 66 | vectors: vectors, 67 | semanticScore: semanticScore, 68 | rankingScore: rankingScore, 69 | rankingScoreDetails: rankingScoreDetails, 70 | matchesPosition: matchesPosition); 71 | } 72 | 73 | @override 74 | String toString() => src.toString(); 75 | } 76 | 77 | class MeiliRankingScoreDetails { 78 | const MeiliRankingScoreDetails._({ 79 | required this.src, 80 | required this.words, 81 | required this.typo, 82 | required this.proximity, 83 | required this.attribute, 84 | required this.exactness, 85 | required this.customRules, 86 | }); 87 | final Map src; 88 | final MeiliRankingScoreDetailsWordsRule? words; 89 | final MeiliRankingScoreDetailsTypoRule? typo; 90 | final MeiliRankingScoreDetailsProximityRule? proximity; 91 | final MeiliRankingScoreDetailsAttributeRule? attribute; 92 | final MeiliRankingScoreDetailsExactnessRule? exactness; 93 | final Map customRules; 94 | 95 | factory MeiliRankingScoreDetails.fromJson(Map src) { 96 | final reservedKeys = { 97 | 'attribute', 98 | 'words', 99 | 'exactness', 100 | 'proximity', 101 | 'typo', 102 | }; 103 | 104 | T? ruleGuarded( 105 | String key, 106 | T Function(Map src) mapper, 107 | ) { 108 | final v = src[key]; 109 | if (v == null) { 110 | return null; 111 | } 112 | return mapper(v as Map); 113 | } 114 | 115 | return MeiliRankingScoreDetails._( 116 | src: src, 117 | attribute: ruleGuarded( 118 | 'attribute', 119 | MeiliRankingScoreDetailsAttributeRule.fromJson, 120 | ), 121 | words: ruleGuarded( 122 | 'words', 123 | MeiliRankingScoreDetailsWordsRule.fromJson, 124 | ), 125 | exactness: ruleGuarded( 126 | 'exactness', 127 | MeiliRankingScoreDetailsExactnessRule.fromJson, 128 | ), 129 | proximity: ruleGuarded( 130 | 'proximity', 131 | MeiliRankingScoreDetailsProximityRule.fromJson, 132 | ), 133 | typo: ruleGuarded( 134 | 'typo', 135 | MeiliRankingScoreDetailsTypoRule.fromJson, 136 | ), 137 | customRules: { 138 | for (var custom in src.entries 139 | .where((element) => !reservedKeys.contains(element.key))) 140 | custom.key: MeiliRankingScoreDetailsCustomRule.fromJson( 141 | custom.value as Map, 142 | ) 143 | }, 144 | ); 145 | } 146 | } 147 | 148 | Map>? _readMatchesPosition( 149 | Map map, 150 | ) { 151 | final src = map['_matchesPosition']; 152 | 153 | if (src == null) return null; 154 | 155 | return (src as Map).map( 156 | (key, value) => MapEntry( 157 | key, 158 | (value as List) 159 | .map((e) => MatchPosition.fromMap(e as Map)) 160 | .toList(), 161 | ), 162 | ); 163 | } 164 | -------------------------------------------------------------------------------- /lib/src/results/experimental_features.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | import 'package:meta/meta.dart'; 3 | import '../http_request.dart'; 4 | import '../annotations.dart'; 5 | 6 | part 'experimental_features.g.dart'; 7 | 8 | @visibleForTesting 9 | @JsonSerializable( 10 | createFactory: true, 11 | createToJson: false, 12 | ) 13 | class ExperimentalFeatures { 14 | const ExperimentalFeatures(); 15 | 16 | factory ExperimentalFeatures.fromJson(Map src) { 17 | return _$ExperimentalFeaturesFromJson(src); 18 | } 19 | } 20 | 21 | @JsonSerializable( 22 | includeIfNull: false, 23 | createToJson: true, 24 | createFactory: false, 25 | ) 26 | class UpdateExperimentalFeatures { 27 | const UpdateExperimentalFeatures(); 28 | 29 | Map toJson() => _$UpdateExperimentalFeaturesToJson(this); 30 | } 31 | 32 | extension ExperimentalFeaturesExt on HttpRequest { 33 | /// Get the status of all experimental features that can be toggled at runtime 34 | @RequiredMeiliServerVersion('1.3.0') 35 | @visibleForTesting 36 | Future getExperimentalFeatures() async { 37 | final response = await getMethod>( 38 | '/experimental-features', 39 | ); 40 | return ExperimentalFeatures.fromJson(response.data!); 41 | } 42 | 43 | /// Set the status of experimental features that can be toggled at runtime 44 | @RequiredMeiliServerVersion('1.3.0') 45 | @visibleForTesting 46 | Future updateExperimentalFeatures( 47 | UpdateExperimentalFeatures input, 48 | ) async { 49 | final inputJson = input.toJson(); 50 | if (inputJson.isEmpty) { 51 | throw ArgumentError.value( 52 | input, 53 | 'input', 54 | 'input must contain at least one entry', 55 | ); 56 | } 57 | final response = await patchMethod>( 58 | '/experimental-features', 59 | data: input.toJson(), 60 | ); 61 | return ExperimentalFeatures.fromJson(response.data!); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/src/results/experimental_features.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'experimental_features.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | ExperimentalFeatures _$ExperimentalFeaturesFromJson( 10 | Map json) => 11 | ExperimentalFeatures(); 12 | 13 | Map _$UpdateExperimentalFeaturesToJson( 14 | UpdateExperimentalFeatures instance) => 15 | {}; 16 | -------------------------------------------------------------------------------- /lib/src/results/facet_hit.dart: -------------------------------------------------------------------------------- 1 | import '../annotations.dart'; 2 | 3 | @RequiredMeiliServerVersion('1.3.0') 4 | class FacetHit { 5 | final String value; 6 | final int count; 7 | 8 | const FacetHit({ 9 | required this.value, 10 | required this.count, 11 | }); 12 | 13 | factory FacetHit.fromMap(Map map) { 14 | return FacetHit( 15 | value: map['value'] as String, 16 | count: map['count'] as int, 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/src/results/facet_search_result.dart: -------------------------------------------------------------------------------- 1 | import '../annotations.dart'; 2 | import 'facet_hit.dart'; 3 | 4 | @RequiredMeiliServerVersion('1.3.0') 5 | class FacetSearchResult { 6 | final List facetHits; 7 | final String facetQuery; 8 | final int processingTimeMs; 9 | 10 | const FacetSearchResult({ 11 | required this.facetHits, 12 | required this.facetQuery, 13 | required this.processingTimeMs, 14 | }); 15 | 16 | factory FacetSearchResult.fromMap(Map map) { 17 | return FacetSearchResult( 18 | facetHits: List.from( 19 | (map['facetHits'] as List?) 20 | ?.cast>() 21 | .map(FacetHit.fromMap) 22 | .toList() ?? 23 | [], 24 | ), 25 | facetQuery: map['facetQuery'] as String, 26 | processingTimeMs: map['processingTimeMs'] as int, 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/src/results/facet_stat.dart: -------------------------------------------------------------------------------- 1 | ///When using the facets parameter, the distributed facets that contain some numeric values are displayed in a facetStats object that contains, per facet, the numeric min and max values of the hits returned by the search query. 2 | ///If none of the hits returned by the search query have a numeric value for a facet, this facet is not part of the facetStats object. 3 | class FacetStat { 4 | ///The minimum value for the numerical facet being distributed. 5 | final num min; 6 | 7 | ///The maximum value for the numerical facet being distributed. 8 | final num max; 9 | 10 | const FacetStat({ 11 | required this.min, 12 | required this.max, 13 | }); 14 | 15 | factory FacetStat.fromMap(Map src) { 16 | return FacetStat( 17 | min: src['min'] as num, 18 | max: src['max'] as num, 19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/src/results/index_stats.dart: -------------------------------------------------------------------------------- 1 | class IndexStats { 2 | IndexStats({ 3 | this.numberOfDocuments, 4 | this.isIndexing, 5 | this.fieldsDistribution, 6 | }); 7 | 8 | final int? numberOfDocuments; 9 | final bool? isIndexing; 10 | final Map? fieldsDistribution; 11 | 12 | factory IndexStats.fromMap(Map map) => IndexStats( 13 | numberOfDocuments: map['numberOfDocuments'] as int?, 14 | isIndexing: map['isIndexing'] as bool?, 15 | fieldsDistribution: 16 | (map['fieldsDistribution'] as Map?)?.cast(), 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /lib/src/results/key.dart: -------------------------------------------------------------------------------- 1 | class Key { 2 | static const defaultActions = ["*"]; 3 | static const defaultIndexes = ["*"]; 4 | final String? uid; 5 | final String key; 6 | final String? name; 7 | final String? description; 8 | final List indexes; 9 | final List actions; 10 | final DateTime? expiresAt; 11 | final DateTime? createdAt; 12 | final DateTime? updatedAt; 13 | 14 | const Key({ 15 | this.uid = "", 16 | this.key = "", 17 | this.name, 18 | this.description, 19 | this.actions = defaultActions, 20 | this.indexes = defaultIndexes, 21 | this.expiresAt, 22 | this.createdAt, 23 | this.updatedAt, 24 | }); 25 | 26 | factory Key.fromJson(Map json) { 27 | final actionsRaw = json["actions"]; 28 | final indexesRaw = json["indexes"]; 29 | final expiresAtRaw = json["expiresAt"]; 30 | final createdAtRaw = json["createdAt"]; 31 | final updatedAtRaw = json["updatedAt"]; 32 | return Key( 33 | description: json["description"] as String?, 34 | key: json["key"] as String? ?? "", 35 | uid: json["uid"] as String?, 36 | actions: actionsRaw is Iterable 37 | ? List.from(actionsRaw) 38 | : defaultActions, 39 | indexes: indexesRaw is Iterable 40 | ? List.from(indexesRaw) 41 | : defaultIndexes, 42 | expiresAt: 43 | expiresAtRaw is String ? DateTime.tryParse(expiresAtRaw) : null, 44 | createdAt: createdAtRaw is String ? DateTime.parse(createdAtRaw) : null, 45 | updatedAt: updatedAtRaw is String ? DateTime.parse(updatedAtRaw) : null, 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/src/results/match_position.dart: -------------------------------------------------------------------------------- 1 | class MatchPosition { 2 | final int start; 3 | final int length; 4 | 5 | const MatchPosition({ 6 | required this.start, 7 | required this.length, 8 | }); 9 | 10 | factory MatchPosition.fromMap(Map map) { 11 | return MatchPosition( 12 | start: map['start'] as int, 13 | length: map['length'] as int, 14 | ); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/src/results/matching_strategy_enum.dart: -------------------------------------------------------------------------------- 1 | enum MatchingStrategy { 2 | all, 3 | last, 4 | } 5 | 6 | extension MatchingStrategyExtension on MatchingStrategy { 7 | String get name { 8 | switch (this) { 9 | case MatchingStrategy.all: 10 | return 'all'; 11 | case MatchingStrategy.last: 12 | return 'last'; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/src/results/multi_search_result.dart: -------------------------------------------------------------------------------- 1 | import 'searchable.dart'; 2 | 3 | class MultiSearchResult { 4 | final List>> results; 5 | 6 | const MultiSearchResult({ 7 | required this.results, 8 | }); 9 | 10 | factory MultiSearchResult.fromMap(Map json) { 11 | final list = json['results'] as List; 12 | final parsed = list 13 | .cast>() 14 | .map((e) => Searcheable.createSearchResult(e)); 15 | 16 | return MultiSearchResult(results: parsed.toList()); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/src/results/paginated_search_result.dart: -------------------------------------------------------------------------------- 1 | part of 'searchable.dart'; 2 | 3 | class PaginatedSearchResult extends Searcheable { 4 | const PaginatedSearchResult({ 5 | required super.src, 6 | required super.indexUid, 7 | required super.hits, 8 | required super.facetDistribution, 9 | required super.processingTimeMs, 10 | required super.query, 11 | required super.facetStats, 12 | required super.vector, 13 | required this.hitsPerPage, 14 | required this.page, 15 | required this.totalHits, 16 | required this.totalPages, 17 | }); 18 | 19 | /// Number of documents skipped 20 | final int? hitsPerPage; 21 | 22 | /// Number of documents to take 23 | final int? page; 24 | 25 | /// Total number of matches 26 | final int? totalHits; 27 | 28 | /// Total number of pages 29 | final int? totalPages; 30 | 31 | static PaginatedSearchResult> fromMap( 32 | Map map, { 33 | String? indexUid, 34 | }) { 35 | return PaginatedSearchResult( 36 | src: map, 37 | page: map['page'] as int?, 38 | hitsPerPage: map['hitsPerPage'] as int?, 39 | totalHits: map['totalHits'] as int?, 40 | totalPages: map['totalPages'] as int?, 41 | hits: _readHits(map), 42 | query: _readQuery(map), 43 | processingTimeMs: _readProcessingTimeMs(map), 44 | facetDistribution: _readFacetDistribution(map), 45 | facetStats: _readFacetStats(map), 46 | indexUid: indexUid ?? _readIndexUid(map), 47 | vector: map['vector'] as List?, 48 | ); 49 | } 50 | 51 | @override 52 | PaginatedSearchResult map( 53 | MeilisearchDocumentMapper mapper, 54 | ) { 55 | return PaginatedSearchResult( 56 | facetStats: facetStats, 57 | src: src, 58 | indexUid: indexUid, 59 | vector: vector, 60 | facetDistribution: facetDistribution, 61 | hits: hits.map(mapper).toList(), 62 | hitsPerPage: hitsPerPage, 63 | page: page, 64 | processingTimeMs: processingTimeMs, 65 | query: query, 66 | totalHits: totalHits, 67 | totalPages: totalPages, 68 | ); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /lib/src/results/ranking_rules/_exports.dart: -------------------------------------------------------------------------------- 1 | export 'base.dart'; 2 | -------------------------------------------------------------------------------- /lib/src/results/ranking_rules/attribute.dart: -------------------------------------------------------------------------------- 1 | part of 'base.dart'; 2 | 3 | class MeiliRankingScoreDetailsAttributeRule 4 | extends MeiliRankingScoreDetailsRuleBase { 5 | const MeiliRankingScoreDetailsAttributeRule._({ 6 | required super.src, 7 | required super.order, 8 | required super.score, 9 | required this.attributeRankingOrderScore, 10 | required this.queryWordDistanceScore, 11 | }); 12 | 13 | /// Score computed depending on the first attribute each word of the query appears in. 14 | /// The first attribute in the `searchableAttributes` list yields the highest score, the last attribute the lowest. 15 | final num attributeRankingOrderScore; 16 | 17 | /// Score computed depending on the position the attributes where each word of the query appears in. 18 | /// 19 | /// Words appearing in an attribute at the same position as in the query yield the highest score. 20 | /// 21 | /// The greater the distance to the position in the query, the lower the score. 22 | final num queryWordDistanceScore; 23 | 24 | factory MeiliRankingScoreDetailsAttributeRule.fromJson( 25 | Map src, 26 | ) => 27 | MeiliRankingScoreDetailsAttributeRule._( 28 | src: src, 29 | order: MeiliRankingScoreDetailsRuleBase._readOrder(src), 30 | score: MeiliRankingScoreDetailsRuleBase._readScore(src), 31 | attributeRankingOrderScore: src['attributeRankingOrderScore'] as num, 32 | queryWordDistanceScore: src['queryWordDistanceScore'] as num, 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /lib/src/results/ranking_rules/base.dart: -------------------------------------------------------------------------------- 1 | part 'attribute.dart'; 2 | part 'exactness.dart'; 3 | part 'proximity.dart'; 4 | part 'typo.dart'; 5 | part 'words.dart'; 6 | 7 | abstract class MeiliRankingScoreDetailsRuleBase { 8 | /// The source json object this was created from. 9 | final Map src; 10 | 11 | /// The order that this ranking rule was applied 12 | final int order; 13 | 14 | /// The relevancy score of a document according to a ranking rule and relative to a search query. 15 | /// 16 | /// Higher is better. 17 | /// 18 | /// - `1.0` indicates a perfect match 19 | /// - `0.0` no match at all (Meilisearch should not return documents that don't match the query). 20 | final double score; 21 | 22 | const MeiliRankingScoreDetailsRuleBase({ 23 | required this.src, 24 | required this.order, 25 | required this.score, 26 | }); 27 | 28 | static int _readOrder(Map src) => src['order'] as int; 29 | static double _readScore(Map src) => src['score'] as double; 30 | } 31 | 32 | /// Custom rule in the form of either `attribute:direction` or `_geoPoint(lat, lng):direction`. 33 | class MeiliRankingScoreDetailsCustomRule { 34 | /// The source json object this was created from. 35 | final Map src; 36 | 37 | /// The order that this ranking rule was applied 38 | final int order; 39 | 40 | /// The value that was used for sorting this document 41 | /// - string 42 | /// - number 43 | /// - point 44 | final dynamic value; 45 | 46 | /// The distance between the target point and the geoPoint in the document 47 | final num? distance; 48 | 49 | const MeiliRankingScoreDetailsCustomRule({ 50 | required this.src, 51 | required this.order, 52 | required this.value, 53 | required this.distance, 54 | }); 55 | 56 | factory MeiliRankingScoreDetailsCustomRule.fromJson( 57 | Map src) => 58 | MeiliRankingScoreDetailsCustomRule( 59 | src: src, 60 | order: MeiliRankingScoreDetailsRuleBase._readOrder(src), 61 | distance: src['distance'] as num?, 62 | value: src['value'], 63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /lib/src/results/ranking_rules/exactness.dart: -------------------------------------------------------------------------------- 1 | part of 'base.dart'; 2 | 3 | class MeiliRankingScoreDetailsExactnessRule 4 | extends MeiliRankingScoreDetailsRuleBase { 5 | const MeiliRankingScoreDetailsExactnessRule._({ 6 | required super.src, 7 | required super.order, 8 | required super.score, 9 | required this.matchType, 10 | }); 11 | 12 | /// One of `exactMatch`, `matchesStart` or `noExactMatch`. 13 | /// - `exactMatch`: the document contains an attribute that exactly matches the query. 14 | /// - `matchesStart`: the document contains an attribute that exactly starts with the query. 15 | /// - `noExactMatch`: any other document. 16 | final String matchType; 17 | 18 | factory MeiliRankingScoreDetailsExactnessRule.fromJson( 19 | Map src, 20 | ) => 21 | MeiliRankingScoreDetailsExactnessRule._( 22 | src: src, 23 | order: MeiliRankingScoreDetailsRuleBase._readOrder(src), 24 | score: MeiliRankingScoreDetailsRuleBase._readScore(src), 25 | matchType: src['matchType'] as String, 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /lib/src/results/ranking_rules/proximity.dart: -------------------------------------------------------------------------------- 1 | part of 'base.dart'; 2 | 3 | class MeiliRankingScoreDetailsProximityRule 4 | extends MeiliRankingScoreDetailsRuleBase { 5 | const MeiliRankingScoreDetailsProximityRule._({ 6 | required super.src, 7 | required super.order, 8 | required super.score, 9 | }); 10 | 11 | factory MeiliRankingScoreDetailsProximityRule.fromJson( 12 | Map src, 13 | ) { 14 | return MeiliRankingScoreDetailsProximityRule._( 15 | src: src, 16 | order: MeiliRankingScoreDetailsRuleBase._readOrder(src), 17 | score: MeiliRankingScoreDetailsRuleBase._readScore(src), 18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/src/results/ranking_rules/typo.dart: -------------------------------------------------------------------------------- 1 | part of 'base.dart'; 2 | 3 | class MeiliRankingScoreDetailsTypoRule 4 | extends MeiliRankingScoreDetailsRuleBase { 5 | const MeiliRankingScoreDetailsTypoRule._({ 6 | required super.src, 7 | required super.order, 8 | required super.score, 9 | required this.typoCount, 10 | required this.maxTypoCount, 11 | }); 12 | 13 | /// The number of typos to correct in the query to match that document. 14 | final int typoCount; 15 | 16 | /// The maximum number of typos that can be corrected in the query to match a document. 17 | final int maxTypoCount; 18 | 19 | factory MeiliRankingScoreDetailsTypoRule.fromJson(Map src) { 20 | return MeiliRankingScoreDetailsTypoRule._( 21 | src: src, 22 | order: MeiliRankingScoreDetailsRuleBase._readOrder(src), 23 | score: MeiliRankingScoreDetailsRuleBase._readScore(src), 24 | typoCount: src['typoCount'] as int, 25 | maxTypoCount: src['maxTypoCount'] as int, 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/src/results/ranking_rules/words.dart: -------------------------------------------------------------------------------- 1 | part of 'base.dart'; 2 | 3 | class MeiliRankingScoreDetailsWordsRule 4 | extends MeiliRankingScoreDetailsRuleBase { 5 | const MeiliRankingScoreDetailsWordsRule._({ 6 | required super.src, 7 | required super.order, 8 | required super.score, 9 | required this.matchingWords, 10 | required this.maxMatchingWords, 11 | }); 12 | 13 | /// the number of words from the query found 14 | final int matchingWords; 15 | 16 | /// 17 | final int maxMatchingWords; 18 | 19 | factory MeiliRankingScoreDetailsWordsRule.fromJson(Map src) { 20 | return MeiliRankingScoreDetailsWordsRule._( 21 | src: src, 22 | order: MeiliRankingScoreDetailsRuleBase._readOrder(src), 23 | score: MeiliRankingScoreDetailsRuleBase._readScore(src), 24 | matchingWords: src['matchingWords'] as int, 25 | maxMatchingWords: src['maxMatchingWords'] as int, 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/src/results/result.dart: -------------------------------------------------------------------------------- 1 | import 'package:meilisearch/meilisearch.dart'; 2 | 3 | class Result { 4 | final List results; 5 | final int total; 6 | final int limit; 7 | final int offset; 8 | 9 | const Result({ 10 | this.results = const [], 11 | this.limit = 0, 12 | this.offset = 0, 13 | this.total = 0, 14 | }); 15 | 16 | factory Result.fromMapWithType( 17 | Map map, 18 | MeilisearchDocumentMapper, T> mapper, 19 | ) => 20 | fromMap(map).map(mapper); 21 | 22 | static Result> fromMap(Map map) => 23 | Result( 24 | results: (map['results'] as Iterable?) 25 | ?.cast>() 26 | .toList() ?? 27 | [], 28 | total: map['total'] as int, 29 | offset: map['offset'] as int, 30 | limit: map['limit'] as int, 31 | ); 32 | 33 | Result map( 34 | MeilisearchDocumentMapper converter, 35 | ) { 36 | return Result( 37 | total: total, 38 | limit: limit, 39 | offset: offset, 40 | results: results.map(converter).toList(), 41 | ); 42 | } 43 | } 44 | 45 | extension ResultExt on Future> { 46 | Future> map( 47 | MeilisearchDocumentMapper mapper, 48 | ) { 49 | return then((value) => value.map(mapper)); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /lib/src/results/search_result.dart: -------------------------------------------------------------------------------- 1 | part of 'searchable.dart'; 2 | 3 | /// Represents an offset-based search result 4 | class SearchResult extends Searcheable { 5 | const SearchResult({ 6 | required super.src, 7 | required super.indexUid, 8 | required super.hits, 9 | required super.facetDistribution, 10 | required super.processingTimeMs, 11 | required super.query, 12 | required super.facetStats, 13 | required super.vector, 14 | required this.offset, 15 | required this.limit, 16 | required this.estimatedTotalHits, 17 | }); 18 | 19 | /// Number of documents skipped 20 | final int? offset; 21 | 22 | /// Number of documents to take 23 | final int? limit; 24 | 25 | /// Estimated number of matches 26 | final int? estimatedTotalHits; 27 | 28 | static SearchResult> fromMap( 29 | Map map, { 30 | String? indexUid, 31 | }) { 32 | return SearchResult( 33 | src: map, 34 | vector: map['vector'] as List?, 35 | limit: map['limit'] as int?, 36 | offset: map['offset'] as int?, 37 | estimatedTotalHits: map['estimatedTotalHits'] as int?, 38 | hits: _readHits(map), 39 | query: _readQuery(map), 40 | processingTimeMs: _readProcessingTimeMs(map), 41 | facetDistribution: _readFacetDistribution(map), 42 | indexUid: indexUid ?? _readIndexUid(map), 43 | facetStats: _readFacetStats(map), 44 | ); 45 | } 46 | 47 | @override 48 | SearchResult map( 49 | MeilisearchDocumentMapper mapper, 50 | ) => 51 | SearchResult( 52 | src: src, 53 | facetStats: facetStats, 54 | vector: vector, 55 | indexUid: indexUid, 56 | facetDistribution: facetDistribution, 57 | hits: hits.map(mapper).toList(), 58 | estimatedTotalHits: estimatedTotalHits, 59 | limit: limit, 60 | offset: offset, 61 | processingTimeMs: processingTimeMs, 62 | query: query, 63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /lib/src/results/searchable.dart: -------------------------------------------------------------------------------- 1 | import 'package:meilisearch/meilisearch.dart'; 2 | 3 | part 'search_result.dart'; 4 | part 'paginated_search_result.dart'; 5 | part 'searchable_helpers.dart'; 6 | 7 | /// Represents a search result. 8 | /// 9 | /// Can be one of: 10 | /// - [SearchResult] if offset, limit are used. 11 | /// - [PaginatedSearchResult] if page, hitsPerPage are used. 12 | abstract class Searcheable { 13 | final Map src; 14 | 15 | final String indexUid; 16 | 17 | /// Query originating the response 18 | final String? query; 19 | 20 | /// Results of the query 21 | final List hits; 22 | 23 | /// Distribution of the given facets 24 | final Map>? facetDistribution; 25 | 26 | /// Distribution of the given facets 27 | final Map? facetStats; 28 | 29 | /// Processing time of the query 30 | final int? processingTimeMs; 31 | final List*/ >? vector; 32 | 33 | const Searcheable({ 34 | required this.src, 35 | required this.indexUid, 36 | required this.query, 37 | required this.hits, 38 | required this.facetDistribution, 39 | required this.processingTimeMs, 40 | required this.facetStats, 41 | required this.vector, 42 | }); 43 | 44 | static Searcheable> createSearchResult( 45 | Map map, { 46 | String? indexUid, 47 | }) { 48 | if (map['totalHits'] != null) { 49 | return PaginatedSearchResult.fromMap(map, indexUid: indexUid); 50 | } else { 51 | return SearchResult.fromMap(map, indexUid: indexUid); 52 | } 53 | } 54 | 55 | Searcheable map(MeilisearchDocumentMapper mapper); 56 | 57 | PaginatedSearchResult asPaginatedResult() { 58 | final src = this; 59 | assert(src is PaginatedSearchResult); 60 | return src as PaginatedSearchResult; 61 | } 62 | 63 | SearchResult asSearchResult() { 64 | final src = this; 65 | assert(src is SearchResult); 66 | return src as SearchResult; 67 | } 68 | } 69 | 70 | extension MapSearcheable on Searcheable> { 71 | Searcheable>> mapToContainer() => 72 | map(MeiliDocumentContainer.fromJson); 73 | } 74 | 75 | extension MapSearcheableSearchResult on SearchResult> { 76 | SearchResult>> mapToContainer() => 77 | map(MeiliDocumentContainer.fromJson); 78 | } 79 | 80 | extension MapSearcheablePaginatedSearchResult 81 | on PaginatedSearchResult> { 82 | PaginatedSearchResult>> 83 | mapToContainer() => map(MeiliDocumentContainer.fromJson); 84 | } 85 | -------------------------------------------------------------------------------- /lib/src/results/searchable_helpers.dart: -------------------------------------------------------------------------------- 1 | part of 'searchable.dart'; 2 | 3 | String _readIndexUid(Map map) => map['indexUid'] as String; 4 | String? _readQuery(Map map) => map['query'] as String?; 5 | 6 | int? _readProcessingTimeMs(Map map) => 7 | map['processingTimeMs'] as int?; 8 | 9 | List> _readHits(Map map) => 10 | (map['hits'] as List?)?.cast>() ?? const []; 11 | 12 | Map? _readFacetStats( 13 | Map map, 14 | ) { 15 | final facetStatsRaw = map['facetStats'] as Map?; 16 | 17 | return facetStatsRaw?.map( 18 | (key, value) => MapEntry( 19 | key, 20 | FacetStat.fromMap(value as Map), 21 | ), 22 | ); 23 | } 24 | 25 | Map>? _readFacetDistribution( 26 | Map map, 27 | ) { 28 | final src = map['facetDistribution']; 29 | 30 | if (src == null) return null; 31 | 32 | return (src as Map).map( 33 | (key, value) => MapEntry( 34 | key, 35 | (value as Map).cast(), 36 | ), 37 | ); 38 | } 39 | 40 | typedef MeilisearchDocumentMapper = TOther Function(TSrc src); 41 | 42 | extension SearchableMapExt on Future>> { 43 | Future>>> 44 | mapToContainer() => then((value) => value.mapToContainer()); 45 | } 46 | 47 | extension SearchResultMapExt on Future>> { 48 | Future>>> 49 | mapToContainer() => then((value) => value.mapToContainer()); 50 | } 51 | 52 | extension PaginatedSearchResultMapExt 53 | on Future>> { 54 | Future>>> 55 | mapToContainer() => then((value) => value.mapToContainer()); 56 | } 57 | 58 | extension SearchableExt on Future> { 59 | Future> asPaginatedResult() => 60 | then((value) => value.asPaginatedResult()); 61 | 62 | Future> asSearchResult() => 63 | then((value) => value.asSearchResult()); 64 | 65 | Future> map( 66 | MeilisearchDocumentMapper mapper, 67 | ) => 68 | then((value) => value.map(mapper)); 69 | } 70 | 71 | extension SearchResultExt on Future> { 72 | Future> map( 73 | MeilisearchDocumentMapper mapper, 74 | ) => 75 | then((value) => value.map(mapper)); 76 | } 77 | 78 | extension PaginatedSearchResultExt on Future> { 79 | Future> map( 80 | MeilisearchDocumentMapper mapper, 81 | ) => 82 | then((value) => value.map(mapper)); 83 | } 84 | -------------------------------------------------------------------------------- /lib/src/results/task.dart: -------------------------------------------------------------------------------- 1 | import 'task_error.dart'; 2 | 3 | class Task { 4 | Task({ 5 | this.status, 6 | this.uid, 7 | this.indexUid, 8 | this.type, 9 | this.duration, 10 | this.enqueuedAt, 11 | this.processedAt, 12 | this.error, 13 | this.details, 14 | }); 15 | 16 | final String? status; 17 | final int? uid; 18 | final String? indexUid; 19 | final String? type; 20 | final String? duration; 21 | final DateTime? enqueuedAt; 22 | final DateTime? processedAt; 23 | final TaskError? error; 24 | final Map? details; 25 | 26 | factory Task.fromMap(Map map) { 27 | final enqueuedAtRaw = map['enqueuedAt']; 28 | final processedAtRaw = map['processedAt']; 29 | final errorRaw = map['error']; 30 | 31 | return Task( 32 | status: map['status'] as String?, 33 | uid: (map['uid'] ?? map['taskUid']) as int?, 34 | indexUid: map['indexUid'] as String?, 35 | duration: map['duration'] as String?, 36 | enqueuedAt: 37 | enqueuedAtRaw is String ? DateTime.tryParse(enqueuedAtRaw) : null, 38 | processedAt: 39 | processedAtRaw is String ? DateTime.tryParse(processedAtRaw) : null, 40 | type: map['type'] as String?, 41 | error: 42 | errorRaw is Map ? TaskError.fromMap(errorRaw) : null, 43 | details: map['details'] as Map?, 44 | ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/src/results/task_error.dart: -------------------------------------------------------------------------------- 1 | class TaskError { 2 | TaskError({ 3 | this.message, 4 | this.code, 5 | this.type, 6 | this.link, 7 | }); 8 | 9 | final String? message; 10 | final String? code; 11 | final String? type; 12 | final String? link; 13 | 14 | factory TaskError.fromMap(Map map) => TaskError( 15 | message: map['message'] as String?, 16 | code: map['code'] as String?, 17 | type: map['type'] as String?, 18 | link: map['link'] as String?, 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /lib/src/results/tasks_results.dart: -------------------------------------------------------------------------------- 1 | import 'task.dart'; 2 | 3 | import '../annotations.dart'; 4 | 5 | class TasksResults { 6 | final List results; 7 | final int? next; 8 | final int? limit; 9 | final int? from; 10 | @RequiredMeiliServerVersion('1.3.0') 11 | final int? total; 12 | const TasksResults({ 13 | this.results = const [], 14 | this.limit, 15 | this.from, 16 | this.next, 17 | this.total, 18 | }); 19 | 20 | factory TasksResults.fromMap(Map map) => TasksResults( 21 | results: (map['results'] as Iterable) 22 | .cast>() 23 | .map((item) => Task.fromMap(item)) 24 | .toList(), 25 | next: map['next'] as int?, 26 | from: map['from'] as int?, 27 | limit: map['limit'] as int?, 28 | total: map['total'] as int?, 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /lib/src/settings/_exports.dart: -------------------------------------------------------------------------------- 1 | //Barrel file to export other settings 2 | 3 | export 'index_settings.dart'; 4 | export 'min_word_size_for_typos.dart'; 5 | export 'typo_tolerance.dart'; 6 | export 'pagination.dart'; 7 | export 'faceting.dart'; 8 | -------------------------------------------------------------------------------- /lib/src/settings/distribution.dart: -------------------------------------------------------------------------------- 1 | /// Describes the mean and sigma of distribution of embedding similarity in the embedding space. 2 | /// 3 | /// The intended use is to make the similarity score more comparable to the regular ranking score. 4 | /// This allows to correct effects where results are too "packed" around a certain value. 5 | class DistributionShift { 6 | /// Value where the results are "packed". 7 | /// Similarity scores are translated so that they are packed around 0.5 instead 8 | final double mean; 9 | 10 | /// standard deviation of a similarity score. 11 | /// 12 | /// Set below 0.4 to make the results less packed around the mean, and above 0.4 to make them more packed. 13 | final double sigma; 14 | 15 | DistributionShift({ 16 | required this.mean, 17 | required this.sigma, 18 | }); 19 | 20 | factory DistributionShift.fromMap(Map map) { 21 | return DistributionShift( 22 | mean: map['mean'] as double, 23 | sigma: map['sigma'] as double, 24 | ); 25 | } 26 | 27 | Map toMap() => { 28 | 'mean': mean, 29 | 'sigma': sigma, 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /lib/src/settings/embedder.dart: -------------------------------------------------------------------------------- 1 | import './distribution.dart'; 2 | 3 | abstract class Embedder { 4 | const Embedder(); 5 | 6 | Map toMap(); 7 | 8 | factory Embedder.fromMap(Map map) { 9 | final source = map['source']; 10 | 11 | return switch (source) { 12 | OpenAiEmbedder.source => OpenAiEmbedder.fromMap(map), 13 | HuggingFaceEmbedder.source => HuggingFaceEmbedder.fromMap(map), 14 | UserProvidedEmbedder.source => UserProvidedEmbedder.fromMap(map), 15 | RestEmbedder.source => RestEmbedder.fromMap(map), 16 | OllamaEmbedder.source => OllamaEmbedder.fromMap(map), 17 | _ => UnknownEmbedder(data: map), 18 | }; 19 | } 20 | } 21 | 22 | class OpenAiEmbedder extends Embedder { 23 | static const source = 'openAi'; 24 | final String? model; 25 | final String? apiKey; 26 | final String? documentTemplate; 27 | final int? dimensions; 28 | final DistributionShift? distribution; 29 | final String? url; 30 | final int? documentTemplateMaxBytes; 31 | final bool? binaryQuantized; 32 | 33 | const OpenAiEmbedder({ 34 | this.model, 35 | this.apiKey, 36 | this.documentTemplate, 37 | this.dimensions, 38 | this.distribution, 39 | this.url, 40 | this.documentTemplateMaxBytes, 41 | this.binaryQuantized, 42 | }); 43 | 44 | @override 45 | Map toMap() => { 46 | 'source': source, 47 | 'model': model, 48 | 'apiKey': apiKey, 49 | 'documentTemplate': documentTemplate, 50 | 'dimensions': dimensions, 51 | 'distribution': distribution?.toMap(), 52 | 'url': url, 53 | 'documentTemplateMaxBytes': documentTemplateMaxBytes, 54 | 'binaryQuantized': binaryQuantized, 55 | }; 56 | 57 | factory OpenAiEmbedder.fromMap(Map map) { 58 | final distribution = map['distribution']; 59 | 60 | return OpenAiEmbedder( 61 | model: map['model'] as String?, 62 | apiKey: map['apiKey'] as String?, 63 | documentTemplate: map['documentTemplate'] as String?, 64 | dimensions: map['dimensions'] as int?, 65 | distribution: distribution is Map 66 | ? DistributionShift.fromMap(distribution) 67 | : null, 68 | url: map['url'] as String?, 69 | documentTemplateMaxBytes: map['documentTemplateMaxBytes'] as int?, 70 | binaryQuantized: map['binaryQuantized'] as bool?, 71 | ); 72 | } 73 | } 74 | 75 | class HuggingFaceEmbedder extends Embedder { 76 | static const source = 'huggingFace'; 77 | final String? model; 78 | final String? revision; 79 | final String? documentTemplate; 80 | final DistributionShift? distribution; 81 | final int? documentTemplateMaxBytes; 82 | final bool? binaryQuantized; 83 | 84 | const HuggingFaceEmbedder({ 85 | this.model, 86 | this.revision, 87 | this.documentTemplate, 88 | this.distribution, 89 | this.documentTemplateMaxBytes, 90 | this.binaryQuantized, 91 | }); 92 | 93 | @override 94 | Map toMap() => { 95 | 'source': source, 96 | 'model': model, 97 | 'documentTemplate': documentTemplate, 98 | 'distribution': distribution?.toMap(), 99 | 'documentTemplateMaxBytes': documentTemplateMaxBytes, 100 | 'binaryQuantized': binaryQuantized, 101 | }; 102 | 103 | factory HuggingFaceEmbedder.fromMap(Map map) { 104 | final distribution = map['distribution']; 105 | 106 | return HuggingFaceEmbedder( 107 | model: map['model'] as String?, 108 | documentTemplate: map['documentTemplate'] as String?, 109 | distribution: distribution is Map 110 | ? DistributionShift.fromMap(distribution) 111 | : null, 112 | documentTemplateMaxBytes: map['documentTemplateMaxBytes'] as int?, 113 | binaryQuantized: map['binaryQuantized'] as bool?, 114 | ); 115 | } 116 | } 117 | 118 | class UserProvidedEmbedder extends Embedder { 119 | static const source = 'userProvided'; 120 | final int dimensions; 121 | final DistributionShift? distribution; 122 | final bool? binaryQuantized; 123 | 124 | const UserProvidedEmbedder({ 125 | required this.dimensions, 126 | this.distribution, 127 | this.binaryQuantized, 128 | }); 129 | 130 | @override 131 | Map toMap() => { 132 | 'source': source, 133 | 'dimensions': dimensions, 134 | 'distribution': distribution?.toMap(), 135 | 'binaryQuantized': binaryQuantized, 136 | }; 137 | 138 | factory UserProvidedEmbedder.fromMap(Map map) { 139 | final distribution = map['distribution']; 140 | 141 | return UserProvidedEmbedder( 142 | dimensions: map['dimensions'] as int, 143 | distribution: distribution is Map 144 | ? DistributionShift.fromMap(distribution) 145 | : null, 146 | binaryQuantized: map['binaryQuantized'] as bool?, 147 | ); 148 | } 149 | } 150 | 151 | class RestEmbedder extends Embedder { 152 | static const source = 'rest'; 153 | final String url; 154 | final Map request; 155 | final Map response; 156 | final String? apiKey; 157 | final int? dimensions; 158 | final String? documentTemplate; 159 | final DistributionShift? distribution; 160 | final Map? headers; 161 | final int? documentTemplateMaxBytes; 162 | final bool? binaryQuantized; 163 | 164 | const RestEmbedder({ 165 | required this.url, 166 | required this.request, 167 | required this.response, 168 | this.apiKey, 169 | this.dimensions, 170 | this.documentTemplate, 171 | this.distribution, 172 | this.headers, 173 | this.documentTemplateMaxBytes, 174 | this.binaryQuantized, 175 | }); 176 | 177 | @override 178 | Map toMap() => { 179 | 'source': source, 180 | 'url': url, 181 | 'request': request, 182 | 'response': response, 183 | 'apiKey': apiKey, 184 | 'dimensions': dimensions, 185 | 'documentTemplate': documentTemplate, 186 | 'distribution': distribution?.toMap(), 187 | 'headers': headers, 188 | 'documentTemplateMaxBytes': documentTemplateMaxBytes, 189 | 'binaryQuantized': binaryQuantized, 190 | }; 191 | 192 | factory RestEmbedder.fromMap(Map map) { 193 | final distribution = map['distribution']; 194 | 195 | return RestEmbedder( 196 | url: map['url'] as String, 197 | request: map['request'] as Map, 198 | response: map['response'] as Map, 199 | apiKey: map['apiKey'] as String?, 200 | dimensions: map['dimensions'] as int?, 201 | documentTemplate: map['documentTemplate'] as String?, 202 | distribution: distribution is Map 203 | ? DistributionShift.fromMap(distribution) 204 | : null, 205 | headers: map['headers'] as Map?, 206 | documentTemplateMaxBytes: map['documentTemplateMaxBytes'] as int?, 207 | binaryQuantized: map['binaryQuantized'] as bool?, 208 | ); 209 | } 210 | } 211 | 212 | class OllamaEmbedder extends Embedder { 213 | static const source = 'ollama'; 214 | final String? url; 215 | final String? apiKey; 216 | final String? model; 217 | final String? documentTemplate; 218 | final DistributionShift? distribution; 219 | final int? dimensions; 220 | final int? documentTemplateMaxBytes; 221 | final bool? binaryQuantized; 222 | 223 | const OllamaEmbedder({ 224 | this.url, 225 | this.apiKey, 226 | this.model, 227 | this.documentTemplate, 228 | this.distribution, 229 | this.dimensions, 230 | this.documentTemplateMaxBytes, 231 | this.binaryQuantized, 232 | }); 233 | 234 | @override 235 | Map toMap() => { 236 | 'source': source, 237 | 'url': url, 238 | 'apiKey': apiKey, 239 | 'model': model, 240 | 'documentTemplate': documentTemplate, 241 | 'distribution': distribution?.toMap(), 242 | 'dimensions': dimensions, 243 | 'documentTemplateMaxBytes': documentTemplateMaxBytes, 244 | 'binaryQuantized': binaryQuantized, 245 | }; 246 | 247 | factory OllamaEmbedder.fromMap(Map map) { 248 | final distribution = map['distribution']; 249 | 250 | return OllamaEmbedder( 251 | url: map['url'] as String?, 252 | apiKey: map['apiKey'] as String?, 253 | model: map['model'] as String?, 254 | documentTemplate: map['documentTemplate'] as String?, 255 | distribution: distribution is Map 256 | ? DistributionShift.fromMap(distribution) 257 | : null, 258 | dimensions: map['dimensions'] as int?, 259 | documentTemplateMaxBytes: map['documentTemplateMaxBytes'] as int?, 260 | binaryQuantized: map['binaryQuantized'] as bool?, 261 | ); 262 | } 263 | } 264 | 265 | class UnknownEmbedder extends Embedder { 266 | final Map data; 267 | 268 | const UnknownEmbedder({ 269 | required this.data, 270 | }); 271 | 272 | @override 273 | Map toMap() => data; 274 | 275 | factory UnknownEmbedder.fromMap(String source, Map map) { 276 | return UnknownEmbedder(data: map); 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /lib/src/settings/faceting.dart: -------------------------------------------------------------------------------- 1 | import 'package:meilisearch/src/annotations.dart'; 2 | 3 | enum FacetingSortTypes { 4 | alpha('alpha'), 5 | count('count'); 6 | 7 | final String value; 8 | 9 | const FacetingSortTypes(this.value); 10 | } 11 | 12 | class Faceting { 13 | /// Define maximum number of value returned for a facet for a **search query**. 14 | /// It means that with the default value of `100`, 15 | /// it is not possible to have `101` different colors if the `color`` field is defined as a facet at search time. 16 | final int? maxValuesPerFacet; 17 | 18 | /// Defines how facet values are sorted. 19 | /// 20 | /// By default, all facets (`*`) are sorted by name, alphanumerically in ascending order (`alpha`). 21 | /// 22 | /// `count` sorts facet values by the number of documents containing a facet value in descending order. 23 | /// 24 | /// example: 25 | /// "*": 'alpha 26 | /// "genres": count 27 | @RequiredMeiliServerVersion('1.3.0') 28 | final Map? sortFacetValuesBy; 29 | 30 | const Faceting({ 31 | this.maxValuesPerFacet, 32 | this.sortFacetValuesBy, 33 | }); 34 | 35 | Map toMap() { 36 | return { 37 | if (maxValuesPerFacet != null) 'maxValuesPerFacet': maxValuesPerFacet, 38 | if (sortFacetValuesBy != null) 39 | 'sortFacetValuesBy': 40 | sortFacetValuesBy?.map((key, value) => MapEntry(key, value.value)), 41 | }; 42 | } 43 | 44 | factory Faceting.fromMap(Map map) { 45 | return Faceting( 46 | maxValuesPerFacet: map['maxValuesPerFacet'] as int?, 47 | sortFacetValuesBy: 48 | (map['sortFacetValuesBy'] as Map?)?.map( 49 | (key, value) => MapEntry( 50 | key, 51 | FacetingSortTypes.values 52 | .firstWhere((element) => element.value == value), 53 | ), 54 | ), 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lib/src/settings/index_settings.dart: -------------------------------------------------------------------------------- 1 | import '../annotations.dart'; 2 | import 'embedder.dart'; 3 | import 'faceting.dart'; 4 | import 'pagination.dart'; 5 | import 'typo_tolerance.dart'; 6 | 7 | class IndexSettings { 8 | IndexSettings({ 9 | this.synonyms, 10 | this.stopWords, 11 | this.rankingRules, 12 | this.filterableAttributes, 13 | this.distinctAttribute, 14 | this.sortableAttributes, 15 | this.searchableAttributes = allAttributes, 16 | this.displayedAttributes = allAttributes, 17 | this.typoTolerance, 18 | this.pagination, 19 | this.faceting, 20 | this.separatorTokens, 21 | this.nonSeparatorTokens, 22 | this.embedders, 23 | }); 24 | 25 | static const allAttributes = ['*']; 26 | 27 | /// List of associated words treated similarly 28 | Map>? synonyms; 29 | 30 | /// List of words ignored by Meilisearch when present in search queries 31 | List? stopWords; 32 | 33 | /// List of ranking rules sorted by order of importance 34 | List? rankingRules; 35 | 36 | /// List of tokens that will be considered as word separators by Meilisearch. 37 | List? separatorTokens; 38 | 39 | /// List of tokens that will not be considered as word separators by Meilisearch. 40 | List? nonSeparatorTokens; 41 | 42 | /// Attributes to use in [filters](https://www.meilisearch.com/docs/reference/api/search#filter) 43 | List? filterableAttributes; 44 | 45 | /// Search returns documents with distinct (different) values of the given field 46 | String? distinctAttribute; 47 | 48 | /// Fields in which to search for matching query words sorted by order of importance 49 | List? searchableAttributes; 50 | 51 | /// Fields displayed in the returned documents 52 | List? displayedAttributes; 53 | 54 | /// List of attributes by which to sort results 55 | List? sortableAttributes; 56 | 57 | /// Customize typo tolerance feature. 58 | TypoTolerance? typoTolerance; 59 | 60 | ///Customize pagination feature. 61 | Pagination? pagination; 62 | 63 | ///Customize faceting feature. 64 | Faceting? faceting; 65 | 66 | /// Set of embedders 67 | @RequiredMeiliServerVersion('1.6.0') 68 | Map? embedders; 69 | 70 | Map toMap() => { 71 | 'synonyms': synonyms, 72 | 'stopWords': stopWords, 73 | 'rankingRules': rankingRules, 74 | 'filterableAttributes': filterableAttributes, 75 | 'distinctAttribute': distinctAttribute, 76 | 'searchableAttributes': searchableAttributes, 77 | 'displayedAttributes': displayedAttributes, 78 | 'sortableAttributes': sortableAttributes, 79 | 'typoTolerance': typoTolerance?.toMap(), 80 | 'pagination': pagination?.toMap(), 81 | 'faceting': faceting?.toMap(), 82 | 'separatorTokens': separatorTokens, 83 | 'nonSeparatorTokens': nonSeparatorTokens, 84 | 'embedders': embedders?.map((k, v) => MapEntry(k, v.toMap())), 85 | }; 86 | 87 | factory IndexSettings.fromMap(Map map) { 88 | final typoTolerance = map['typoTolerance']; 89 | final pagination = map['pagination']; 90 | final faceting = map['faceting']; 91 | final synonyms = map['synonyms']; 92 | final stopWords = map['stopWords']; 93 | final rankingRules = map['rankingRules']; 94 | final filterableAttributes = map['filterableAttributes']; 95 | final searchableAttributes = map['searchableAttributes']; 96 | final displayedAttributes = map['displayedAttributes']; 97 | final sortableAttributes = map['sortableAttributes']; 98 | final separatorTokens = map['separatorTokens']; 99 | final nonSeparatorTokens = map['nonSeparatorTokens']; 100 | final embedders = map['embedders']; 101 | 102 | return IndexSettings( 103 | synonyms: synonyms is Map 104 | ? synonyms 105 | .cast>() 106 | .map((key, value) => MapEntry(key, value.cast())) 107 | : null, 108 | stopWords: 109 | stopWords is List ? stopWords.cast().toList() : null, 110 | rankingRules: 111 | rankingRules is List ? rankingRules.cast() : null, 112 | filterableAttributes: filterableAttributes is List 113 | ? filterableAttributes.cast() 114 | : null, 115 | distinctAttribute: map['distinctAttribute'] as String?, 116 | searchableAttributes: searchableAttributes is List 117 | ? searchableAttributes.cast() 118 | : allAttributes, 119 | displayedAttributes: displayedAttributes is List 120 | ? displayedAttributes.cast() 121 | : allAttributes, 122 | sortableAttributes: sortableAttributes is List 123 | ? sortableAttributes.cast() 124 | : null, 125 | typoTolerance: typoTolerance is Map 126 | ? TypoTolerance.fromMap(typoTolerance) 127 | : null, 128 | pagination: pagination is Map 129 | ? Pagination.fromMap(pagination) 130 | : null, 131 | faceting: 132 | faceting is Map ? Faceting.fromMap(faceting) : null, 133 | nonSeparatorTokens: nonSeparatorTokens is List 134 | ? nonSeparatorTokens.cast() 135 | : null, 136 | separatorTokens: separatorTokens is List 137 | ? separatorTokens.cast() 138 | : null, 139 | embedders: embedders is Map 140 | ? embedders.map((k, v) => 141 | MapEntry(k, Embedder.fromMap(v as Map))) 142 | : null, 143 | ); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /lib/src/settings/min_word_size_for_typos.dart: -------------------------------------------------------------------------------- 1 | const _defaultOneTypo = 5; 2 | const _defaultTwoTypo = 9; 3 | 4 | class MinWordSizeForTypos { 5 | ///Customize the minimum size for a word to tolerate 1 typo. 6 | int oneTypo; 7 | 8 | ///Customize the minimum size for a word to tolerate 2 typo. 9 | int twoTypos; 10 | 11 | MinWordSizeForTypos({ 12 | this.oneTypo = _defaultOneTypo, 13 | this.twoTypos = _defaultTwoTypo, 14 | }); 15 | 16 | Map toMap() { 17 | return { 18 | 'oneTypo': oneTypo, 19 | 'twoTypos': twoTypos, 20 | }; 21 | } 22 | 23 | factory MinWordSizeForTypos.fromMap(Map map) { 24 | return MinWordSizeForTypos( 25 | oneTypo: map['oneTypo'] as int? ?? _defaultOneTypo, 26 | twoTypos: map['twoTypos'] as int? ?? _defaultTwoTypo, 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/src/settings/pagination.dart: -------------------------------------------------------------------------------- 1 | const _defaultMaxTotalHits = 1000; 2 | 3 | class Pagination { 4 | ///Define the maximum number of documents reachable for a search request. 5 | ///It means that with the default value of `1000`, it is not possible to see the `1001`st result for a **search query**. 6 | int maxTotalHits; 7 | 8 | Pagination({ 9 | this.maxTotalHits = _defaultMaxTotalHits, 10 | }); 11 | 12 | Map toMap() { 13 | return { 14 | 'maxTotalHits': maxTotalHits, 15 | }; 16 | } 17 | 18 | factory Pagination.fromMap(Map map) { 19 | return Pagination( 20 | maxTotalHits: map['maxTotalHits'] as int? ?? _defaultMaxTotalHits, 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/src/settings/typo_tolerance.dart: -------------------------------------------------------------------------------- 1 | import 'min_word_size_for_typos.dart'; 2 | 3 | class TypoTolerance { 4 | ///Enable the typo tolerance feature. 5 | bool enabled; 6 | 7 | ///Disable the typo tolerance feature on the specified attributes. 8 | List disableOnAttributes; 9 | 10 | ///Disable the typo tolerance feature for a set of query terms given for a search query. 11 | List disableOnWords; 12 | 13 | ///Customize the minimum size for a typo in a word 14 | MinWordSizeForTypos? minWordSizeForTypos; 15 | 16 | TypoTolerance({ 17 | this.enabled = true, 18 | this.disableOnAttributes = const [], 19 | this.disableOnWords = const [], 20 | MinWordSizeForTypos? minWordSizeForTypos, 21 | }) : minWordSizeForTypos = minWordSizeForTypos ?? MinWordSizeForTypos(); 22 | 23 | Map toMap() { 24 | return { 25 | 'enabled': enabled, 26 | 'disableOnAttributes': disableOnAttributes, 27 | 'disableOnWords': disableOnWords, 28 | 'minWordSizeForTypos': minWordSizeForTypos?.toMap(), 29 | }; 30 | } 31 | 32 | factory TypoTolerance.fromMap(Map map) { 33 | final minWordSizeForTypos = map['minWordSizeForTypos']; 34 | return TypoTolerance( 35 | enabled: map['enabled'] as bool? ?? true, 36 | disableOnAttributes: 37 | (map['disableOnAttributes'] as List?)?.cast() ?? [], 38 | disableOnWords: (map['disableOnWords'] as List?)?.cast() ?? [], 39 | minWordSizeForTypos: minWordSizeForTypos is Map 40 | ? MinWordSizeForTypos.fromMap(minWordSizeForTypos) 41 | : MinWordSizeForTypos(), 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/src/tenant_token.dart: -------------------------------------------------------------------------------- 1 | library tenant_token; 2 | 3 | import 'dart:convert'; 4 | import 'dart:typed_data'; 5 | import 'package:crypto/crypto.dart'; 6 | 7 | part "tenant_token/generator.dart"; 8 | part "tenant_token/exceptions.dart"; 9 | -------------------------------------------------------------------------------- /lib/src/tenant_token/exceptions.dart: -------------------------------------------------------------------------------- 1 | part of '../tenant_token.dart'; 2 | 3 | class ExpiredSignatureException implements Exception { 4 | const ExpiredSignatureException(); 5 | } 6 | 7 | class NotUTCException implements Exception { 8 | const NotUTCException(); 9 | } 10 | 11 | class InvalidApiKeyException implements Exception { 12 | const InvalidApiKeyException(); 13 | } 14 | -------------------------------------------------------------------------------- /lib/src/tenant_token/generator.dart: -------------------------------------------------------------------------------- 1 | part of '../tenant_token.dart'; 2 | 3 | final _jsonEncoder = json.fuse(utf8.fuse(base64Url)); 4 | 5 | const _headers = {"typ": 'JWT', "alg": 'HS256'}; 6 | 7 | int? _getTimestamp(DateTime? time) { 8 | final now = DateTime.now().toUtc(); 9 | 10 | if (time == null) return null; 11 | if (!time.isUtc) throw const NotUTCException(); 12 | if (time.isBefore(now)) throw ExpiredSignatureException(); 13 | 14 | return time.millisecondsSinceEpoch; 15 | } 16 | 17 | Uint8List _sign(String secretKey, String msg) { 18 | final hmac = Hmac(sha256, utf8.encode(secretKey)); 19 | final body = Uint8List.fromList(utf8.encode(msg)); 20 | 21 | return Uint8List.fromList(hmac.convert(body).bytes); 22 | } 23 | 24 | String _tobase64(String value) { 25 | return value.replaceAll(RegExp('='), ''); 26 | } 27 | 28 | String generateToken(String uid, Object? searchRules, String apiKey, 29 | {DateTime? expiresAt}) { 30 | if (uid.isEmpty || apiKey.isEmpty) throw InvalidApiKeyException(); 31 | 32 | final expiration = _getTimestamp(expiresAt); 33 | final payload = { 34 | "searchRules": searchRules, 35 | "apiKeyUid": uid, 36 | if (expiration != null) 'exp': expiration, 37 | }; 38 | 39 | final encodedHeader = _tobase64(_jsonEncoder.encode(_headers)); 40 | final encodedBody = _tobase64(_jsonEncoder.encode(payload)); 41 | final unsignedBody = '$encodedHeader.$encodedBody'; 42 | final signature = _tobase64(base64Url.encode(_sign(apiKey, unsignedBody))); 43 | 44 | return '$unsignedBody.$signature'; 45 | } 46 | -------------------------------------------------------------------------------- /lib/src/version.dart: -------------------------------------------------------------------------------- 1 | class Version { 2 | static const String current = '0.17.0'; 3 | 4 | static String get qualifiedVersion { 5 | return "Meilisearch Dart (v$current)"; 6 | } 7 | 8 | static String get qualifiedVersionWeb { 9 | return "Meilisearch Dart Web (v$current)"; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: meilisearch 2 | description: Meilisearch Dart is the Meilisearch API client for Dart and Flutter developers. 3 | version: 0.17.0 4 | homepage: https://meilisearch.com 5 | repository: https://github.com/meilisearch/meilisearch-dart 6 | issue_tracker: https://github.com/meilisearch/meilisearch-dart/issues 7 | 8 | environment: 9 | sdk: ">=3.0.0 <4.0.0" 10 | 11 | dependencies: 12 | dio: ^5.0.0 13 | crypto: ^3.0.1 14 | collection: ^1.17.0 15 | json_annotation: ^4.8.1 16 | meta: ^1.9.1 17 | 18 | dev_dependencies: 19 | test: ^1.0.0 20 | dart_jsonwebtoken: ^2.12.2 21 | lints: ">=2.1.0 <4.0.0" 22 | json_serializable: ^6.7.1 23 | build_runner: ^2.4.6 24 | pub_semver: ^2.1.5 25 | 26 | screenshots: 27 | - description: The Meilisearch logo. 28 | path: screenshots/logo.png 29 | -------------------------------------------------------------------------------- /screenshots/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meilisearch/meilisearch-dart/8088dac252ca5f5466973645a8bdc27f6b9a0886/screenshots/logo.png -------------------------------------------------------------------------------- /test/analytics_test.dart: -------------------------------------------------------------------------------- 1 | @TestOn('vm') 2 | 3 | import 'dart:io'; 4 | 5 | import 'package:meilisearch/src/version.dart'; 6 | import 'package:test/test.dart'; 7 | 8 | import 'utils/client.dart'; 9 | 10 | void main() { 11 | final RegExp semVer = RegExp( 12 | r"version\:.(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?"); 13 | 14 | group('Version', () { 15 | test('matches with the current package version in pubspec.yaml', () { 16 | final path = '${Directory.current.path}/pubspec.yaml'; 17 | String data = File(path).readAsStringSync(); 18 | String? version = semVer.stringMatch(data)?.replaceFirst('version: ', ''); 19 | 20 | expect(version, isNotNull); 21 | expect(Version.current, isNotNull); 22 | expect(Version.current, equals(version)); 23 | }); 24 | }); 25 | 26 | group('Analytics', () { 27 | setUpClient(); 28 | 29 | test('sends the User-Agent header in every call', () { 30 | final headers = client.http.headers(); 31 | 32 | expect(headers.keys, contains('X-Meilisearch-Client')); 33 | expect(headers['X-Meilisearch-Client'], isNotNull); 34 | }); 35 | 36 | test('has current version data from Version class', () { 37 | final headers = client.http.headers(); 38 | 39 | expect(headers['X-Meilisearch-Client'], equals(Version.qualifiedVersion)); 40 | }); 41 | }); 42 | } 43 | -------------------------------------------------------------------------------- /test/custom_dio_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | 3 | import 'package:meilisearch/meilisearch.dart'; 4 | import 'package:test/test.dart'; 5 | 6 | import 'models/adapter.dart'; 7 | import 'utils/client.dart'; 8 | 9 | class TestInterceptor extends Interceptor { 10 | bool onRequestCalled = false; 11 | @override 12 | void onRequest(RequestOptions options, RequestInterceptorHandler handler) { 13 | onRequestCalled = true; 14 | super.onRequest(options, handler); 15 | } 16 | } 17 | 18 | void main() { 19 | group('Custom dio', () { 20 | test('Interceptor', () async { 21 | final interceptor = TestInterceptor(); 22 | final client = MeiliSearchClient.withCustomDio( 23 | testServer, 24 | apiKey: testApiKey, 25 | interceptors: [interceptor], 26 | ); 27 | 28 | var health = await client.health(); 29 | 30 | expect(health, {'status': 'available'}); 31 | expect(interceptor.onRequestCalled, equals(true)); 32 | }); 33 | test("Adapter", () async { 34 | final adapter = createTestAdapter(); 35 | final client = MeiliSearchClient.withCustomDio( 36 | testServer, 37 | apiKey: testApiKey, 38 | adapter: adapter, 39 | ); 40 | 41 | var health = await client.health(); 42 | 43 | expect(health, {'status': 'available'}); 44 | expect(adapter.fetchCalled, equals(true)); 45 | }); 46 | }); 47 | } 48 | -------------------------------------------------------------------------------- /test/dump_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:meilisearch/meilisearch.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | import 'utils/client.dart'; 5 | 6 | void main() { 7 | group('Dump', () { 8 | setUpClient(); 9 | test('creates a dump', () async { 10 | final task = await client.createDump(); 11 | //this teardown is to ensure no dump actually happens, since we are only checking the returned task 12 | addTearDown( 13 | () => client.cancelTasks(params: CancelTasksQuery(uids: [task.uid!])), 14 | ); 15 | 16 | expect(task.type, equals('dumpCreation')); 17 | expect(task.status, anyOf('succeeded', 'enqueued')); 18 | expect(task.indexUid, isNull); 19 | }); 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /test/exceptions_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:test/test.dart'; 2 | import 'package:meilisearch/meilisearch.dart'; 3 | 4 | import 'utils/client.dart'; 5 | 6 | void main() { 7 | setUpClient(); 8 | 9 | group('Exceptions', () { 10 | test('Throw exception with the detailed information from Meilisearch', 11 | () async { 12 | await expectLater( 13 | () => client.getIndex('wrongUID'), 14 | throwsA(isA().having( 15 | (error) => error.code, // Actual 16 | 'code', // Description of the check 17 | 'index_not_found', // Expected 18 | )), 19 | ); 20 | }); 21 | 22 | test('Throw basic 404 exception', () async { 23 | await expectLater( 24 | () => http.getMethod>('/wrong-path'), 25 | throwsA(isA().having( 26 | (error) => error.toString(), // Actual 27 | 'toString() method', // Description of the check 28 | contains('404'), // Expected 29 | )), 30 | ); 31 | }); 32 | 33 | test('Throw a CommunicationException', () async { 34 | final wrongClient = MeiliSearchClient('http://wrongURL', 'masterKey'); 35 | 36 | await expectLater( 37 | () => wrongClient.getIndex('test'), 38 | throwsA(isA()), 39 | ); 40 | }); 41 | }); 42 | } 43 | -------------------------------------------------------------------------------- /test/filter_builder_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:meilisearch/meilisearch.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | group('Filter builder', () { 6 | group("Attributes", () { 7 | test('basic transform', () { 8 | expect(Meili.attr('book_id').transform(), equals("\"book_id\"")); 9 | expect(Meili.attr('book.id').transform(), equals("\"book.id\"")); 10 | expect( 11 | Meili.attr(' book. id ').transform(), 12 | equals("\"book.id\""), 13 | ); 14 | expect(Meili.attr(' book.id. ').transform(), equals("\"book.id\"")); 15 | }); 16 | 17 | test('From Parts', () { 18 | final attr1 = Meili.attrFromParts(['contact', 'phone']); 19 | final attr2 = Meili.attr('contact.phone'); 20 | 21 | expect(attr1, attr2); 22 | }); 23 | }); 24 | 25 | group("Values", () { 26 | test("Strings", () { 27 | // 28 | final testData = [ 29 | "Hello", 30 | "hello", 31 | "hello!", 32 | "hello spaces", 33 | "doesn't need escape", 34 | ['needs escape"', 'needs escape\\"'], 35 | [r"fe\male", r'fe\\male'], 36 | ]; 37 | 38 | for (var element in testData) { 39 | if (element is List) { 40 | final value = element.first; 41 | final expected = element.last; 42 | 43 | expect(value.toMeiliValue().transform(), equals("\"$expected\"")); 44 | } else if (element is String) { 45 | expect(element.toMeiliValue().transform(), equals("\"$element\"")); 46 | } 47 | } 48 | }); 49 | 50 | test("Booleans", () { 51 | final testData = [ 52 | [true, "true"], 53 | [false, "false"], 54 | ]; 55 | for (var element in testData) { 56 | final value = element.first; 57 | final expected = element.last; 58 | 59 | expect(Meili.value(value).transform(), equals(expected)); 60 | } 61 | }); 62 | 63 | test("Numbers", () { 64 | final testData = [ 65 | [10, "10"], 66 | [11.5, "11.5"], 67 | ]; 68 | 69 | for (var element in testData) { 70 | final value = element.first; 71 | final expected = element.last; 72 | 73 | expect(Meili.value(value).transform(), equals(expected)); 74 | } 75 | }); 76 | test("Dates", () { 77 | final testData = [ 78 | [DateTime.utc(1999, 12, 14, 18, 53, 56), '945197636'], 79 | ]; 80 | 81 | for (var element in testData) { 82 | final value = element.first; 83 | final expected = element.last; 84 | 85 | expect(Meili.value(value).transform(), equals(expected)); 86 | } 87 | 88 | expect( 89 | () => Meili.value(DateTime(1999, 12, 14, 18, 53, 56)), 90 | throwsA( 91 | isA().having( 92 | (p0) => p0.message, 93 | 'message', 94 | equals( 95 | "DateTime passed to Meili must be in UTC to avoid inconsistency accross multiple devices", 96 | ), 97 | ), 98 | ), 99 | ); 100 | }); 101 | 102 | test("Arbitrary", () { 103 | expect( 104 | Meili.value(_ArbitraryClass()).transform(), 105 | equals('"ArbitraryString"'), 106 | ); 107 | }); 108 | }); 109 | 110 | group('[AND]', () { 111 | test("No expressions", () { 112 | final and = Meili.and([]); 113 | 114 | expect(and.transform(), equals("")); 115 | }); 116 | 117 | test("One expressions", () { 118 | final expr1 = 'book_id'.toMeiliAttribute().lt(100.toMeiliValue()); 119 | final and = Meili.and([expr1]); 120 | 121 | expect(and.transform(), equals(expr1.transform())); 122 | }); 123 | 124 | test("Two expressions", () { 125 | final expr1 = 'book_id'.toMeiliAttribute().lt(100.toMeiliValue()); 126 | final expr2 = 'tag'.toMeiliAttribute().eq("Tale".toMeiliValue()); 127 | final expr = expr1.and(expr2); 128 | 129 | expect( 130 | expr.transform(), 131 | "(\"book_id\" < 100) AND (\"tag\" = \"Tale\")", 132 | ); 133 | }); 134 | 135 | test("Three expressions", () { 136 | final expr1 = 'book_id'.toMeiliAttribute().lt(100.toMeiliValue()); 137 | final expr2 = 'tag'.toMeiliAttribute().eq("Tale".toMeiliValue()); 138 | final expr3 = 'tag'.toMeiliAttribute().exists(); 139 | final expr = expr1.andList([expr2, expr3]); 140 | 141 | expect( 142 | expr.transform(), 143 | "(\"book_id\" < 100) AND (\"tag\" = \"Tale\") AND (\"tag\" EXISTS)", 144 | ); 145 | }); 146 | }); 147 | 148 | group('[OR]', () { 149 | test("No expressions", () { 150 | final or = Meili.or([]); 151 | 152 | expect(or.transform(), equals("")); 153 | }); 154 | 155 | test("One expressions", () { 156 | final expr1 = 'book_id'.toMeiliAttribute().lt(100.toMeiliValue()); 157 | final or = Meili.or([expr1]); 158 | 159 | expect(or.transform(), equals(expr1.transform())); 160 | }); 161 | 162 | test("Two expressions", () { 163 | final expr1 = 'book_id'.toMeiliAttribute().lt(100.toMeiliValue()); 164 | final expr2 = 'tag'.toMeiliAttribute().eq("Tale".toMeiliValue()); 165 | final expr = expr1.or(expr2); 166 | 167 | expect(expr.transform(), "(\"book_id\" < 100) OR (\"tag\" = \"Tale\")"); 168 | }); 169 | 170 | test("Three expressions", () { 171 | final expr1 = 'book_id'.toMeiliAttribute().lt(100.toMeiliValue()); 172 | final expr2 = 'tag'.toMeiliAttribute().eq("Tale".toMeiliValue()); 173 | final expr3 = 'tag'.toMeiliAttribute().exists(); 174 | final expr = expr1.orList([expr2, expr3]); 175 | 176 | expect( 177 | expr.transform(), 178 | "(\"book_id\" < 100) OR (\"tag\" = \"Tale\") OR (\"tag\" EXISTS)", 179 | ); 180 | }); 181 | }); 182 | 183 | group("Geo", () { 184 | test('BoundingBox', () { 185 | final op = Meili.geoBoundingBox((lat: 10, lng: 5.3), (lat: 10, lng: 5)); 186 | 187 | expect(op.transform(), '_geoBoundingBox([10,5.3],[10,5])'); 188 | }); 189 | 190 | test('Radius', () { 191 | final op = Meili.geoRadius((lat: 10, lng: 5.3), 15); 192 | 193 | //in chrome, this will output (15), while in VM it will output (15.0) 194 | expect(op.transform(), '_geoRadius(10,5.3,${15.0})'); 195 | }); 196 | }); 197 | 198 | group('IS', () { 199 | test("NULL", () { 200 | final expr = "tag".toMeiliAttribute().isNull(); 201 | expect(expr.transform(), "\"tag\" IS NULL"); 202 | }); 203 | 204 | test("NOT NULL", () { 205 | final expr = "tag".toMeiliAttribute().isNotNull(); 206 | expect(expr.transform(), "\"tag\" IS NOT NULL"); 207 | }); 208 | 209 | test("EMPTY", () { 210 | final expr = "tag".toMeiliAttribute().isEmpty(); 211 | expect(expr.transform(), "\"tag\" IS EMPTY"); 212 | }); 213 | 214 | test("NOT EMPTY", () { 215 | final expr = "tag".toMeiliAttribute().isNotEmpty(); 216 | expect(expr.transform(), "\"tag\" IS NOT EMPTY"); 217 | }); 218 | }); 219 | 220 | group('IN', () { 221 | test('Mixed Types', () { 222 | final expr = "tag".toMeiliAttribute().$in(Meili.values(["hello", 5])); 223 | expect(expr.transform(), "\"tag\" IN [\"hello\",5]"); 224 | }); 225 | }); 226 | }); 227 | } 228 | 229 | class _ArbitraryClass { 230 | @override 231 | String toString() { 232 | return "ArbitraryString"; 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /test/get_client_stats_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:meilisearch/meilisearch.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | import 'utils/client.dart'; 5 | import 'utils/wait_for.dart'; 6 | 7 | void main() { 8 | group('Stats', () { 9 | setUpClient(); 10 | 11 | test('Getting all stats', () async { 12 | //index 1 13 | final uid1 = randomUid(); 14 | var index = client.index(uid1); 15 | 16 | var response = await index.addDocuments([ 17 | {'book_id': 123, 'title': 'Pride and Prejudice'}, 18 | {'book_id': 456, 'title': 'The Martin'}, 19 | ]).waitFor(client: client); 20 | 21 | expect(response.status, 'succeeded'); 22 | 23 | //index 2 24 | final uid2 = randomUid(); 25 | index = client.index(uid2); 26 | 27 | response = await index.addDocuments([ 28 | {'book_id': 789, 'title': 'Project Hail Mary'}, 29 | ]).waitFor(client: client); 30 | 31 | expect(response.status, 'succeeded'); 32 | 33 | //stats 34 | final stats = await client.getStats(); 35 | /*since tests might run concurrently, this needs to only check specific index uids*/ 36 | expect(stats.indexes!.keys, containsAll([uid1, uid2])); 37 | }); 38 | 39 | test('gets all tasks', () async { 40 | final uid = randomUid(); 41 | await client.createIndex(uid); 42 | 43 | final tasks = await client.getTasks(); 44 | 45 | expect(tasks.results, hasLength(greaterThan(0))); 46 | expect(tasks.results.first, isA()); 47 | }); 48 | 49 | test('gets a task by taskId', () async { 50 | final uid = randomUid(); 51 | final info = await client.createIndex(uid); 52 | 53 | final task = await client.getTask(info.uid!); 54 | 55 | expect(task, isA()); 56 | expect(task.uid, equals(info.uid)); 57 | }); 58 | }); 59 | } 60 | -------------------------------------------------------------------------------- /test/get_keys_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:meilisearch/meilisearch.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | import 'utils/client.dart'; 5 | import 'utils/wait_for.dart'; 6 | 7 | void main() { 8 | group('Keys', () { 9 | setUpClient(); 10 | 11 | group('When has master key', () { 12 | test('responds with all keys', () async { 13 | final key = await client.createKey(indexes: ['movies'], actions: ['*']); 14 | 15 | final allKeys = await client.getKeys(); 16 | 17 | expect( 18 | allKeys.results, 19 | contains(predicate((Key p0) => p0.key == key.key)), 20 | ); 21 | }); 22 | 23 | test('gets a key from server by key/uid', () async { 24 | final createdKey = 25 | await client.createKey(indexes: ['*'], actions: ['*']); 26 | 27 | Key key = await client.getKey(createdKey.key); 28 | 29 | expect(key.description, equals(createdKey.description)); 30 | expect(key.actions, equals(['*'])); 31 | expect(key.indexes, equals(['*'])); 32 | expect(key.key, createdKey.key); 33 | expect(key.expiresAt, isNull); 34 | expect(key.createdAt, createdKey.createdAt); 35 | expect(key.updatedAt, createdKey.updatedAt); 36 | }); 37 | 38 | test('creates a new key', () async { 39 | Key key = await client.createKey( 40 | description: "awesome-key", 41 | actions: ["documents.add"], 42 | indexes: ["movies"]); 43 | 44 | expect(key.description, equals("awesome-key")); 45 | expect(key.actions, equals(["documents.add"])); 46 | expect(key.indexes, equals(["movies"])); 47 | expect(key.expiresAt, isNull); 48 | }); 49 | 50 | test('creates a new key with uid', () async { 51 | Key key = await client.createKey( 52 | description: "awesome-key", 53 | uid: "8dbfeeee-65d4-4de2-b4cc-2b981d58d112", 54 | actions: ["documents.add"], 55 | indexes: ["movies"]); 56 | 57 | expect(key.uid, equals("8dbfeeee-65d4-4de2-b4cc-2b981d58d112")); 58 | expect(key.description, equals("awesome-key")); 59 | expect(key.actions, equals(["documents.add"])); 60 | expect(key.indexes, equals(["movies"])); 61 | expect(key.expiresAt, isNull); 62 | }); 63 | 64 | test('creates a new key with expiresAt', () async { 65 | var dt = DateTime.now().add(const Duration(days: 50)).toUtc(); 66 | 67 | Key key = await client.createKey( 68 | actions: ["documents.add"], indexes: ["movies"], expiresAt: dt); 69 | 70 | expect(key.description, isNull); 71 | // Meilisearch's API doesn't support microseconds/milliseconds 72 | // so we must crop them before send and remove it to assert properly. 73 | dt = dt.subtract(Duration( 74 | microseconds: dt.microsecond, milliseconds: dt.millisecond)); 75 | expect(key.expiresAt, equals(dt)); 76 | expect(key.expiresAt!.isAtSameMomentAs(dt), isTrue); 77 | }); 78 | 79 | test('updates a key partially', () async { 80 | final key = await client.createKey( 81 | actions: ["*"], indexes: ["*"], expiresAt: DateTime(2114)); 82 | 83 | final newKey = await client.updateKey(key.key, description: 'new desc'); 84 | 85 | expect(newKey.indexes, equals(['*'])); 86 | expect(newKey.actions, equals(['*'])); 87 | expect(newKey.expiresAt, isNotNull); 88 | expect(newKey.expiresAt, equals(key.expiresAt)); 89 | expect(newKey.description, equals('new desc')); 90 | }); 91 | 92 | test('deletes a key', () async { 93 | final key = await client.createKey(actions: ["*"], indexes: ["*"]); 94 | 95 | await expectLater(client.deleteKey(key.key), completion(isTrue)); 96 | }); 97 | }); 98 | 99 | group('When has a key with search scope only', () { 100 | late MeiliSearchClient tempClient; 101 | late String indexName; 102 | 103 | setUp(() async { 104 | final key = await client.createKey(indexes: ['*'], actions: ['search']); 105 | indexName = randomUid(); 106 | await client.createIndex(indexName).waitFor(client: client); 107 | 108 | tempClient = MeiliSearchClient(testServer, key.key); 109 | }); 110 | 111 | test('throws MeiliSearchApiException in getKeys call', () async { 112 | await expectLater( 113 | tempClient.getKeys(), 114 | throwsA(isA()), 115 | ); 116 | }); 117 | 118 | test('throws MeiliSearchApiException in createKey call', () async { 119 | await expectLater( 120 | tempClient.createKey(actions: ['*'], indexes: ['*']), 121 | throwsA(isA()), 122 | ); 123 | }); 124 | 125 | test('searches successfully', () async { 126 | await expectLater( 127 | tempClient.index(indexName).search('name'), 128 | completion(isA>>()), 129 | ); 130 | }); 131 | }); 132 | 133 | group('When has a invalid key', () { 134 | late MeiliSearchClient tempClient; 135 | setUp(() async { 136 | tempClient = MeiliSearchClient(testServer, 'this-is-a-invalid-key'); 137 | }); 138 | 139 | test('throws MeiliSearchApiException in getKeys call', () async { 140 | await expectLater( 141 | tempClient.getKeys(), 142 | throwsA(isA()), 143 | ); 144 | }); 145 | }); 146 | }); 147 | } 148 | -------------------------------------------------------------------------------- /test/get_version_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:test/test.dart'; 2 | 3 | import 'utils/client.dart'; 4 | 5 | void main() { 6 | group('Keys', () { 7 | setUpClient(); 8 | 9 | test('version is returned from the server', () async { 10 | var keys = await client.getVersion(); 11 | 12 | expect(keys.keys, contains('commitSha')); 13 | expect(keys.keys, contains('commitDate')); 14 | expect(keys.keys, contains('pkgVersion')); 15 | expect(keys['commitSha'], isNotEmpty); 16 | expect(keys['commitDate'], isNotEmpty); 17 | expect(keys['pkgVersion'], isNotEmpty); 18 | }); 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /test/health_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:meilisearch/meilisearch.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | import 'utils/client.dart'; 5 | 6 | void main() { 7 | group('Health', () { 8 | setUpClient(); 9 | 10 | test('of the server when the url is valid', () async { 11 | final health = await client.health(); 12 | 13 | expect(health, {'status': 'available'}); 14 | }); 15 | 16 | test('of the server when the url is valid with isHealthy', () async { 17 | final health = await client.isHealthy(); 18 | 19 | expect(health, true); 20 | }); 21 | }); 22 | 23 | group('Health Fail', () { 24 | late MeiliSearchClient badClient; 25 | setUp(() { 26 | final String server = 'http://wrongurl:1234'; 27 | final connectTimeout = Duration(milliseconds: 1000); 28 | badClient = MeiliSearchClient( 29 | server, 30 | testApiKey, 31 | connectTimeout, 32 | ); 33 | }); 34 | 35 | test('when the url is not valid', () async { 36 | await expectLater(badClient.health(), throwsException); 37 | }); 38 | 39 | test('when the url is not valid with isHealthy', () async { 40 | final health = await badClient.isHealthy(); 41 | 42 | expect(health, false); 43 | }); 44 | }); 45 | } 46 | -------------------------------------------------------------------------------- /test/indexes_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:meilisearch/meilisearch.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | import 'utils/client.dart'; 5 | import 'utils/wait_for.dart'; 6 | 7 | void main() { 8 | group('Indexes', () { 9 | late String uid; 10 | setUpClient(); 11 | setUp(() { 12 | uid = randomUid(); 13 | }); 14 | 15 | test('Create index with right UID without any primary passed', () async { 16 | await client.createIndex(uid).waitFor(client: client); 17 | 18 | final index = await client.getIndex(uid); 19 | 20 | expect(index.uid, uid); 21 | expect(index.primaryKey, null); 22 | }); 23 | 24 | test('Create index with right UID with a primary', () async { 25 | await client.createIndex(uid, primaryKey: 'myId').waitFor(client: client); 26 | 27 | final index = await client.getIndex(uid); 28 | 29 | expect(index.uid, uid); 30 | expect(index.primaryKey, 'myId'); 31 | }); 32 | 33 | test('Update an index where the primary has not been set', () async { 34 | await client.createIndex(uid).waitFor(client: client); 35 | 36 | var index = await client.getIndex(uid); 37 | await index.update(primaryKey: 'nextId').waitFor(client: client); 38 | index = await client.getIndex(uid); 39 | 40 | expect(index.primaryKey, equals('nextId')); 41 | }); 42 | 43 | test( 44 | 'Update an index from the client where the primary has not been set', 45 | () async { 46 | await client.createIndex(uid).waitFor(client: client); 47 | 48 | await client.updateIndex(uid, 'nextId').waitFor(client: client); 49 | 50 | final index = await client.getIndex(uid); 51 | expect(index.primaryKey, equals('nextId')); 52 | }, 53 | ); 54 | 55 | test('Delete an existing index', () async { 56 | await client.createIndex(uid).waitFor(client: client); 57 | 58 | final index = await client.getIndex(uid); 59 | await index.delete().waitFor(client: client); 60 | 61 | await expectLater( 62 | client.getIndex(uid), 63 | throwsA(isA()), 64 | ); 65 | }); 66 | 67 | test('Delete index with right UID from the client', () async { 68 | await client.createIndex(uid).waitFor(client: client); 69 | await client.deleteIndex(uid).waitFor(client: client); 70 | 71 | await expectLater( 72 | client.getIndex(uid), 73 | throwsA(isA()), 74 | ); 75 | }); 76 | 77 | test('Get an existing index', () async { 78 | await client.createIndex(uid).waitFor(client: client); 79 | 80 | var index = await client.getIndex(uid); 81 | 82 | expect(index.uid, uid); 83 | expect(index.primaryKey, null); 84 | }); 85 | 86 | test('gets raw information about an index', () async { 87 | await client.createIndex(uid).waitFor(client: client); 88 | 89 | final index = await client.getRawIndex(uid); 90 | final keys = ['uid', 'primaryKey', 'createdAt', 'updatedAt']; 91 | 92 | expect(index.keys, containsAll(keys)); 93 | expect(index.keys.length, keys.length); 94 | expect(index['primaryKey'], isNull); 95 | }); 96 | 97 | test('throws exception with a non-existing index', () async { 98 | expect(client.getIndex(randomUid('loremIpsum')), 99 | throwsA(isA())); 100 | }); 101 | 102 | test('Get all indexes', () async { 103 | const count = 3; 104 | final ids = List.generate(count, (index) => randomUid()); 105 | await Future.wait(ids.map(client.createIndex)).waitFor(client: client); 106 | 107 | final response = await client.getIndexes(); 108 | 109 | expect(response.results.map((e) => e.uid), containsAll(ids)); 110 | }); 111 | 112 | test('Create index object with UID', () async { 113 | final index = client.index(uid, deleteWhenDone: false); 114 | 115 | expect(index.uid, uid); 116 | expect(index.primaryKey, null); 117 | }); 118 | 119 | test('Create index object with UID and add Document', () async { 120 | var index = client.index(uid); 121 | await index.addDocuments([ 122 | {'book_id': 123, 'title': 'Pride and Prejudice'} 123 | ]).waitFor(client: client); 124 | 125 | index = await client.getIndex(uid); 126 | 127 | expect(index.uid, uid); 128 | }); 129 | 130 | test('Create index object and get it without add it', () async { 131 | client.index(uid, deleteWhenDone: false); 132 | 133 | await expectLater( 134 | client.getIndex(uid), 135 | throwsA(isA()), 136 | ); 137 | }); 138 | 139 | test('Geting index stats', () async { 140 | final index = client.index(uid); 141 | 142 | final response = await index.addDocuments([ 143 | {'book_id': 123, 'title': 'Pride and Prejudice'}, 144 | {'book_id': 456, 'title': 'The Martin'}, 145 | ]).waitFor(client: client); 146 | 147 | expect(response.status, 'succeeded'); 148 | final stats = await index.getStats(); 149 | expect(stats.numberOfDocuments, 2); 150 | }); 151 | 152 | test('gets all tasks by index', () async { 153 | await client.createIndex(uid).waitFor(client: client); 154 | final index = await client.getIndex(uid); 155 | 156 | await index.addDocuments([ 157 | {'book_id': 1234, 'title': 'Pride and Prejudice'} 158 | ]); 159 | await index.addDocuments([ 160 | {'book_id': 5678} 161 | ]); 162 | 163 | final tasks = await index.getTasks(); 164 | 165 | expect(tasks.results, isNotEmpty); 166 | }); 167 | 168 | test('gets a task from a index by taskId', () async { 169 | final index = client.index(uid); 170 | final response = await index.addDocuments([ 171 | {'book_id': 1234, 'title': 'Pride and Prejudice'} 172 | ]); 173 | 174 | final task = await index.getTask(response.uid!); 175 | 176 | expect(task.uid, response.uid!); 177 | }); 178 | 179 | test('gets a task with a failure', () async { 180 | final index = client.index(uid); 181 | 182 | await expectLater( 183 | index.updateRankingRules(['invalid-rule']), 184 | throwsA(isA()), 185 | ); 186 | }); 187 | 188 | test('Getting non-existant update status', () async { 189 | await client.createIndex(uid).waitFor(client: client); 190 | 191 | final index = await client.getIndex(uid); 192 | await expectLater( 193 | index.getTask(-1), 194 | throwsA(isA()), 195 | ); 196 | }); 197 | 198 | test('extracts all possible properties from task', () async { 199 | final task = await client.createIndex(uid); 200 | 201 | expect(task.uid, greaterThan(0)); 202 | expect(task.indexUid, equals(uid)); 203 | expect(task.type, equals("indexCreation")); 204 | }); 205 | }); 206 | } 207 | -------------------------------------------------------------------------------- /test/models/adapter.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | 3 | import 'package:dio/dio.dart'; 4 | 5 | import 'adapter_io.dart' if (dart.library.html) 'adapter_browser.dart' 6 | as adapter; 7 | 8 | mixin TestAdapterBase on HttpClientAdapter { 9 | bool fetchCalled = false; 10 | @override 11 | Future fetch( 12 | RequestOptions options, 13 | Stream? requestStream, 14 | Future? cancelFuture, 15 | ) { 16 | fetchCalled = true; 17 | return super.fetch(options, requestStream, cancelFuture); 18 | } 19 | } 20 | 21 | TestAdapterBase createTestAdapter() => adapter.TestAdapter(); 22 | -------------------------------------------------------------------------------- /test/models/adapter_browser.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/browser.dart'; 2 | import 'adapter.dart'; 3 | 4 | class TestAdapter extends BrowserHttpClientAdapter with TestAdapterBase {} 5 | -------------------------------------------------------------------------------- /test/models/adapter_io.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/io.dart'; 2 | import 'adapter.dart'; 3 | 4 | class TestAdapter extends IOHttpClientAdapter with TestAdapterBase {} 5 | -------------------------------------------------------------------------------- /test/models/test_client.dart: -------------------------------------------------------------------------------- 1 | import 'package:collection/collection.dart'; 2 | import 'package:dio/dio.dart'; 3 | import 'package:meilisearch/meilisearch.dart'; 4 | 5 | class TestMeiliSearchClient extends MeiliSearchClient { 6 | TestMeiliSearchClient( 7 | super.serverUrl, [ 8 | super.apiKey, 9 | super.connectTimeout, 10 | super.adapter, 11 | super.interceptors, 12 | ]); 13 | 14 | factory TestMeiliSearchClient.withCustomDio( 15 | String serverUrl, { 16 | String? apiKey, 17 | Duration? connectTimeout, 18 | HttpClientAdapter? adapter, 19 | List? interceptors, 20 | }) => 21 | TestMeiliSearchClient( 22 | serverUrl, 23 | apiKey, 24 | connectTimeout, 25 | adapter, 26 | interceptors, 27 | ); 28 | 29 | final usedIndexes = {}; 30 | final usedKeys = {}; 31 | 32 | @override 33 | MeiliSearchIndex index(String uid, {bool deleteWhenDone = true}) { 34 | if (deleteWhenDone) { 35 | usedIndexes.add(uid); 36 | } 37 | return super.index(uid); 38 | } 39 | 40 | @override 41 | Future createIndex(String uid, {String? primaryKey}) { 42 | usedIndexes.add(uid); 43 | return super.createIndex(uid, primaryKey: primaryKey); 44 | } 45 | 46 | @override 47 | Future deleteIndex(String uid) { 48 | usedIndexes.remove(uid); 49 | return super.deleteIndex(uid); 50 | } 51 | 52 | @override 53 | Future> getRawIndex( 54 | String uid, { 55 | bool deleteWhenDone = true, 56 | }) { 57 | return super.getRawIndex(uid).then((value) { 58 | if (deleteWhenDone) { 59 | usedIndexes.add(uid); 60 | } 61 | return value; 62 | }).onError((error, stackTrace) { 63 | usedIndexes.remove(uid); 64 | throw error!; 65 | }); 66 | } 67 | 68 | @override 69 | Future swapIndexes( 70 | List param, { 71 | bool deleteWhenDone = true, 72 | }) { 73 | if (deleteWhenDone) { 74 | usedIndexes.addAll(param.map((e) => e.indexes).flattened); 75 | } 76 | return super.swapIndexes(param); 77 | } 78 | 79 | @override 80 | Future createKey({ 81 | required List indexes, 82 | required List actions, 83 | DateTime? expiresAt, 84 | String? description, 85 | String? uid, 86 | bool deleteWhenDone = true, 87 | }) { 88 | return super 89 | .createKey( 90 | expiresAt: expiresAt, 91 | description: description, 92 | uid: uid, 93 | indexes: indexes, 94 | actions: actions, 95 | ) 96 | .then((value) { 97 | if (deleteWhenDone) { 98 | usedKeys.add(value.key); 99 | } 100 | return value; 101 | }); 102 | } 103 | 104 | @override 105 | Future deleteKey(String key) { 106 | usedKeys.remove(key); 107 | return super.deleteKey(key); 108 | } 109 | 110 | Future disposeUsedResources() async { 111 | await Future.wait([ 112 | _deleteUsedIndexes(), 113 | _deleteUsedKeys(), 114 | ]); 115 | } 116 | 117 | Future _deleteUsedIndexes() async { 118 | await Future.wait( 119 | usedIndexes.toSet().map((e) => deleteIndex(e)), 120 | ); 121 | } 122 | 123 | Future _deleteUsedKeys() async { 124 | await Future.wait( 125 | usedKeys.toSet().map( 126 | (e) => deleteKey(e).onError((error, stackTrace) => false), 127 | ), 128 | ); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /test/multi_index_search_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:meilisearch/meilisearch.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | import 'utils/books_data.dart'; 5 | import 'utils/client.dart'; 6 | import 'utils/wait_for.dart'; 7 | 8 | void main() { 9 | group("MultiIndex search", () { 10 | setUpClient(); 11 | late MeiliSearchIndex index1; 12 | late MeiliSearchIndex index2; 13 | 14 | setUp(() async { 15 | index1 = client.index(randomUid()); 16 | index2 = client.index(randomUid()); 17 | 18 | await Future.wait([ 19 | index1.updateFilterableAttributes([ktag]).waitFor(client: client), 20 | index2.updateFilterableAttributes([ktag]).waitFor(client: client), 21 | index1.addDocuments(books).waitFor(client: client), 22 | index2.addDocuments(books).waitFor(client: client), 23 | ]); 24 | }); 25 | 26 | test("Multi search from 2 indexes", () async { 27 | final result = await client.multiSearch(MultiSearchQuery(queries: [ 28 | IndexSearchQuery( 29 | query: "", 30 | indexUid: index1.uid, 31 | filterExpression: 32 | ktag.toMeiliAttribute().eq("Romance".toMeiliValue()), 33 | ), 34 | IndexSearchQuery( 35 | indexUid: index2.uid, 36 | filterExpression: ktag.toMeiliAttribute().eq("Tale".toMeiliValue()), 37 | ), 38 | ])); 39 | 40 | expect(result.results, hasLength(2)); 41 | //test first result 42 | expect(result.results.first.indexUid, index1.uid); 43 | expect(result.results.first.hits.length, 1); 44 | //test second result 45 | expect(result.results.last.indexUid, index2.uid); 46 | expect(result.results.last.hits.length, 2); 47 | }); 48 | }); 49 | 50 | test('code samples', () async { 51 | // #docregion multi_search_1 52 | await client.multiSearch(MultiSearchQuery(queries: [ 53 | IndexSearchQuery(query: 'pooh', indexUid: 'movies', limit: 5), 54 | IndexSearchQuery(query: 'nemo', indexUid: 'movies', limit: 5), 55 | IndexSearchQuery(query: 'us', indexUid: 'movies_ratings'), 56 | ])); 57 | // #enddocregion 58 | }, skip: true); 59 | } 60 | -------------------------------------------------------------------------------- /test/queryable_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:meilisearch/src/query_parameters/queryable.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | class FakeQueryClass extends Queryable { 5 | final int? myInteger; 6 | final String? myString; 7 | final DateTime? myDate; 8 | final List myList; 9 | 10 | FakeQueryClass({ 11 | this.myInteger, 12 | this.myString, 13 | this.myDate, 14 | this.myList = const [], 15 | }); 16 | 17 | @override 18 | Map buildMap() { 19 | return { 20 | 'myInteger': myInteger, 21 | 'myString': myString, 22 | 'myDate': myDate, 23 | 'myList': myList, 24 | }; 25 | } 26 | } 27 | 28 | void main() { 29 | test('responds with non-null values', () { 30 | var query = FakeQueryClass(myList: [1, 2], myInteger: 99); 31 | 32 | expect(query.toQuery(), { 33 | 'myList': '1,2', 34 | 'myInteger': 99, 35 | }); 36 | }); 37 | 38 | test('supports all main types', () { 39 | var date = DateTime.now(); 40 | var query = FakeQueryClass( 41 | myList: [1, 2], 42 | myInteger: 99, 43 | myString: 'foo', 44 | myDate: date, 45 | ); 46 | 47 | expect(query.toQuery(), { 48 | 'myDate': date.toUtc().toIso8601String(), 49 | 'myString': 'foo', 50 | 'myList': '1,2', 51 | 'myInteger': 99, 52 | }); 53 | }); 54 | } 55 | -------------------------------------------------------------------------------- /test/swaps_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:meilisearch/meilisearch.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | import 'utils/client.dart'; 5 | import 'utils/wait_for.dart'; 6 | 7 | void main() { 8 | group('Swaps indexes', () { 9 | setUpClient(); 10 | 11 | test('swaps indexes from input', () async { 12 | var books = [randomUid('books'), randomUid('books_new')]; 13 | var movies = [randomUid('movies'), randomUid('movies_new')]; 14 | var swaps = [SwapIndex(books), SwapIndex(movies)]; 15 | 16 | // first create the indexes to be swapped 17 | for (var index in books + movies) { 18 | await client.createIndex(index).waitFor(client: client); 19 | } 20 | 21 | var response = await client 22 | .swapIndexes( 23 | swaps, 24 | deleteWhenDone: false, 25 | ) 26 | .waitFor( 27 | client: client, 28 | throwFailed: true, 29 | ); 30 | 31 | expect(response.type, 'indexSwap'); 32 | expect(response.error, null); 33 | expect(response.status, 'succeeded'); 34 | expect(response.details!['swaps'], [ 35 | {'indexes': books}, 36 | {'indexes': movies} 37 | ]); 38 | }); 39 | }); 40 | 41 | test('code samples', () async { 42 | // #docregion swap_indexes_1 43 | await client.swapIndexes([ 44 | SwapIndex(['indexA', 'indexB']), 45 | SwapIndex(['indexX', 'indexY']), 46 | ]); 47 | // #enddocregion 48 | }, skip: true); 49 | } 50 | -------------------------------------------------------------------------------- /test/tasks_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:meilisearch/meilisearch.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | import 'utils/books_data.dart'; 5 | import 'utils/client.dart'; 6 | import 'utils/wait_for.dart'; 7 | 8 | void main() { 9 | group("Tasks", () { 10 | late String uid; 11 | late MeiliSearchIndex index; 12 | 13 | setUpClient(); 14 | setUp(() { 15 | uid = randomUid(); 16 | index = client.index(uid); 17 | }); 18 | 19 | test('Query by type', () async { 20 | final docs = books; 21 | final task = await index.addDocuments(docs); 22 | 23 | expect(task.type, 'documentAdditionOrUpdate'); 24 | //test several permutations of indexUids 25 | final uidsToTest = [ 26 | ["some_random_index"], 27 | [index.uid, "some_random_index"], 28 | const [], 29 | ]; 30 | for (final indexUids in uidsToTest) { 31 | final queryRes = await index.getTasks( 32 | params: TasksQuery( 33 | indexUids: indexUids, 34 | types: ['documentAdditionOrUpdate'], 35 | ), 36 | ); 37 | expect(queryRes.results.first.uid, task.uid); 38 | expect(queryRes.total, isPositive); 39 | } 40 | }); 41 | test('cancels given an input', () async { 42 | final date = DateTime.now(); 43 | final response = await client 44 | .cancelTasks( 45 | params: CancelTasksQuery(uids: [1, 2], beforeStartedAt: date), 46 | ) 47 | .waitFor(client: client); 48 | 49 | expect( 50 | response.details!['originalFilter'], 51 | '?beforeStartedAt=${Uri.encodeComponent(date.toUtc().toIso8601String())}&uids=1%2C2', 52 | ); 53 | }); 54 | 55 | test('deletes given an input', () async { 56 | final date = DateTime.now(); 57 | final response = await client 58 | .deleteTasks( 59 | params: DeleteTasksQuery(uids: [1, 2], beforeStartedAt: date), 60 | ) 61 | .waitFor(client: client); 62 | 63 | expect( 64 | response.details!['originalFilter'], 65 | '?beforeStartedAt=${Uri.encodeComponent(date.toUtc().toIso8601String())}&uids=1%2C2', 66 | ); 67 | }); 68 | }); 69 | 70 | test( 71 | 'code samples', 72 | () async { 73 | // #docregion async_guide_filter_by_date_1 74 | await client.getTasks( 75 | params: TasksQuery( 76 | afterEnqueuedAt: DateTime(2020, 10, 11, 11, 49, 53), 77 | ), 78 | ); 79 | // #enddocregion 80 | // #docregion async_guide_multiple_filters_1 81 | await client.getTasks( 82 | params: TasksQuery( 83 | indexUids: ['movies'], 84 | types: ['documentAdditionOrUpdate', 'documentDeletion'], 85 | statuses: ['processing'], 86 | ), 87 | ); 88 | // #enddocregion 89 | // #docregion async_guide_filter_by_ids_1 90 | await client.getTasks( 91 | params: TasksQuery( 92 | uids: [5, 10, 13], 93 | ), 94 | ); 95 | // #enddocregion 96 | // #docregion async_guide_filter_by_statuses_1 97 | await client.getTasks( 98 | params: TasksQuery( 99 | statuses: ['failed', 'canceled'], 100 | ), 101 | ); 102 | // #enddocregion 103 | // #docregion async_guide_filter_by_types_1 104 | await client.getTasks( 105 | params: TasksQuery( 106 | types: ['dumpCreation', 'indexSwap'], 107 | ), 108 | ); 109 | // #enddocregion 110 | // #docregion async_guide_filter_by_index_uids_1 111 | await client.getTasks(params: TasksQuery(indexUids: ['movies'])); 112 | // #enddocregion 113 | // #docregion delete_tasks_1 114 | await client.deleteTasks(params: DeleteTasksQuery(uids: [1, 2])); 115 | // #enddocregion 116 | // #docregion cancel_tasks_1 117 | await client.cancelTasks(params: CancelTasksQuery(uids: [1, 2])); 118 | // #enddocregion 119 | // #docregion async_guide_canceled_by_1 120 | await client.getTasks(params: TasksQuery(canceledBy: [9, 15])); 121 | // #enddocregion 122 | }, 123 | skip: true, 124 | ); 125 | } 126 | -------------------------------------------------------------------------------- /test/tenant_token_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart'; 2 | import 'package:meilisearch/meilisearch.dart'; 3 | import 'package:meilisearch/src/tenant_token.dart'; 4 | import 'package:test/test.dart'; 5 | 6 | import 'utils/books.dart'; 7 | import 'utils/client.dart'; 8 | import 'utils/wait_for.dart'; 9 | 10 | void main() { 11 | final Map searchRules = {"*": null}; 12 | 13 | group('Tenant Tokens', () { 14 | setUpClient(); 15 | 16 | final List possibleRules = [ 17 | {'*': {}}, 18 | {'*': null}, 19 | ['*'], 20 | { 21 | '*': {"filter": 'tag = Tale'} 22 | }, 23 | {"my_index": {}}, 24 | {"my_index": null}, 25 | ['my_index'], 26 | { 27 | "my_index": {"filter": 'tag = comedy AND book_id = 1'} 28 | } 29 | ]; 30 | 31 | group('client.generateTenantToken', () { 32 | test('decodes successfully using apiKey from instance', () { 33 | final token = client.generateTenantToken('uid', searchRules); 34 | 35 | expect( 36 | () => JWT.verify(token, SecretKey(client.apiKey!)), 37 | returnsNormally, 38 | ); 39 | }); 40 | 41 | test('decodes successfully using uid from param', () { 42 | final key = sha1RandomString(); 43 | final token = 44 | client.generateTenantToken('uid', searchRules, apiKey: key); 45 | 46 | expect(() => JWT.verify(token, SecretKey(key)), returnsNormally); 47 | }); 48 | 49 | test('throws InvalidApiKeyException if all given keys are invalid', () { 50 | final custom = MeiliSearchClient(testServer, null); 51 | 52 | expect( 53 | () => custom.generateTenantToken('uid', searchRules), 54 | throwsA(isA()), 55 | ); 56 | }); 57 | 58 | test('invokes search successfully with the new token', () async { 59 | final admKey = await client.createKey(indexes: ["*"], actions: ["*"]); 60 | final admClient = MeiliSearchClient(testServer, admKey.key); 61 | final index = await createBooksIndex(uid: 'my_index'); 62 | 63 | await index.updateFilterableAttributes(['tag', 'book_id']).waitFor( 64 | client: client, 65 | ); 66 | 67 | await Future.wait( 68 | possibleRules.map((rule) { 69 | final token = admClient.generateTenantToken(admKey.uid!, rule); 70 | final custom = MeiliSearchClient(testServer, token); 71 | return custom.index('my_index').search(''); 72 | }), 73 | ); 74 | }); 75 | }); 76 | 77 | group('tenant_token.generateToken', () { 78 | test('generates a signed token with given key', () { 79 | final key = sha1RandomString(); 80 | final uid = sha1RandomString(); 81 | final token = generateToken(uid, searchRules, key); 82 | 83 | expect(() => JWT.verify(token, SecretKey(key)), returnsNormally); 84 | expect(() => JWT.verify(token, SecretKey('not-the-same-key')), 85 | throwsA(isA())); 86 | }); 87 | 88 | test('does not generate a signed token without a key', () { 89 | expect(() => generateToken('', searchRules, ''), 90 | throwsA(isA())); 91 | }); 92 | 93 | test('generates a signed token with a given expiration', () { 94 | final key = sha1RandomString(); 95 | final uid = sha1RandomString(); 96 | final tomorrow = DateTime.now().add(Duration(days: 1)).toUtc(); 97 | final token = generateToken(uid, searchRules, key, expiresAt: tomorrow); 98 | 99 | expect(() => JWT.verify(token, SecretKey(key), checkExpiresIn: true), 100 | returnsNormally); 101 | }); 102 | 103 | test('generates a signed token without expiration', () { 104 | final key = sha1RandomString(); 105 | final uid = sha1RandomString(); 106 | final token = generateToken(uid, searchRules, key, expiresAt: null); 107 | 108 | expect(() => JWT.verify(token, SecretKey(key), checkExpiresIn: true), 109 | returnsNormally); 110 | }); 111 | 112 | test('throws ExpiredSignatureException when expiresAt is in the past', 113 | () { 114 | final key = sha1RandomString(); 115 | final uid = sha1RandomString(); 116 | final oldDate = DateTime.utc(1995, 12, 20); 117 | 118 | expect(() => generateToken(uid, searchRules, key, expiresAt: oldDate), 119 | throwsA(isA())); 120 | }); 121 | 122 | test('throws NotUTCException if expiresAt are in localDate', () { 123 | final key = sha1RandomString(); 124 | final uid = sha1RandomString(); 125 | final localDate = DateTime(2300, 1, 20); 126 | 127 | expect(() => generateToken(uid, searchRules, key, expiresAt: localDate), 128 | throwsA(isA())); 129 | }); 130 | test('contains custom claims', () { 131 | final key = sha1RandomString(); 132 | final uid = sha1RandomString(); 133 | final token = generateToken(uid, searchRules, key); 134 | final claims = JWT.verify(token, SecretKey(key)).payload; 135 | 136 | expect(claims['apiKeyUid'], equals(uid)); 137 | expect(claims['searchRules'], equals(searchRules)); 138 | }); 139 | }); 140 | }); 141 | } 142 | -------------------------------------------------------------------------------- /test/utils/books.dart: -------------------------------------------------------------------------------- 1 | import 'package:meilisearch/meilisearch.dart'; 2 | 3 | import 'books_data.dart'; 4 | import 'client.dart'; 5 | import 'wait_for.dart'; 6 | 7 | Future createDynamicBooksIndex({ 8 | String? uid, 9 | required int count, 10 | }) { 11 | return createIndexWithData( 12 | uid: uid, 13 | data: dynamicBooks(count), 14 | ); 15 | } 16 | 17 | Future createBooksIndex({String? uid}) async { 18 | return _createIndex(uid: uid); 19 | } 20 | 21 | Future createNestedBooksIndex({String? uid}) async { 22 | return _createIndex(uid: uid, isNested: true); 23 | } 24 | 25 | Future _createIndex({ 26 | String? uid, 27 | bool isNested = false, 28 | }) { 29 | return createIndexWithData( 30 | uid: uid, 31 | data: isNested ? nestedBooks : books, 32 | ); 33 | } 34 | 35 | Future createIndexWithData({ 36 | String? uid, 37 | required List> data, 38 | }) async { 39 | final index = client.index(uid ?? randomUid()); 40 | final response = await index.addDocuments(data).waitFor(client: client); 41 | 42 | if (response.status != 'succeeded') { 43 | throw Exception( 44 | 'Impossible to process test suite, the documents were not added into the index.', 45 | ); 46 | } 47 | return index; 48 | } 49 | 50 | class BookDto { 51 | final int bookId; 52 | final String title; 53 | final String? tag; 54 | 55 | const BookDto({ 56 | required this.bookId, 57 | required this.title, 58 | required this.tag, 59 | }); 60 | 61 | Map toMap() { 62 | return { 63 | kbookId: bookId, 64 | ktitle: title, 65 | ktag: tag, 66 | }; 67 | } 68 | 69 | factory BookDto.fromMap(Map map) { 70 | return BookDto( 71 | bookId: map[kbookId] as int, 72 | title: map[ktitle] as String, 73 | tag: map[ktag] as String?, 74 | ); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /test/utils/books_data.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | const kbookId = 'book_id'; 4 | const ktitle = 'title'; 5 | const ktag = 'tag'; 6 | const kid = 'id'; 7 | 8 | List> dynamicBooks(int count) { 9 | final tags = List.generate(4, (index) => "Tag $index"); 10 | return List.generate( 11 | count, 12 | (index) => { 13 | kbookId: index, 14 | ktitle: 'Book $index', 15 | ktag: tags[index % tags.length], 16 | }, 17 | ); 18 | } 19 | 20 | List> dynamicPartialBookUpdate(int count) { 21 | return List.generate( 22 | count, 23 | (index) { 24 | //shift index by 5 to simulate 5 non-existent book update 25 | index += 5; 26 | return { 27 | kbookId: index, 28 | ktitle: 'UPDATED Book $index', 29 | }; 30 | }, 31 | ); 32 | } 33 | 34 | final partialBookUpdate = [ 35 | {kbookId: 123, ktitle: 'UPDATED Pride and Prejudice'}, 36 | {kbookId: 1344, ktitle: 'UPDATED The Hobbit'}, 37 | //New book should be upserted 38 | {kbookId: 654, ktitle: 'UPDATED Not Le Petit Prince'}, 39 | ]; 40 | 41 | final books = [ 42 | {kbookId: 123, ktitle: 'Pride and Prejudice', ktag: 'Romance'}, 43 | {kbookId: 456, ktitle: 'Le Petit Prince', ktag: 'Tale'}, 44 | {kbookId: 1, ktitle: 'Alice In Wonderland', ktag: 'Tale'}, 45 | {kbookId: 1344, ktitle: 'The Hobbit', ktag: 'Epic fantasy'}, 46 | { 47 | kbookId: 4, 48 | ktitle: 'Harry Potter and the Half-Blood Prince', 49 | ktag: 'Epic fantasy' 50 | }, 51 | { 52 | kbookId: 42, 53 | ktitle: 'The Hitchhiker\'s Guide to the Galaxy', 54 | ktag: 'Epic fantasy' 55 | }, 56 | {kbookId: 9999, ktitle: 'The Hobbit', ktag: null}, 57 | ]; 58 | 59 | final vectorBooks = [ 60 | { 61 | "id": 0, 62 | "title": "Across The Universe", 63 | "_vectors": { 64 | "default": { 65 | "embeddings": [0, 0.8, -0.2], 66 | "regenerate": false, 67 | } 68 | } 69 | }, 70 | { 71 | "id": 1, 72 | "title": "All Things Must Pass", 73 | "_vectors": { 74 | "default": { 75 | "embeddings": [1, -0.2, 0], 76 | "regenerate": false, 77 | } 78 | } 79 | }, 80 | { 81 | "id": 2, 82 | "title": "And Your Bird Can Sing", 83 | "_vectors": { 84 | "default": { 85 | "embeddings": [-0.2, 4, 6], 86 | "regenerate": false, 87 | } 88 | } 89 | }, 90 | { 91 | "id": 3, 92 | "title": "The Matrix", 93 | "_vectors": { 94 | "default": { 95 | "embeddings": [5, -0.5, 0.3], 96 | "regenerate": false, 97 | } 98 | }, 99 | } 100 | ]; 101 | 102 | enum CSVHeaderTypes { 103 | string, 104 | boolean, 105 | number, 106 | unkown, 107 | } 108 | 109 | String dataAsCSV(List> data, {String delimiter = ','}) { 110 | final csvHeaders = {}; 111 | final csvDataBuffer = StringBuffer(); 112 | for (final element in data) { 113 | for (final entry in element.entries) { 114 | if (!csvHeaders.containsKey(entry.key)) { 115 | final value = entry.value; 116 | if (value != null) { 117 | csvHeaders[entry.key] = value is String 118 | ? CSVHeaderTypes.string 119 | : value is num 120 | ? CSVHeaderTypes.number 121 | : value is bool 122 | ? CSVHeaderTypes.boolean 123 | : CSVHeaderTypes.unkown; 124 | } 125 | } 126 | } 127 | } 128 | final csvHeaderEntries = csvHeaders.entries.toList(); 129 | 130 | data 131 | .map( 132 | (obj) => csvHeaderEntries 133 | .map((e) => e.key) 134 | .map((headerKey) => json.encode(obj[headerKey] ?? "")) 135 | .join(delimiter), 136 | ) 137 | .forEach(csvDataBuffer.writeln); 138 | 139 | final headerStr = csvHeaders.entries.map((header) { 140 | final headerType = header.value; 141 | final typeStr = headerType == CSVHeaderTypes.number 142 | ? ':number' 143 | : headerType == CSVHeaderTypes.boolean 144 | ? ':boolean' 145 | : null; 146 | return jsonEncode('${header.key}${typeStr ?? ""}'); 147 | }).join(delimiter); 148 | 149 | return '$headerStr\n${csvDataBuffer.toString()}'; 150 | } 151 | 152 | String dataAsNDJson(List> data) { 153 | return data.map(jsonEncode).join("\n"); 154 | } 155 | 156 | final nestedBooks = [ 157 | { 158 | kid: 1, 159 | ktitle: 'Pride and Prejudice', 160 | "info": { 161 | "comment": 'A great book', 162 | "reviewNb": 500, 163 | }, 164 | }, 165 | { 166 | kid: 2, 167 | ktitle: 'Le Petit Prince', 168 | "info": { 169 | "comment": 'A french book', 170 | "reviewNb": 600, 171 | }, 172 | }, 173 | { 174 | kid: 3, 175 | ktitle: 'Le Rouge et le Noir', 176 | "info": { 177 | "comment": 'Another french book', 178 | "reviewNb": 700, 179 | }, 180 | }, 181 | { 182 | kid: 4, 183 | ktitle: 'Alice In Wonderland', 184 | "comment": 'A weird book', 185 | "info": { 186 | "comment": 'A weird book', 187 | "reviewNb": 800, 188 | }, 189 | }, 190 | { 191 | kid: 5, 192 | ktitle: 'The Hobbit', 193 | "info": { 194 | "comment": 'An awesome book', 195 | "reviewNb": 900, 196 | }, 197 | }, 198 | { 199 | kid: 6, 200 | ktitle: 'Harry Potter and the Half-Blood Prince', 201 | "info": { 202 | "comment": 'The best book', 203 | "reviewNb": 1000, 204 | }, 205 | }, 206 | {kid: 7, ktitle: "The Hitchhiker's Guide to the Galaxy"}, 207 | ]; 208 | -------------------------------------------------------------------------------- /test/utils/client.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | import 'dart:math'; 4 | 5 | import 'package:crypto/crypto.dart'; 6 | import 'package:meilisearch/src/http_request.dart'; 7 | import 'package:pub_semver/pub_semver.dart'; 8 | import 'package:test/test.dart'; 9 | 10 | import '../models/test_client.dart'; 11 | 12 | HttpRequest get http => client.http; 13 | late TestMeiliSearchClient client; 14 | final random = Random(); 15 | 16 | const bool _kIsWeb = bool.fromEnvironment('dart.library.js_util'); 17 | String get testServer { 18 | const defaultUrl = 'http://localhost:7700'; 19 | if (_kIsWeb) { 20 | return defaultUrl; 21 | } else { 22 | return Platform.environment['MEILISEARCH_URL'] ?? defaultUrl; 23 | } 24 | } 25 | 26 | String get testApiKey { 27 | return 'masterKey'; 28 | } 29 | 30 | Version? get meiliServerVersion { 31 | const meilisearchVersionKey = 'MEILISEARCH_VERSION'; 32 | String? compileTimeValue = String.fromEnvironment(meilisearchVersionKey); 33 | if (compileTimeValue.isEmpty) { 34 | compileTimeValue = null; 35 | } 36 | if (_kIsWeb) { 37 | if (compileTimeValue != null) { 38 | return Version.parse(compileTimeValue); 39 | } 40 | } else { 41 | var str = Platform.environment[meilisearchVersionKey] ?? compileTimeValue; 42 | if (str != null && str.isNotEmpty) { 43 | return Version.parse(str); 44 | } 45 | } 46 | return null; 47 | } 48 | 49 | String? get openAiKey { 50 | const keyName = "OPEN_AI_API_KEY"; 51 | String? compileTimeValue = String.fromEnvironment(keyName); 52 | if (compileTimeValue.isEmpty) { 53 | compileTimeValue = null; 54 | } 55 | if (_kIsWeb) { 56 | return compileTimeValue; 57 | } else { 58 | return Platform.environment[keyName] ?? compileTimeValue; 59 | } 60 | } 61 | 62 | void setUpClient() { 63 | setUp(() { 64 | client = TestMeiliSearchClient(testServer, testApiKey); 65 | }); 66 | tearDown(() => client.disposeUsedResources()); 67 | } 68 | 69 | String randomUid([String prefix = 'index']) { 70 | return '${prefix}_${random.nextInt(9999)}'; 71 | } 72 | 73 | // Stolen from: https://www.kindacode.com/article/flutter-dart-ways-to-generate-random-strings/ 74 | String sha1RandomString() { 75 | final randomNumber = random.nextDouble(); 76 | final randomBytes = utf8.encode(randomNumber.toString()); 77 | 78 | return sha1.convert(randomBytes).toString(); 79 | } 80 | -------------------------------------------------------------------------------- /test/utils/wait_for.dart: -------------------------------------------------------------------------------- 1 | import 'package:meilisearch/meilisearch.dart'; 2 | import 'package:collection/collection.dart'; 3 | 4 | extension TaskWaiter on Task { 5 | Future waitFor({ 6 | required MeiliSearchClient client, 7 | Duration timeout = const Duration(seconds: 5), 8 | Duration interval = const Duration(milliseconds: 50), 9 | bool throwFailed = true, 10 | }) async { 11 | var endingTime = DateTime.now().add(timeout); 12 | 13 | while (DateTime.now().isBefore(endingTime)) { 14 | final task = await client.getTask(uid!); 15 | 16 | if (task.status != 'enqueued' && task.status != 'processing') { 17 | if (throwFailed && task.status != 'succeeded') { 18 | throw MeiliSearchApiException( 19 | "Task ($uid) failed", 20 | code: task.error?.code, 21 | link: task.error?.link, 22 | type: task.error?.type, 23 | ); 24 | } 25 | return task; 26 | } 27 | 28 | await Future.delayed(interval); 29 | } 30 | 31 | throw Exception('The task $uid timed out.'); 32 | } 33 | } 34 | 35 | extension TaskWaiterForLists on Iterable { 36 | Future> waitFor({ 37 | required MeiliSearchClient client, 38 | Duration timeout = const Duration(seconds: 20), 39 | Duration interval = const Duration(milliseconds: 50), 40 | bool throwFailed = true, 41 | }) async { 42 | final endingTime = DateTime.now().add(timeout); 43 | final originalUids = toList(); 44 | final remainingUids = map((e) => e.uid).nonNulls.toList(); 45 | final completedTasks = {}; 46 | final statuses = ['enqueued', 'processing']; 47 | 48 | while (DateTime.now().isBefore(endingTime)) { 49 | final taskRes = 50 | await client.getTasks(params: TasksQuery(uids: remainingUids)); 51 | final tasks = taskRes.results; 52 | final completed = tasks.where((e) => !statuses.contains(e.status)); 53 | if (throwFailed) { 54 | final failed = completed 55 | .firstWhereOrNull((element) => element.status != 'succeeded'); 56 | if (failed != null) { 57 | throw MeiliSearchApiException( 58 | "Task (${failed.uid}) failed", 59 | code: failed.error?.code, 60 | link: failed.error?.link, 61 | type: failed.error?.type, 62 | ); 63 | } 64 | } 65 | 66 | completedTasks.addEntries(completed.map((e) => MapEntry(e.uid!, e))); 67 | remainingUids 68 | .removeWhere((element) => completedTasks.containsKey(element)); 69 | 70 | if (remainingUids.isEmpty) { 71 | return originalUids.map((e) => completedTasks[e.uid]).nonNulls.toList(); 72 | } 73 | await Future.delayed(interval); 74 | } 75 | 76 | throw Exception('The tasks $originalUids timed out.'); 77 | } 78 | } 79 | 80 | extension TaskWaiterForFutures on Future { 81 | Future waitFor({ 82 | required MeiliSearchClient client, 83 | Duration timeout = const Duration(seconds: 5), 84 | Duration interval = const Duration(milliseconds: 50), 85 | bool throwFailed = true, 86 | }) async { 87 | return await (await this).waitFor( 88 | timeout: timeout, 89 | interval: interval, 90 | client: client, 91 | throwFailed: throwFailed, 92 | ); 93 | } 94 | } 95 | 96 | extension TaskWaiterForFutureList on Future> { 97 | Future> waitFor({ 98 | required MeiliSearchClient client, 99 | Duration timeout = const Duration(seconds: 20), 100 | Duration interval = const Duration(milliseconds: 50), 101 | bool throwFailed = true, 102 | }) async { 103 | return await (await this).waitFor( 104 | timeout: timeout, 105 | interval: interval, 106 | client: client, 107 | throwFailed: throwFailed, 108 | ); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /tool/.gitignore: -------------------------------------------------------------------------------- 1 | # https://dart.dev/guides/libraries/private-files 2 | # Created by `dart pub` 3 | .dart_tool/ 4 | -------------------------------------------------------------------------------- /tool/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the static analysis results for your project (errors, 2 | # warnings, and lints). 3 | # 4 | # This enables the 'recommended' set of lints from `package:lints`. 5 | # This set helps identify many issues that may lead to problems when running 6 | # or consuming Dart code, and enforces writing Dart using a single, idiomatic 7 | # style and format. 8 | # 9 | # If you want a smaller set of lints you can change this to specify 10 | # 'package:lints/core.yaml'. These are just the most critical lints 11 | # (the recommended set includes the core lints). 12 | # The core lints are also what is used by pub.dev for scoring packages. 13 | 14 | include: package:lints/recommended.yaml 15 | 16 | linter: 17 | rules: 18 | avoid_print: false 19 | -------------------------------------------------------------------------------- /tool/bin/meili.dart: -------------------------------------------------------------------------------- 1 | export 'package:meili_tool/src/main.dart'; 2 | -------------------------------------------------------------------------------- /tool/lib/src/command_base.dart: -------------------------------------------------------------------------------- 1 | import 'package:args/command_runner.dart'; 2 | import 'package:file/file.dart'; 3 | import 'package:meili_tool/src/result.dart'; 4 | import 'package:platform/platform.dart'; 5 | import 'package:path/path.dart' as p; 6 | 7 | abstract class MeiliCommandBase extends Command { 8 | final Directory packageDirectory; 9 | 10 | MeiliCommandBase( 11 | this.packageDirectory, { 12 | this.platform = const LocalPlatform(), 13 | }); 14 | 15 | /// The current platform. 16 | /// 17 | /// This can be overridden for testing. 18 | final Platform platform; 19 | 20 | /// A context that matches the default for [platform]. 21 | p.Context get path => platform.isWindows ? p.windows : p.posix; 22 | // Returns the relative path from [from] to [entity] in Posix style. 23 | /// 24 | /// This should be used when, for example, printing package-relative paths in 25 | /// status or error messages. 26 | String getRelativePosixPath( 27 | FileSystemEntity entity, { 28 | required Directory from, 29 | }) => 30 | p.posix.joinAll(path.split(path.relative(entity.path, from: from.path))); 31 | 32 | String get indentation => ' '; 33 | 34 | bool getBoolArg(String key) { 35 | return (argResults![key] as bool?) ?? false; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tool/lib/src/core.dart: -------------------------------------------------------------------------------- 1 | /// Error thrown when a command needs to exit with a non-zero exit code. 2 | /// 3 | /// While there is no specific definition of the meaning of different non-zero 4 | /// exit codes for this tool, commands should follow the general convention: 5 | /// 1: The command ran correctly, but found errors. 6 | /// 2: The command failed to run because the arguments were invalid. 7 | /// >2: The command failed to run correctly for some other reason. Ideally, 8 | /// each such failure should have a unique exit code within the context of 9 | /// that command. 10 | class ToolExit extends Error { 11 | /// Creates a tool exit with the given [exitCode]. 12 | ToolExit(this.exitCode); 13 | 14 | /// The code that the process should exit with. 15 | final int exitCode; 16 | } 17 | -------------------------------------------------------------------------------- /tool/lib/src/main.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io' as io; 2 | 3 | import 'package:args/command_runner.dart'; 4 | import 'package:file/file.dart'; 5 | import 'package:file/local.dart'; 6 | import 'package:meili_tool/src/output_utils.dart'; 7 | import 'package:meili_tool/src/result.dart'; 8 | 9 | import 'core.dart'; 10 | import 'update_samples_command.dart'; 11 | 12 | void main(List arguments) { 13 | const FileSystem fileSystem = LocalFileSystem(); 14 | final Directory scriptDir = 15 | fileSystem.file(io.Platform.script.toFilePath()).parent; 16 | final Directory toolsDir = 17 | scriptDir.basename == 'bin' ? scriptDir.parent : scriptDir.parent.parent; 18 | 19 | final Directory meilisearchDirectory = toolsDir.parent; 20 | 21 | final commandRunner = CommandRunner( 22 | 'dart run ./tool/bin/meili.dart', 'Productivity utils for meilisearch.') 23 | ..addCommand(UpdateSamplesCommand(meilisearchDirectory)); 24 | 25 | commandRunner.run(arguments).then((value) { 26 | if (value == null) { 27 | print('MUST output either a success or fail.'); 28 | assert(false); 29 | io.exit(255); 30 | } 31 | switch (value.state) { 32 | case RunState.succeeded: 33 | printSuccess('Success!'); 34 | break; 35 | case RunState.failed: 36 | printError('Failed!'); 37 | if (value.details.isNotEmpty) { 38 | printError(value.details.join('\n')); 39 | } 40 | io.exit(255); 41 | } 42 | }).catchError((Object e) { 43 | final ToolExit toolExit = e as ToolExit; 44 | int exitCode = toolExit.exitCode; 45 | // This should never happen; this check is here to guarantee that a ToolExit 46 | // never accidentally has code 0 thus causing CI to pass. 47 | if (exitCode == 0) { 48 | assert(false); 49 | exitCode = 255; 50 | } 51 | io.exit(exitCode); 52 | }, test: (Object e) => e is ToolExit); 53 | } 54 | -------------------------------------------------------------------------------- /tool/lib/src/output_utils.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'dart:io'; 6 | 7 | import 'package:colorize/colorize.dart'; 8 | import 'package:meta/meta.dart'; 9 | 10 | export 'package:colorize/colorize.dart' show Styles; 11 | 12 | /// True if color should be applied. 13 | /// 14 | /// Defaults to autodetecting stdout. 15 | @visibleForTesting 16 | bool useColorForOutput = stdout.supportsAnsiEscapes; 17 | 18 | String _colorizeIfAppropriate(String string, Styles color) { 19 | if (!useColorForOutput) { 20 | return string; 21 | } 22 | return Colorize(string).apply(color).toString(); 23 | } 24 | 25 | /// Prints [message] in green, if the environment supports color. 26 | void printSuccess(String message) { 27 | print(_colorizeIfAppropriate(message, Styles.GREEN)); 28 | } 29 | 30 | /// Prints [message] in yellow, if the environment supports color. 31 | void printWarning(String message) { 32 | print(_colorizeIfAppropriate(message, Styles.YELLOW)); 33 | } 34 | 35 | /// Prints [message] in red, if the environment supports color. 36 | void printError(String message) { 37 | print(_colorizeIfAppropriate(message, Styles.RED)); 38 | } 39 | 40 | /// Returns [message] with escapes to print it in [color], if the environment 41 | /// supports color. 42 | String colorizeString(String message, Styles color) { 43 | return _colorizeIfAppropriate(message, color); 44 | } 45 | -------------------------------------------------------------------------------- /tool/lib/src/result.dart: -------------------------------------------------------------------------------- 1 | /// Possible outcomes of a command run for a package. 2 | enum RunState { 3 | /// The command succeeded for the package. 4 | succeeded, 5 | 6 | /// The command failed for the package. 7 | failed, 8 | } 9 | 10 | /// The result of a [runForPackage] call. 11 | class PackageResult { 12 | /// A successful result. 13 | PackageResult.success() : this._(RunState.succeeded); 14 | 15 | /// A run that failed. 16 | /// 17 | /// If [errors] are provided, they will be listed in the summary, otherwise 18 | /// the summary will simply show that the package failed. 19 | PackageResult.fail([List errors = const []]) 20 | : this._(RunState.failed, errors); 21 | 22 | const PackageResult._(this.state, [this.details = const []]); 23 | 24 | /// The state the package run completed with. 25 | final RunState state; 26 | 27 | /// Information about the result: 28 | /// - For `succeeded`, this is empty. 29 | /// - For `skipped`, it contains a single entry describing why the run was 30 | /// skipped. 31 | /// - For `failed`, it contains zero or more specific error details to be 32 | /// shown in the summary. 33 | final List details; 34 | } 35 | -------------------------------------------------------------------------------- /tool/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: meili_tool 2 | description: | 3 | Productivity tools for meilisearch dart repository, 4 | most of this is inspired from the flutter packages repository https://github.com/flutter/packages/. 5 | version: 1.0.0 6 | repository: https://github.com/meilisearch/meilisearch-dart 7 | 8 | environment: 9 | sdk: '>=3.0.0 <4.0.0' 10 | 11 | # Add regular dependencies here. 12 | dependencies: 13 | lints: ^2.0.0 14 | test: ^1.21.0 15 | args: ^2.4.2 16 | cli_util: ^0.4.0 17 | file: ^7.0.0 18 | path: ^1.8.3 19 | platform: ^3.1.2 20 | collection: ^1.15.0 21 | colorize: ^3.0.0 22 | meta: ^1.10.0 23 | yaml: ^3.1.2 24 | yaml_edit: ^2.1.1 25 | http: ^1.1.0 26 | 27 | dev_dependencies: 28 | build_runner: ^2.0.3 29 | matcher: ^0.12.10 30 | mockito: '>=5.3.2 <=5.4.0' 31 | --------------------------------------------------------------------------------