diff --git a/.code-samples.meilisearch.yaml b/.code-samples.meilisearch.yaml index 991d978a..db106fe8 100644 --- a/.code-samples.meilisearch.yaml +++ b/.code-samples.meilisearch.yaml @@ -1,133 +1,55 @@ -# This code-samples file is used by the Meilisearch documentation +# This code-samples file is used by the Meilisearch documentation & landing page. # Every example written here will be automatically fetched by -# the documentation on build -# You can read more on https://github.com/meilisearch/documentation/tree/master/.vuepress/code-samples +# the documentation and the landing page on build --- -search_parameter_guide_show_ranking_score_1: |- - await client.index('movies').search('dragon', SearchQuery(showRankingScore: true)); -facet_search_2: |- - await client.index('books').updateFaceting(Faceting(sortFacetValuesBy: {'genres': 'count'})); -search_parameter_guide_attributes_to_search_on_1: |- - await client.index('movies').search('adventure', SearchQuery(attributesToSearchOn: ['overview'])); -get_documents_post_1: |- - await client.index('movies').getDocuments( - params: DocumentsQuery( - filter: '(rating > 3 AND (genres = Adventure OR genres = Fiction)) AND language = English', - fields: ['title', 'genres', 'rating', 'language'], - limit: 3 - ) - ); -delete_documents_by_filter_1: |- - await client.index('movies') - .deleteDocuments(DeleteDocumentsQuery(filter: 'genres = action OR genres = adventure')); -get_documents_1: |- - await client.index('movies') - .getDocuments(params: DocumentsQuery(limit: 2, filter: 'genres = action')); -multi_search_1: |- - await client.multiSearch(MultiSearchQuery(queries: [ - IndexSearchQuery( - query: "pooh", - indexUid: "movies", - limit: 5 - ), - IndexSearchQuery( - query: "nemo", - indexUid: "movies", - limit: 5 - ), - IndexSearchQuery( - query: "us", - indexUid: "movies_ratings" - ), - ])); -faceted_search_2: |- - await client.multiSearch(MultiSearchQuery(queries: [ - IndexSearchQuery( - indexUid: 'books', - facets: ['language', 'genres', 'author', 'format'], - filter: '(language = English AND language = French) OR genres = Fiction' - ), - IndexSearchQuery( - indexUid: 'books', - facets: ['language'], - filter: 'genres = Fiction' - ), - IndexSearchQuery( - indexUid: "books", - facets: ['genres'], - filter: 'language = English OR language = French' - ), - ])); -faceted_search_update_settings_1: |- - await client.index('movie_ratings').updateFilterableAttributes(['genres', 'rating', 'language']); -search_parameter_guide_facet_stats_1: |- - await client.index('movie_ratings').search('Batman', SearchQuery(facets: ['genres', 'rating'])); -faceted_search_1: |- - await client.index('books').search('', SearchQuery(facets: ['genres', 'rating', 'language'])); -filtering_guide_nested_1: |- - await client.index('movie_ratings').search('thriller', SearchQuery(filter: 'rating.users >= 90')); -sorting_guide_sort_nested_1: |- - await client.index('movie_ratings').search('thriller', SearchQuery(sort: ["rating.users:asc"])); -async_guide_filter_by_date_1: |- - await client.getTasks(params: TasksQuery(afterEnqueuedAt: DateTime(2020, 10, 11, 11, 49, 53))); -async_guide_multiple_filters_1: |- - await client.getTasks(params: TasksQuery(indexUids: ['movies'], types: ['documentAdditionOrUpdate','documentDeletion'], statuses: ['processing'])); -async_guide_filter_by_ids_1: |- - await client.getTasks(params: TasksQuery(uids: [5, 10, 13])); -async_guide_filter_by_statuses_1: |- - await client.getTasks(params: TasksQuery(statuses: ['failed', 'canceled'])); -async_guide_filter_by_types_1: |- - await client.getTasks(params: TasksQuery(types: ['dumpCreation', 'indexSwap'])); +search_parameter_guide_show_ranking_score_1: "await client\n .index('movies')\n .search('dragon', SearchQuery(showRankingScore: true));" +facet_search_1: "await client.index('books').facetSearch(\n FacetSearchQuery(\n facetQuery: 'fiction',\n facetName: 'genres',\n filter: 'rating > 3',\n ),\n );" +facet_search_2: "await client.index('books').updateFaceting(\n Faceting(\n sortFacetValuesBy: {\n 'genres': FacetingSortTypes.count,\n },\n ),\n );" +facet_search_3: "await client.index('books').facetSearch(\n FacetSearchQuery(\n facetQuery: 'c',\n facetName: 'genres',\n ),\n );" +search_parameter_guide_attributes_to_search_on_1: "await client.index('books').facetSearch(\n FacetSearchQuery(\n facetQuery: 'c',\n facetName: 'genres',\n ),\n );" +get_documents_post_1: "await client.index('movies').getDocuments(\n params: DocumentsQuery(\n filterExpression: Meili.and([\n 'language'.toMeiliAttribute().eq('English'.toMeiliValue()),\n Meili.and([\n 'rating'.toMeiliAttribute().gt(3.toMeiliValue()),\n Meili.or([\n 'genres'.toMeiliAttribute().eq('Adventure'.toMeiliValue()),\n 'genres'.toMeiliAttribute().eq('Fiction'.toMeiliValue()),\n ]),\n ]),\n ]),\n fields: ['title', 'genres', 'rating', 'language'],\n limit: 3,\n ),\n );" +delete_documents_by_filter_1: "await client.index('movies').deleteDocuments(\n DeleteDocumentsQuery(\n filterExpression: Meili.or([\n Meili.attr('genres').eq(Meili.value('action')),\n Meili.attr('genres').eq(Meili.value('adventure')),\n ]),\n ),\n );" +get_documents_1: "await client.index('movies').getDocuments(\n params: DocumentsQuery(\n limit: 2,\n filter: Meili.attr('genres').eq('action'.toMeiliValue()),\n ),\n );" +multi_search_1: "await client.multiSearch(MultiSearchQuery(queries: [\n IndexSearchQuery(query: 'pooh', indexUid: 'movies', limit: 5),\n IndexSearchQuery(query: 'nemo', indexUid: 'movies', limit: 5),\n IndexSearchQuery(query: 'us', indexUid: 'movies_ratings'),\n]));" +faceted_search_update_settings_1: "await client\n .index('movie_ratings')\n .updateFilterableAttributes(['genres', 'rating', 'language']);" +search_parameter_guide_facet_stats_1: "await client\n .index('movie_ratings')\n .search('Batman', SearchQuery(facets: ['genres', 'rating']));" +faceted_search_1: "await client\n .index('books')\n .search('', SearchQuery(facets: ['genres', 'rating', 'language']));" +filtering_guide_nested_1: "await client.index('movie_ratings').search(\n 'thriller',\n SearchQuery(\n filterExpression: Meili.gte(\n \/\/or Meili.attr('rating.users')\n \/\/or 'rating.users'.toMeiliAttribute()\n Meili.attrFromParts(['rating', 'users']),\n Meili.value(90),\n ),\n ),\n );" +sorting_guide_sort_nested_1: "await client\n .index('movie_ratings')\n .search('thriller', SearchQuery(sort: ['rating.users:asc']));" +async_guide_filter_by_date_1: "await client.getTasks(\n params: TasksQuery(\n afterEnqueuedAt: DateTime(2020, 10, 11, 11, 49, 53),\n ),\n);" +async_guide_multiple_filters_1: "await client.getTasks(\n params: TasksQuery(\n indexUids: ['movies'],\n types: ['documentAdditionOrUpdate', 'documentDeletion'],\n statuses: ['processing'],\n ),\n);" +async_guide_filter_by_ids_1: "await client.getTasks(\n params: TasksQuery(\n uids: [5, 10, 13],\n ),\n);" +async_guide_filter_by_statuses_1: "await client.getTasks(\n params: TasksQuery(\n statuses: ['failed', 'canceled'],\n ),\n);" +async_guide_filter_by_types_1: "await client.getTasks(\n params: TasksQuery(\n types: ['dumpCreation', 'indexSwap'],\n ),\n);" async_guide_filter_by_index_uids_1: |- await client.getTasks(params: TasksQuery(indexUids: ['movies'])); delete_tasks_1: |- await client.deleteTasks(params: DeleteTasksQuery(uids: [1, 2])); cancel_tasks_1: |- await client.cancelTasks(params: CancelTasksQuery(uids: [1, 2])); -async_guide_canceled_by: |- - await client.getTasks(params: TasksQuery(canceledBy: [9])); -swap_indexes_1: |- - await client.swapIndexes([SwapIndex(['indexA', 'indexB']), SwapIndex(['indexX', 'indexY'])]); -search_parameter_guide_hitsperpage_1: |- - await client.index('movies').search('', SearchQuery(hitsPerPage: 15)) as PaginatedSearchResult; -search_parameter_guide_page_1: |- - await client.index('movies').search('', SearchQuery(page: 2)) as PaginatedSearchResult; -synonyms_guide_1: |- - await client.index('movies').updateSynonyms({ - 'great': ['fantastic'], - 'fantastic': ['great'] - }); -date_guide_index_1: |- - import 'dart:io'; - import 'dart:convert'; - - final json = await File('games.json').readAsString(); - - await client.index('games').addDocumentsJson(json); -date_guide_filterable_attributes_1: |- - await client.index('games').updateFilterableAttributes(['release_timestamp']); -date_guide_filter_1: |- - await client.index('games').search('', - SearchQuery(filter: - 'release_timestamp >= 1514761200 AND release_timestamp < 1672527600')); -date_guide_sortable_attributes_1: |- - await client.index('games').updateSortableAttributes(['release_timestamp']); -date_guide_sort_1: |- - await client.index('games').search('', SearchQuery(sort: ['release_timestamp:desc'])); +async_guide_canceled_by_1: |- + await client.getTasks(params: TasksQuery(canceledBy: [9, 15])); +swap_indexes_1: "await client.swapIndexes([\n SwapIndex(['indexA', 'indexB']),\n SwapIndex(['indexX', 'indexY']),\n]);" +search_parameter_guide_hitsperpage_1: "await client\n .index('movies')\n .search('', SearchQuery(hitsPerPage: 15))\n .asPaginatedResult();" +search_parameter_guide_page_1: "await client\n .index('movies')\n .search('', SearchQuery(page: 2))\n .asPaginatedResult();" +synonyms_guide_1: "await client.index('movies').updateSynonyms({\n 'great': ['fantastic'],\n 'fantastic': ['great'],\n});" +date_guide_index_1: "\/\/import 'dart:io';\n\/\/import 'dart:convert';\nfinal json = await File('games.json').readAsString();\nawait client.index('games').addDocumentsJson(json);" +date_guide_filterable_attributes_1: "await client\n .index('games')\n .updateFilterableAttributes(['release_timestamp']);" +date_guide_filter_1: "await client.index('games').search(\n '',\n SearchQuery(\n filterExpression: Meili.and([\n Meili.gte(\n 'release_timestamp'.toMeiliAttribute(),\n Meili.value(DateTime(2017, 12, 31, 23, 0)),\n ),\n Meili.lt(\n 'release_timestamp'.toMeiliAttribute(),\n Meili.value(DateTime(2022, 12, 31, 23, 0)),\n ),\n ]),\n ),\n );" +date_guide_sortable_attributes_1: "await client\n .index('games')\n .updateSortableAttributes(['release_timestamp']);" +date_guide_sort_1: "await client\n .index('games')\n .search('', SearchQuery(sort: ['release_timestamp:desc']));" get_all_tasks_paginating_1: |- await client.getTasks(params: TasksQuery(limit: 2, from: 10)); get_all_tasks_paginating_2: |- await client.getTasks(params: TasksQuery(limit: 2, from: 8)); get_pagination_settings_1: |- await client.index('movies').getPagination(); -update_pagination_settings_1: |- - await client.index('books').updatePagination(Pagination(maxTotalHits: 100)); +update_pagination_settings_1: "await client\n .index('books')\n .updatePagination(Pagination(maxTotalHits: 100));" reset_pagination_settings_1: |- await client.index('movies').resetPagination(); get_faceting_settings_1: |- await client.index('movies').getFaceting(); -update_faceting_settings_1: |- - await client.index('books').updateFaceting(Faceting(maxValuesPerFacet: 2, sortFacetValuesBy: {'*': 'alpha', 'genres': 'count'})); +update_faceting_settings_1: "await client.index('books').updateFaceting(Faceting(\n maxValuesPerFacet: 2,\n sortFacetValuesBy: {\n '*': FacetingSortTypes.alpha,\n 'genres': FacetingSortTypes.count\n }));" reset_faceting_settings_1: |- await client.index('movies').resetFaceting(); get_one_index_1: |- @@ -140,8 +62,7 @@ update_an_index_1: |- await client.index('movies').update(primaryKey: 'id'); delete_an_index_1: |- await client.index('movies').delete(); -get_one_document_1: |- - await client.index('movies').getDocument(25684, fields: ['id', 'title', 'poster', 'release_date']); +get_one_document_1: "await client.index('movies').getDocument(25684,\n fields: ['id', 'title', 'poster', 'release_date']);" add_or_replace_documents_1: |- await client.index('movies').addDocuments([ { @@ -154,65 +75,26 @@ add_or_replace_documents_1: |- 'release_date': '2019-03-23' } ]); -add_or_update_documents_1: |- - await client.index('movies').updateDocuments([ - { - 'id': 287947, - 'title': 'Shazam ⚡️', - 'genres': 'comedy' - } - ]); +add_or_update_documents_1: "await client.index('movies').updateDocuments([\n {\n 'id': 287947,\n 'title': 'Shazam ⚡️',\n 'genres': 'comedy',\n }\n]);" delete_all_documents_1: |- await client.index('movies').deleteAllDocuments(); delete_one_document_1: |- await client.index('movies').deleteDocument(25684); -delete_documents_by_batch_1: |- - await client.index('movies') - .deleteDocuments(DeleteDocumentsQuery(ids: [23488, 153738, 437035, 363869])); +delete_documents_by_batch_1: "await client.index('movies').deleteDocuments(\n DeleteDocumentsQuery(\n ids: [23488, 153738, 437035, 363869],\n ),\n );" search_post_1: |- await client.index('movies').search('American ninja'); get_task_1: |- await client.getTask(1); get_all_tasks_1: |- await client.getTasks(); -get_keys_1: |- - await client.getKeys(); get_settings_1: |- await client.index('movies').getSettings(); -update_settings_1: |- - await client.index('movies').updateSettings(IndexSettings( - rankingRules: [ - 'words', - 'typo', - 'proximity', - 'attribute', - 'sort', - 'exactness', - 'release_date:desc', - 'rank:desc' - ], - distinctAttribute: 'movie_id', - searchableAttributes: ['title', 'overview', 'genres'], - displayedAttributes: ['title', 'overview', 'genres', 'release_date'], - stopWords: ['the', 'a', 'an'], - sortableAttributes: ['title', 'release_date'], - synonyms: { - 'wolverine': ['xmen', 'logan'], - 'logan': ['wolverine'], - }, - )); +update_settings_1: "await client.index('movies').updateSettings(\n IndexSettings(\n rankingRules: [\n 'words',\n 'typo',\n 'proximity',\n 'attribute',\n 'sort',\n 'exactness',\n 'release_date:desc',\n 'rank:desc'\n ],\n distinctAttribute: 'movie_id',\n searchableAttributes: ['title', 'overview', 'genres'],\n displayedAttributes: [\n 'title',\n 'overview',\n 'genres',\n 'release_date'\n ],\n stopWords: ['the', 'a', 'an'],\n sortableAttributes: ['title', 'release_date'],\n synonyms: {\n 'wolverine': ['xmen', 'logan'],\n 'logan': ['wolverine'],\n },\n ),\n );" reset_settings_1: |- await client.index('movies').resetSettings(); get_synonyms_1: |- await client.index('movies').getSynonyms(); -update_synonyms_1: |- - await client.index('movies').updateSynonyms( - { - 'wolverine': ['xmen', 'logan'], - 'logan': ['wolverine', 'xmen'], - 'wow': ['world of warcraft'] - } - ); +update_synonyms_1: "await client.index('movies').updateSynonyms({\n 'wolverine': ['xmen', 'logan'],\n 'logan': ['wolverine', 'xmen'],\n 'wow': ['world of warcraft'],\n});" reset_synonyms_1: |- await client.index('movies').resetSynonyms(); get_stop_words_1: |- @@ -223,17 +105,7 @@ reset_stop_words_1: |- await client.index('movies').resetStopWords(); get_ranking_rules_1: |- await client.index('movies').getRankingRules(); -update_ranking_rules_1: |- - await client.index('movies').updateRankingRules([ - 'words', - 'typo', - 'proximity', - 'attribute', - 'sort', - 'exactness', - 'release_date:asc', - 'rank:desc' - ]); +update_ranking_rules_1: "await client.index('movies').updateRankingRules([\n 'words',\n 'typo',\n 'proximity',\n 'attribute',\n 'sort',\n 'exactness',\n 'release_date:asc',\n 'rank:desc',\n]);" reset_ranking_rules_1: |- await client.index('movies').resetRankingRules(); get_distinct_attribute_1: |- @@ -244,34 +116,32 @@ reset_distinct_attribute_1: |- await client.index('shoes').resetDistinctAttribute(); get_filterable_attributes_1: |- await client.index('movies').getFilterableAttributes(); -update_filterable_attributes_1: |- - await client.index('movies').updateFilterableAttributes([ - 'genres', - 'director' - ]); +update_filterable_attributes_1: "await client\n .index('movies')\n .updateFilterableAttributes(['genres', 'director']);" reset_filterable_attributes_1: |- await client.index('movies').resetFilterableAttributes(); get_searchable_attributes_1: |- await client.index('movies').getSearchableAttributes(); -update_searchable_attributes_1: |- - await client.index('movies').updateSearchableAttributes([ - 'title', - 'overview', - 'genres' - ]); +update_searchable_attributes_1: "await client\n .index('movies')\n .updateSearchableAttributes(['title', 'overview', 'genres']);" reset_searchable_attributes_1: |- await client.index('movies').resetSearchableAttributes(); get_displayed_attributes_1: |- await client.index('movies').getDisplayedAttributes(); -update_displayed_attributes_1: |- - await client.index('movies').updateDisplayedAttributes([ - 'title', - 'overview', - 'genres', - 'release_date' - ]); +update_displayed_attributes_1: "await client.index('movies').updateDisplayedAttributes([\n 'title',\n 'overview',\n 'genres',\n 'release_date',\n]);" reset_displayed_attributes_1: |- await client.index('movies').resetDisplayedAttributes(); +get_typo_tolerance_1: |- + await client.index('books').getTypoTolerance(); +update_typo_tolerance_1: |- + final toUpdate = TypoTolerance( + minWordSizeForTypos: MinWordSizeForTypos( + oneTypo: 4, + twoTypos: 10, + ), + disableOnAttributes: ['title'], + ); + await client.index('books').updateTypoTolerance(toUpdate); +reset_typo_tolerance_1: |- + await client.index('books').resetTypoTolerance(); get_index_stats_1: |- await client.index('movies').getStats(); get_indexes_stats_1: |- @@ -282,58 +152,24 @@ get_version_1: |- await client.getVersion(); distinct_attribute_guide_1: |- await client.index('jackets').updateDistinctAttribute('product_id'); -field_properties_guide_searchable_1: |- - await client.index('movies').updateSearchableAttributes(['title', 'overview', 'genres']); -field_properties_guide_displayed_1: |- - await client.index('movies').updateDisplayedAttributes(['title', 'overview', 'genres', 'release_date']); -filtering_guide_1: |- - await client - .index('movie_ratings') - .search('Avengers', SearchQuery(filter: 'release_date > 795484800')); -filtering_guide_2: |- - await client.index('movie_ratings') - .search('Batman', - SearchQuery(filter: 'release_date > 795484800 AND (director = "Tim Burton" OR director = "Christopher Nolan")' - ) - ); -filtering_guide_3: |- - await client.index('movie_ratings').search('Planet of the Apes', - SearchQuery(filter: 'release_date > 1577884550 AND (NOT director = "Tim Burton")')); +field_properties_guide_searchable_1: "await client\n .index('movies')\n .updateSearchableAttributes(['title', 'overview', 'genres']);" +field_properties_guide_displayed_1: "await client.index('movies').updateDisplayedAttributes([\n 'title',\n 'overview',\n 'genres',\n 'release_date',\n]);" +filtering_guide_1: "await client.index('movie_ratings').search(\n 'Avengers',\n SearchQuery(\n filterExpression: Meili.gt(\n Meili.attr('release_date'),\n DateTime.utc(1995, 3, 18).toMeiliValue(),\n ),\n ),\n );" +filtering_guide_2: "await client.index('movie_ratings').search(\n 'Batman',\n SearchQuery(\n filterExpression: Meili.and([\n Meili.attr('release_date')\n .gt(DateTime.utc(1995, 3, 18).toMeiliValue()),\n Meili.or([\n 'director'.toMeiliAttribute().eq('Tim Burton'.toMeiliValue()),\n 'director'\n .toMeiliAttribute()\n .eq('Christopher Nolan'.toMeiliValue()),\n ]),\n ]),\n ),\n );" +filtering_guide_3: "await client.index('movie_ratings').search(\n 'Planet of the Apes',\n SearchQuery(\n filterExpression: Meili.and([\n Meili.attr('release_date')\n .gt(DateTime.utc(2020, 1, 1, 13, 15, 50).toMeiliValue()),\n Meili.not(\n Meili.attr('director').eq(\"Tim Burton\".toMeiliValue()),\n ),\n ]),\n ),\n );" search_parameter_guide_query_1: |- await client.index('movies').search('shifu'); search_parameter_guide_offset_1: |- await client.index('movies').search('shifu', SearchQuery(offset: 1)); search_parameter_guide_limit_1: |- await client.index('movies').search('shifu', SearchQuery(limit: 2)); -search_parameter_guide_matching_strategy_1: |- - await client.index('movies').search('big fat liar', SearchQuery(matchingStrategy: MatchingStrategy.last)); -search_parameter_guide_matching_strategy_2: |- - await client.index('movies').search('big fat liar', SearchQuery(matchingStrategy: MatchingStrategy.all)); -search_parameter_guide_retrieve_1: |- - await client - .index('movies') - .search('shifu', SearchQuery(attributesToRetrieve: ['overview', 'title'])); -search_parameter_guide_crop_1: |- - await client - .index('movies') - .search('shifu', SearchQuery(attributesToCrop: ['overview'], cropLength: 5)); -search_parameter_guide_highlight_1: |- - await client - .index('movies') - .search('winter feast', SearchQuery(attributesToHighlight: ['overview'])); -search_parameter_guide_show_matches_position_1: |- - await client.index('movies').search('winter feast', SearchQuery(showMatchesPosition: true)); -add_movies_json_1: |- - import 'dart:io'; - import 'dart:convert'; - - final json = await File('movies.json').readAsString(); - - await client.index('movies').addDocumentsJson(json); -documents_guide_add_movie_1: |- - await client.index('movies').addDocuments([ - { 'movie_id': '123sq178', 'title': 'Amélie Poulain' } - ]); +search_parameter_guide_matching_strategy_1: "await client.index('movies').search(\n 'big fat liar', SearchQuery(matchingStrategy: MatchingStrategy.last));" +search_parameter_guide_matching_strategy_2: "await client.index('movies').search(\n 'big fat liar', SearchQuery(matchingStrategy: MatchingStrategy.all));" +search_parameter_guide_retrieve_1: "await client.index('movies').search(\n 'shifu', SearchQuery(attributesToRetrieve: ['overview', 'title']));" +search_parameter_guide_crop_1: "await client.index('movies').search(\n 'shifu', SearchQuery(attributesToCrop: ['overview'], cropLength: 5));" +search_parameter_guide_highlight_1: "await client.index('movies').search(\n 'winter feast', SearchQuery(attributesToHighlight: ['overview']));" +search_parameter_guide_show_matches_position_1: "await client\n .index('movies')\n .search('winter feast', SearchQuery(showMatchesPosition: true));" +add_movies_json_1: "\/\/ import 'dart:io';\n\/\/ import 'dart:convert';\nfinal json = await File('movies.json').readAsString();\nawait client.index('movies').addDocumentsJson(json);" primary_field_guide_create_index_primary_key: |- await client.createIndex('books', primaryKey: 'reference_number'); primary_field_guide_update_document_primary_key: |- @@ -348,147 +184,79 @@ primary_field_guide_add_document_primary_key: |- 'price': 5.00 } ], primaryKey: 'reference_number'); -getting_started_add_documents_md: |- - ```bash - dart pub add meilisearch - ``` - - ```dart - import 'package:meilisearch/meilisearch.dart'; - import 'dart:io'; - import 'dart:convert'; - - var client = MeiliSearchClient('http://localhost:7700', 'aSampleMasterKey'); - - final json = await File('movies.json').readAsString(); - - await client.index('movies').addDocumentsJson(json); - ``` - - [About this SDK](https://github.com/meilisearch/meilisearch-dart/) -getting_started_search_md: |- - ```dart - await client.index('movies').search('botman'); - ``` - - [About this SDK](https://github.com/meilisearch/meilisearch-dart/) -getting_started_update_ranking_rules: |- - await client.index('movies').updateRankingRules([ - 'exactness', - 'words', - 'typo', - 'proximity', - 'attribute', - 'sort', - 'release_date:asc', - 'rank:desc' - ]); -getting_started_update_searchable_attributes: |- - await client.index('movies').updateSearchableAttributes([ - 'title' - ]); +typo_tolerance_guide_1: |- + final toUpdate = TypoTolerance(enabled: false); + await client.index('movies').updateTypoTolerance(toUpdate); +typo_tolerance_guide_2: |- + final toUpdate = TypoTolerance( + disableOnAttributes: ['title'], + ); + await client.index('movies').updateTypoTolerance(toUpdate); +typo_tolerance_guide_3: |- + final toUpdate = TypoTolerance( + disableOnWords: ['shrek'], + ); + await client.index('movies').updateTypoTolerance(toUpdate); +typo_tolerance_guide_4: |- + final toUpdate = TypoTolerance( + minWordSizeForTypos: MinWordSizeForTypos( + oneTypo: 4, + twoTypos: 10, + ), + ); + await client.index('movies').updateTypoTolerance(toUpdate); +getting_started_add_documents_md: "```bash\ndart pub add meilisearch\n```\n```dart\nimport 'package:meilisearch\/meilisearch.dart';\nimport 'dart:io';\nimport 'dart:convert';\nvar client = MeiliSearchClient('http:\/\/localhost:7700', 'aSampleMasterKey');\nfinal json = await File('movies.json').readAsString();\nawait client.index('movies').addDocumentsJson(json);\n```\n[About this SDK](https:\/\/github.com\/meilisearch\/meilisearch-dart\/)" +getting_started_search_md: "```dart\nawait client.index('movies').search('botman');\n```\n[About this SDK](https:\/\/github.com\/meilisearch\/meilisearch-dart\/)" +getting_started_update_ranking_rules: "await client.index('movies').updateRankingRules([\n 'exactness',\n 'words',\n 'typo',\n 'proximity',\n 'attribute',\n 'sort',\n 'release_date:asc',\n 'rank:desc',\n]);" +getting_started_update_searchable_attributes: "await client.index('movies').updateSearchableAttributes(['title']);" getting_started_update_stop_words: |- await client.index('movies').updateStopWords(['the']); getting_started_check_task_status: |- await client.getTask(0); -getting_started_synonyms: |- - await client.index('movies').updateSynonyms({ - 'winnie': ['piglet'], - 'piglet': ['winnie'] - }); -getting_started_update_displayed_attributes: |- - await client.index('movies').updateDisplayedAttributes([ - 'title', - 'overview', - 'poster' - ]); -getting_started_communicating_with_a_protected_instance: |- - var client = MeiliSearchClient('http://localhost:7700', 'apiKey'); - await client.index('movies').search(''); -getting_started_add_meteorites: |- - final json = await File('meteorites.json').readAsString(); - - await client.index('meteorites').addDocumentsJson(json); -getting_started_configure_settings: |- - await client.index('meteorites').updateSettings(IndexSettings( - filterableAttributes: ['mass', '_geo'], - sortableAttributes: ['mass', '_geo'] - )); -getting_started_geo_radius: |- - await client.index('meteorites').search('', SearchQuery(filter: '_geoRadius(46.9480, 7.4474, 210000)')); -getting_started_geo_point: |- - await client.index('meteorites').search('', SearchQuery(sort: ['_geoPoint(48.8583701, 2.2922926):asc'])); -getting_started_sorting: |- - await client.index('meteorites').search('', SearchQuery(sort: ['mass:asc'], filter: 'mass < 200')); -getting_started_filtering: |- - await client.index('meteorites').search('', SearchQuery(filter: 'mass < 200')); -getting_started_faceting: |- - await client.index('books').updateFaceting(Faceting(maxValuesPerFacet: 2, sortFacetValuesBy: {'*': 'count'})); +getting_started_synonyms: "await client.index('movies').updateSynonyms({\n 'winnie': ['piglet'],\n 'piglet': ['winnie'],\n});" +getting_started_update_displayed_attributes: "await client\n .index('movies')\n .updateDisplayedAttributes(['title', 'overview', 'poster']);" +getting_started_add_meteorites: "final json = await File('meteorites.json').readAsString();\nawait client.index('meteorites').addDocumentsJson(json);" +getting_started_configure_settings: "await client.index('meteorites').updateSettings(IndexSettings(\n filterableAttributes: ['mass', '_geo'],\n sortableAttributes: ['mass', '_geo']));" +getting_started_geo_radius: "await client.index('meteorites').search(\n '',\n SearchQuery(\n filterExpression: Meili.geoRadius(\n (lat: 46.9480, lng: 7.4474),\n 210000,\n ),\n ),\n );" +getting_started_geo_point: "await client.index('meteorites').search(\n '', SearchQuery(sort: ['_geoPoint(48.8583701, 2.2922926):asc']));" +getting_started_sorting: "await client.index('meteorites').search(\n '',\n SearchQuery(\n sort: ['mass:asc'],\n filterExpression: Meili.attr('mass').lt(200.toMeiliValue()),\n ),\n );" +getting_started_filtering: "await client\n .index('meteorites')\n .search('', SearchQuery(filter: 'mass < 200'));" +getting_started_faceting: "await client.index('books').updateFaceting(Faceting(\n maxValuesPerFacet: 2,\n sortFacetValuesBy: {'*': FacetingSortTypes.count}));" getting_started_typo_tolerance: |- final toUpdate = TypoTolerance( minWordSizeForTypos: MinWordSizeForTypos(oneTypo: 4), ); await client.index('movies').updateTypoTolerance(toUpdate); -getting_started_pagination: |- - await client.index('books').updatePagination(Pagination(maxTotalHits: 500)); +getting_started_pagination: "await client\n .index('books')\n .updatePagination(Pagination(maxTotalHits: 500));" filtering_update_settings_1: |- await client.index('movies').updateFilterableAttributes([ 'director', 'genres', ]); -faceted_search_facets_1: |- - await client.index('movies').search('Batman', SearchQuery(facets: ['genres'])); -faceted_search_walkthrough_filter_1: |- - await client.index('movies').search('thriller', SearchQuery(filter: [ - ['genres = Horror', 'genres = Mystery'], - 'director = "Jordan Peele"' - ])); +faceted_search_walkthrough_filter_1: "await client.index('movies').search(\n 'thriller',\n SearchQuery(filter: [\n ['genres = Horror', 'genres = Mystery'],\n 'director = \"Jordan Peele\"'\n ]));" post_dump_1: |- await client.createDump(); phrase_search_1: |- await client.index('movies').search('"african american" horror'); sorting_guide_update_sortable_attributes_1: |- await client.index('books').updateSortableAttributes(['author', 'price']); -sorting_guide_update_ranking_rules_1: |- - await client.index('books').updateRankingRules([ - 'words', - 'sort', - 'typo', - 'proximity', - 'attribute', - 'exactness' - ]); -sorting_guide_sort_parameter_1: |- - await client.index('books').search('science fiction', SearchQuery(sort: ['price:asc'])); -sorting_guide_sort_parameter_2: |- - await client.index('books').search('butler', SearchQuery(sort: ['author:desc'])); +sorting_guide_update_ranking_rules_1: "await client.index('books').updateRankingRules(\n ['words', 'sort', 'typo', 'proximity', 'attribute', 'exactness']);" +sorting_guide_sort_parameter_1: "await client\n .index('books')\n .search('science fiction', SearchQuery(sort: ['price:asc']));" +sorting_guide_sort_parameter_2: "await client\n .index('books')\n .search('butler', SearchQuery(sort: ['author:desc']));" get_sortable_attributes_1: |- await client.index('books').getSortableAttributes(); -update_sortable_attributes_1: |- - await client.index('books').updateSortableAttributes([ - 'price', - 'author' - ]); +update_sortable_attributes_1: "await client.index('books').updateSortableAttributes(['price', 'author']);" reset_sortable_attributes_1: |- await client.index('books').resetSortableAttributes(); -search_parameter_guide_sort_1: |- - await client.index('books').search('science fiction', SearchQuery(sort: ['price:asc'])); +search_parameter_guide_sort_1: "await client\n .index('books')\n .search('science fiction', SearchQuery(sort: ['price:asc']));" geosearch_guide_filter_settings_1: |- await client.index('restaurants').updateFilterableAttributes(['_geo']); -geosearch_guide_filter_usage_1: |- - await await client - .index('restaurants') - .search('', SearchQuery(filter: '_geoRadius(45.472735, 9.184019, 2000)')); -geosearch_guide_filter_usage_2: |- - await await client - .index('restaurants') - .search('', SearchQuery(filter: '_geoRadius(45.472735, 9.184019, 2000) AND type = pizza')); +geosearch_guide_filter_usage_1: "await client.index('restaurants').search(\n '',\n SearchQuery(\n filterExpression: Meili.geoRadius(\n (lat: 45.472735, lng: 9.184019),\n 2000,\n ),\n ),\n );" +geosearch_guide_filter_usage_2: "await client.index('restaurants').search(\n '',\n SearchQuery(\n filterExpression: Meili.and([\n Meili.geoRadius(\n (lat: 45.472735, lng: 9.184019),\n 2000,\n ),\n Meili.attr('type').eq('pizza'.toMeiliValue())\n ]),\n ),\n );" geosearch_guide_sort_settings_1: |- await client.index('restaurants').updateSortableAttributes(['_geo']); -geosearch_guide_sort_usage_1: |- - await client.index('restaurants').search('', SearchQuery(sort: ['_geoPoint(48.8561446, 2.2978204):asc'])); -geosearch_guide_sort_usage_2: |- - await client.index('restaurants').search('', SearchQuery(sort: ['_geoPoint(48.8561446, 2.2978204):asc', 'rating:desc'])); +geosearch_guide_sort_usage_1: "await client.index('restaurants').search(\n '', SearchQuery(sort: ['_geoPoint(48.8561446, 2.2978204):asc']));" +geosearch_guide_sort_usage_2: "await client.index('restaurants').search(\n '',\n SearchQuery(\n sort: ['_geoPoint(48.8561446, 2.2978204):asc', 'rating:desc']));" authorization_header_1: |- var client = MeiliSearchClient('http://localhost:7700', 'masterKey'); await client.getKeys(); @@ -496,27 +264,14 @@ get_one_key_1: |- await client.getKey('6062abda-a5aa-4414-ac91-ecd7944c0f8d'); get_all_keys_1: |- await client.getKeys(params: KeysQuery(limit: 3)); -create_a_key_1: |- - await client.createKey( - description: 'Add documents: Products API key', - actions: ['documents.add'], - indexes: ['products'], - expiresAt: DateTime(2042, 04, 02) - ); -update_a_key_1: |- - await client.updateKey( - '6062abda-a5aa-4414-ac91-ecd7944c0f8d', - description: 'Manage documents: Products/Reviews API key', - name: 'Products/Reviews API key' - ); +create_a_key_1: "await client.createKey(\n description: 'Add documents: Products API key',\n actions: ['documents.add'],\n indexes: ['products'],\n expiresAt: DateTime(2042, 04, 02));" +update_a_key_1: "await client.updateKey(\n '6062abda-a5aa-4414-ac91-ecd7944c0f8d',\n description: 'Manage documents: Products\/Reviews API key',\n name: 'Products\/Reviews API key',\n);" delete_a_key_1: |- await client.deleteKey('6062abda-a5aa-4414-ac91-ecd7944c0f8d'); security_guide_search_key_1: |- var client = MeiliSearchClient('http://localhost:7700', 'apiKey'); await client.index('patient_medical_records').search(''); -security_guide_update_key_1: |- - var client = MeiliSearchClient('http://localhost:7700', 'masterKey'); - await client.updateKey('74c9c733-3368-4738-bbe5-1d18a5fecb37', description: 'Default Search API Key'); +security_guide_update_key_1: "var client = MeiliSearchClient('http:\/\/localhost:7700', 'masterKey');\nawait client.updateKey(\n '74c9c733-3368-4738-bbe5-1d18a5fecb37',\n description: 'Default Search API Key',\n);" security_guide_create_key_1: |- var client = MeiliSearchClient('http://localhost:7700', 'masterKey'); await client.createKey( @@ -531,48 +286,7 @@ security_guide_list_keys_1: |- security_guide_delete_key_1: |- var client = MeiliSearchClient('http://localhost:7700', 'masterKey'); await client.deleteKey('ac5cd97d-5a4b-4226-a868-2d0eb6d197ab'); -tenant_token_guide_generate_sdk_1: |- - final uid = '85c3c2f9-bdd6-41f1-abd8-11fcf80e0f76'; - final apiKey = 'B5KdX2MY2jV6EXfUs6scSfmC...'; - final expiresAt = DateTime.utc(2025, 12, 20); - final searchRules = { - 'patient_medical_records': { - 'filter': 'user_id = 1' - } - }; - - final token = client.generateTenantToken( - uid, - searchRules, - apiKey: apiKey, // optional - expiresAt: expiresAt // optional - ); -tenant_token_guide_search_sdk_1: |- - final frontEndClient = MeiliSearchClient('http://localhost:7700', token); - await frontEndClient.index('patient_medical_records').search('blood test'); -landing_getting_started_1: |- - var client = MeiliSearchClient('http://localhost:7700', 'masterKey'); - - await client.index('movies').addDocuments([ - { 'id': 1, 'title': 'Carol' }, - { 'id': 2, 'title': 'Wonder Woman' }, - { 'id': 3, 'title': 'Life of Pi' }, - { 'id': 4, 'title': 'Mad Max: Fury Road' }, - { 'id': 5, 'title': 'Moana' }, - { 'id': 6, 'title': 'Philadelphia'} - ]); -search_parameter_guide_crop_marker_1: |- - await client - .index('movies') - .search('shifu', SearchQuery(attributesToCrop: ['overview'], cropMarker: "[…]")); -search_parameter_guide_highlight_tag_1: |- - await client.index('movies').search( - 'winter feast', - SearchQuery( - attributesToHighlight: ['overview'], - highlightPreTag: '', - highlightPostTag: '')); -geosearch_guide_filter_usage_3: |- - await client.index('restaurants') - .search('', SearchQuery(filter: '_geoBoundingBox([45.494181, 9.214024], [45.449484, 9.179175])')), - }) +search_parameter_guide_crop_marker_1: "await client.index('movies').search(\n 'shifu',\n SearchQuery(\n attributesToCrop: ['overview'],\n cropMarker: '[…]',\n ),\n );" +search_parameter_guide_highlight_tag_1: "await client.index('movies').search(\n 'winter feast',\n SearchQuery(\n attributesToHighlight: ['overview'],\n highlightPreTag: '',\n highlightPostTag: '<\/span>',\n ),\n );" +geosearch_guide_filter_usage_3: "await client.index('restaurants').search(\n '',\n SearchQuery(\n filter:\n '_geoBoundingBox([45.494181, 9.214024], [45.449484, 9.179175])'));\n});" +search_get_1: await client.index('movies').search('American ninja'); diff --git a/.github/release-draft-template.yml b/.github/release-draft-template.yml index e8adefa2..c1c4ac77 100644 --- a/.github/release-draft-template.yml +++ b/.github/release-draft-template.yml @@ -16,6 +16,10 @@ categories: label: 'bug' - title: '🔒 Security' label: 'security' + - title: '⚙️ Maintenance/misc' + label: + - 'maintenance' + - 'documentation' template: | $CHANGES diff --git a/.github/workflows/pre-release-tests.yml b/.github/workflows/pre-release-tests.yml index a498df8c..5d7e43ec 100644 --- a/.github/workflows/pre-release-tests.yml +++ b/.github/workflows/pre-release-tests.yml @@ -21,10 +21,10 @@ jobs: name: integration-tests-against-rc (dart ${{ matrix.version }}) steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Get the latest Meilisearch RC run: echo "MEILISEARCH_VERSION=$(curl https://raw.githubusercontent.com/meilisearch/integration-guides/main/scripts/get-latest-meilisearch-rc.sh | bash)" >> $GITHUB_ENV - name: Meilisearch (${{ env.MEILISEARCH_VERSION }}) setup with Docker run: docker run -d -p 7700:7700 getmeili/meilisearch:${{ env.MEILISEARCH_VERSION }} meilisearch --master-key=masterKey --no-analytics - name: Run integration tests - run: docker run --net="host" -v $PWD:/package -w /package dart:${{ matrix.version }} /bin/sh -c 'dart pub get && dart run test' + 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' diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index b2ed242c..aed70f61 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Check release validity run: sh .github/scripts/check-release.sh - name: Publish diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8f39a1fc..2bf37595 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -18,12 +18,11 @@ jobs: # Will still run for each push to bump-meilisearch-v* if: github.event_name != 'pull_request' || !startsWith(github.base_ref, 'bump-meilisearch-v') timeout-minutes: 10 - runs-on: ${{ matrix.os }} + runs-on: ubuntu-latest strategy: fail-fast: false matrix: - os: [ ubuntu-latest, macos-latest, windows-latest ] - version: ['3.0.0'] + version: ['latest'] name: integration-tests (dart ${{ matrix.version }}) services: meilisearch: @@ -35,23 +34,25 @@ jobs: - 7700:7700 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: dart-lang/setup-dart@v1 with: sdk: ${{ matrix.version }} - name: Install dependencies run: | dart pub get + dart pub get -C tool dart pub global activate coverage - name: Run integration tests run: dart test --concurrency=4 --reporter=github --coverage=./coverage/reports - name: Generate coverage reports run: dart pub global run coverage:format_coverage --report-on=./lib --lcov --in=./coverage/reports --out=coverage/lcov.info - name: Report to Codecov - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 with: file: coverage/lcov.info - fail_ci_if_error: true + fail_ci_if_error: false + linter: name: linter-check @@ -59,12 +60,14 @@ jobs: container: image: dart:latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: dart-lang/setup-dart@v1 with: sdk: '3.0.0' - name: Install dependencies - run: dart pub get + run: | + dart pub get + dart pub get -C tool - name: Run linter run: dart analyze --fatal-infos && dart format . --set-exit-if-changed @@ -72,19 +75,32 @@ jobs: name: Yaml linting check runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Yaml lint check uses: ibiqlik/action-yamllint@v3 with: config_file: .yamllint.yml + check-code-samples: + name: check .code-samples.meilisearch.yaml + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dart-lang/setup-dart@v1 + with: + sdk: 'latest' + - name: check if samples changed + run: | + dart pub get + dart pub get -C tool + dart run ./tool/bin/meili.dart update-samples --fail-on-change pana: runs-on: ubuntu-latest timeout-minutes: 10 container: image: dart:latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: dart-lang/setup-dart@v1 with: sdk: '3.0.0' diff --git a/CHANGELOG.md b/CHANGELOG.md index 52a52aaa..2971b016 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,28 @@ [comment]: <> (All notable changes to this project will be documented in this file.) +# 0.15.0 +### Breaking Changes: + +- Change members of `Faceting` to be `final`, and remove the default values +- Mark all `Searcheable` fields in the constructor as `required` +- Bug Fix `Searcheable` had a wrong `matchesPosition` property, which was moved into `MeiliDocumentContainer` + +### Changes: + +- Add `int? total` to `TasksResults` +- Add `attributesToSearchOn` to `SearchQuery` and `IndexSearchQuery` +- Add `Future facetSearch(FacetSearchQuery query)` to `MeiliSearchIndex` +- Add `enum FacetingSortTypes` +- Add `Map? sortFacetValuesBy` to `Faceting` +- [experimental]* Add `List? vector` to `SearchQuery` and `IndexSearchQuery` +- [experimental]* Add `bool? showRankingScoreDetails` to `SearchQuery` and `IndexSearchQuery` +- Add `bool? showRankingScore` to `SearchQuery` and `IndexSearchQuery` +- Add `MeiliDocumentContainer` +- Add `Map src` to `Searchable` which exposes the raw json object returned from the server. + Just in case we don't keep up with new meilisearch releases, the user has a way to access new features. + +[experimental]* To adopt a experimental [change you must opt-in manually](https://www.meilisearch.com/docs/learn/experimental/overview#activating-experimental-features) + # 0.14.0 ### Breaking Changes: - Moved `indexUid`, `query` from `SearchQuery` to the new `IndexSearchQuery`. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c7903028..eecd17b4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -9,6 +9,7 @@ First of all, thank you for contributing to Meilisearch! The goal of this docume - [Requirements ](#requirements-) - [Setup ](#setup-) - [Tests and Linter ](#tests-and-linter-) + - [Updating code samples](#updating-code-samples) - [Git Guidelines](#git-guidelines) - [Git Branches ](#git-branches-) - [Git Commits ](#git-commits-) @@ -76,6 +77,31 @@ dart test dart analyze ``` +### Updating code samples + +Some PRs require updating the code samples (found in `.code-samples.meilisearch.yaml`), this is done automatically using code excerpts, which are actual pieces of code subject to testing and linting. + +A lot of them are placed in `test/code_samples.dart`. + +Also most of the tests in that file are skipped, since they are mostly duplicated in other test files. + +The process to define a new code sample is as follows: +1. Add the piece of code in `test/code_samples.dart` +2. surround it with `#docregion key` and `#enddocregion`, e.g. + ``` + // #docregion meilisearch_contributing_1 + final client = MeilisearchClient(); + anything(); + // #enddocregion + ``` +3. run this command to update the code samples + ```bash + dart run ./tool/bin/meili.dart update-samples + ``` +4. to test if the code samples are updated correctly, run: + ```bash + dart run ./tool/bin/meili.dart update-samples --fail-on-change + ``` ## Git Guidelines ### Git Branches diff --git a/LICENSE b/LICENSE index 18221f6a..1600b0ab 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2020-2022 Meili SAS +Copyright (c) 2020-2024 Meili SAS Copyright (c) 2020 Misir Jafarov Permission is hereby granted, free of charge, to any person obtaining a copy diff --git a/README.md b/README.md index 0a912988..d3858d34 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ **Meilisearch** is an open-source search engine. [Learn more about Meilisearch.](https://github.com/meilisearch/meilisearch) -## Table of Contents +## Table of Contents - [📖 Documentation](#-documentation) - [⚡ Supercharge your Meilisearch experience](#-supercharge-your-meilisearch-experience) @@ -55,7 +55,7 @@ You can install the **meilisearch** package by adding a few lines into `pubspec. ```yaml dependencies: - meilisearch: ^0.14.0 + meilisearch: ^0.15.0 ``` Then open your terminal and update dart packages. @@ -202,6 +202,8 @@ await index.search('wonder', filter: ['id > 1 AND genres = Action']); This package guarantees compatibility with [version v1.x of Meilisearch](https://github.com/meilisearch/meilisearch/releases/tag/latest), but some features may not be present. Please check the [issues](https://github.com/meilisearch/meilisearch-dart/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22+label%3Aenhancement) for more info. +⚠️ This package is not compatible with the [`vectoreStore` experimental feature](https://www.meilisearch.com/docs/learn/experimental/vector_search) of Meilisearch v1.6.0 and later. More information on this [issue](https://github.com/meilisearch/meilisearch-dart/issues/369). + ## 💡 Learn more The following sections in our main documentation website may interest you: diff --git a/bors.toml b/bors.toml index cc14a4c6..e714ecb3 100644 --- a/bors.toml +++ b/bors.toml @@ -1,7 +1,7 @@ status = [ - 'integration-tests (dart 3.0.0)', - 'linter-check', - 'pana' + 'integration-tests (dart latest)', + 'linter-check', + 'pana' ] # 1 hour timeout timeout-sec = 3600 diff --git a/lib/src/filter_builder/operators.dart b/lib/src/filter_builder/operators.dart index 4a970f7a..33549dd3 100644 --- a/lib/src/filter_builder/operators.dart +++ b/lib/src/filter_builder/operators.dart @@ -1,325 +1,325 @@ -import 'package:collection/collection.dart'; -import 'package:meilisearch/meilisearch.dart'; - -import '../annotations.dart'; - -const _eqUnordered = DeepCollectionEquality.unordered(); - -typedef MeiliPoint = ({num lat, num lng}); - -/// Represents an empty filter -/// -/// works as a starting point for filter builders -class MeiliEmptyExpression extends MeiliOperatorExpressionBase { - const MeiliEmptyExpression(); - - @override - String transform() => ""; -} - -class MeiliAndOperatorExpression extends MeiliOperatorExpressionBase { - final List operands; - - MeiliAndOperatorExpression({ - required MeiliOperatorExpressionBase first, - required MeiliOperatorExpressionBase second, - }) : this.fromList([first, second]); - - const MeiliAndOperatorExpression.fromList(this.operands); - - @override - String transform() { - //space is mandatory - final filteredOperands = operands - .map((e) => e.transform()) - .where((element) => element.isNotEmpty); - if (filteredOperands.isEmpty) { - return ''; - } else if (filteredOperands.length == 1) { - return filteredOperands.first; - } else { - return filteredOperands.map((e) => '($e)').join(" AND "); - } - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - - return other is MeiliAndOperatorExpression && - _eqUnordered.equals(other.operands, operands); - } - - @override - int get hashCode => Object.hash("AND", _eqUnordered.hash(operands)); -} - -class MeiliOrOperatorExpression extends MeiliOperatorExpressionBase { - final List operands; - - MeiliOrOperatorExpression({ - required MeiliOperatorExpressionBase first, - required MeiliOperatorExpressionBase second, - }) : this.fromList([first, second]); - - const MeiliOrOperatorExpression.fromList(this.operands); - - @override - String transform() { - final filteredOperands = operands - .map((e) => e.transform()) - .where((element) => element.isNotEmpty); - if (filteredOperands.isEmpty) { - return ''; - } else if (filteredOperands.length == 1) { - return filteredOperands.first; - } else { - return filteredOperands.map((e) => '($e)').join(" OR "); - } - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - - return other is MeiliOrOperatorExpression && - _eqUnordered.equals(other.operands, operands); - } - - @override - int get hashCode => Object.hash("OR", _eqUnordered.hash(operands)); -} - -class MeiliToOperatorExpression extends MeiliOperatorExpressionBase { - final MeiliAttributeExpression attribute; - final MeiliValueExpressionBase min; - final MeiliValueExpressionBase max; - - const MeiliToOperatorExpression({ - required this.min, - required this.max, - required this.attribute, - }); - - @override - String transform() { - final attributeTransformed = attribute.transform(); - return "$attributeTransformed ${min.transform()} TO $attributeTransformed ${max.transform()}"; - } -} - -class MeiliGeoRadiusOperatorExpression extends MeiliOperatorExpressionBase { - final MeiliPoint point; - final double distanceInMeters; - - const MeiliGeoRadiusOperatorExpression( - this.point, - this.distanceInMeters, - ); - - @override - String transform() { - return '_geoRadius(${point.lat},${point.lng},$distanceInMeters)'; - } -} - -@RequiredMeiliServerVersion('1.1.0') -class MeiliGeoBoundingBoxOperatorExpression - extends MeiliOperatorExpressionBase { - final MeiliPoint point1; - final MeiliPoint point2; - - const MeiliGeoBoundingBoxOperatorExpression( - this.point1, - this.point2, - ); - - @override - String transform() { - return '_geoBoundingBox([${point1.lat},${point1.lng}],[${point2.lat},${point2.lng}])'; - } -} - -class MeiliExistsOperatorExpression extends MeiliOperatorExpressionBase { - final MeiliAttributeExpression attribute; - - const MeiliExistsOperatorExpression(this.attribute); - - @override - String transform() { - return "${attribute.transform()} EXISTS"; - } -} - -class MeiliNotExistsOperatorExpression extends MeiliOperatorExpressionBase { - final MeiliAttributeExpression attribute; - - const MeiliNotExistsOperatorExpression(this.attribute); - - @override - String transform() { - return "${attribute.transform()} NOT EXISTS"; - } -} - -@RequiredMeiliServerVersion('1.2.0') -class MeiliIsNullOperatorExpression extends MeiliOperatorExpressionBase { - final MeiliAttributeExpression attribute; - - const MeiliIsNullOperatorExpression(this.attribute); - - @override - String transform() { - return "${attribute.transform()} IS NULL"; - } -} - -@RequiredMeiliServerVersion('1.2.0') -class MeiliIsNotNullOperatorExpression extends MeiliOperatorExpressionBase { - final MeiliAttributeExpression attribute; - - const MeiliIsNotNullOperatorExpression(this.attribute); - - @override - String transform() { - return "${attribute.transform()} IS NOT NULL"; - } -} - -@RequiredMeiliServerVersion('1.2.0') -class MeiliIsEmptyOperatorExpression extends MeiliOperatorExpressionBase { - final MeiliAttributeExpression attribute; - - const MeiliIsEmptyOperatorExpression(this.attribute); - - @override - String transform() { - return "${attribute.transform()} IS EMPTY"; - } -} - -@RequiredMeiliServerVersion('1.2.0') -class MeiliIsNotEmptyOperatorExpression extends MeiliOperatorExpressionBase { - final MeiliAttributeExpression attribute; - - const MeiliIsNotEmptyOperatorExpression(this.attribute); - - @override - String transform() { - return "${attribute.transform()} IS NOT EMPTY"; - } -} - -class MeiliNotOperatorExpression extends MeiliOperatorExpressionBase { - final MeiliOperatorExpressionBase operator; - - const MeiliNotOperatorExpression(this.operator) - : assert(operator is! MeiliEmptyExpression, - "Cannot negate (NOT) an empty operator"); - - @override - String transform() { - return "NOT ${operator.transform()}"; - } -} - -class MeiliInOperatorExpression extends MeiliOperatorExpressionBase { - final MeiliAttributeExpression attribute; - final List values; - - const MeiliInOperatorExpression({ - required this.attribute, - required this.values, - }); - - @override - String transform() { - //TODO(ahmednfwela): escape commas in values ? - return "${attribute.transform()} IN [${values.map((e) => e.transform()).join(',')}]"; - } -} - -/// Represents an operator that has a value as an operand -abstract class MeiliValueOperandOperatorExpressionBase - extends MeiliOperatorExpressionBase { - final MeiliAttributeExpression property; - final MeiliValueExpressionBase value; - - const MeiliValueOperandOperatorExpressionBase({ - required this.property, - required this.value, - }); - - String get operator; - - @override - String transform() { - return '${property.transform()} $operator ${value.transform()}'; - } -} - -class MeiliEqualsOperatorExpression - extends MeiliValueOperandOperatorExpressionBase { - const MeiliEqualsOperatorExpression({ - required MeiliAttributeExpression property, - required MeiliValueExpressionBase value, - }) : super(property: property, value: value); - - @override - final String operator = "="; -} - -class MeiliNotEqualsOperatorExpression - extends MeiliValueOperandOperatorExpressionBase { - const MeiliNotEqualsOperatorExpression({ - required MeiliAttributeExpression property, - required MeiliValueExpressionBase value, - }) : super(property: property, value: value); - - @override - final String operator = "!="; -} - -class MeiliGreaterThanOperatorExpression - extends MeiliValueOperandOperatorExpressionBase { - const MeiliGreaterThanOperatorExpression({ - required MeiliAttributeExpression property, - required MeiliValueExpressionBase value, - }) : super(property: property, value: value); - - @override - final String operator = ">"; -} - -class MeiliGreaterThanEqualsOperatorExpression - extends MeiliValueOperandOperatorExpressionBase { - const MeiliGreaterThanEqualsOperatorExpression({ - required MeiliAttributeExpression property, - required MeiliValueExpressionBase value, - }) : super(property: property, value: value); - - @override - final String operator = ">="; -} - -class MeiliLessThanOperatorExpression - extends MeiliValueOperandOperatorExpressionBase { - const MeiliLessThanOperatorExpression({ - required MeiliAttributeExpression property, - required MeiliValueExpressionBase value, - }) : super(property: property, value: value); - - @override - final String operator = "<"; -} - -class MeiliLessThanEqualsOperatorExpression - extends MeiliValueOperandOperatorExpressionBase { - const MeiliLessThanEqualsOperatorExpression({ - required MeiliAttributeExpression property, - required MeiliValueExpressionBase value, - }) : super(property: property, value: value); - - @override - final String operator = "<="; -} +import 'package:collection/collection.dart'; +import 'package:meilisearch/meilisearch.dart'; + +import '../annotations.dart'; + +const _eqUnordered = DeepCollectionEquality.unordered(); + +typedef MeiliPoint = ({num lat, num lng}); + +/// Represents an empty filter +/// +/// works as a starting point for filter builders +class MeiliEmptyExpression extends MeiliOperatorExpressionBase { + const MeiliEmptyExpression(); + + @override + String transform() => ""; +} + +class MeiliAndOperatorExpression extends MeiliOperatorExpressionBase { + final List operands; + + MeiliAndOperatorExpression({ + required MeiliOperatorExpressionBase first, + required MeiliOperatorExpressionBase second, + }) : this.fromList([first, second]); + + const MeiliAndOperatorExpression.fromList(this.operands); + + @override + String transform() { + //space is mandatory + final filteredOperands = operands + .map((e) => e.transform()) + .where((element) => element.isNotEmpty); + if (filteredOperands.isEmpty) { + return ''; + } else if (filteredOperands.length == 1) { + return filteredOperands.first; + } else { + return filteredOperands.map((e) => '($e)').join(" AND "); + } + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is MeiliAndOperatorExpression && + _eqUnordered.equals(other.operands, operands); + } + + @override + int get hashCode => Object.hash("AND", _eqUnordered.hash(operands)); +} + +class MeiliOrOperatorExpression extends MeiliOperatorExpressionBase { + final List operands; + + MeiliOrOperatorExpression({ + required MeiliOperatorExpressionBase first, + required MeiliOperatorExpressionBase second, + }) : this.fromList([first, second]); + + const MeiliOrOperatorExpression.fromList(this.operands); + + @override + String transform() { + final filteredOperands = operands + .map((e) => e.transform()) + .where((element) => element.isNotEmpty); + if (filteredOperands.isEmpty) { + return ''; + } else if (filteredOperands.length == 1) { + return filteredOperands.first; + } else { + return filteredOperands.map((e) => '($e)').join(" OR "); + } + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is MeiliOrOperatorExpression && + _eqUnordered.equals(other.operands, operands); + } + + @override + int get hashCode => Object.hash("OR", _eqUnordered.hash(operands)); +} + +class MeiliToOperatorExpression extends MeiliOperatorExpressionBase { + final MeiliAttributeExpression attribute; + final MeiliValueExpressionBase min; + final MeiliValueExpressionBase max; + + const MeiliToOperatorExpression({ + required this.min, + required this.max, + required this.attribute, + }); + + @override + String transform() { + final attributeTransformed = attribute.transform(); + return "$attributeTransformed ${min.transform()} TO $attributeTransformed ${max.transform()}"; + } +} + +class MeiliGeoRadiusOperatorExpression extends MeiliOperatorExpressionBase { + final MeiliPoint point; + final double distanceInMeters; + + const MeiliGeoRadiusOperatorExpression( + this.point, + this.distanceInMeters, + ); + + @override + String transform() { + return '_geoRadius(${point.lat},${point.lng},$distanceInMeters)'; + } +} + +@RequiredMeiliServerVersion('1.1.0') +class MeiliGeoBoundingBoxOperatorExpression + extends MeiliOperatorExpressionBase { + final MeiliPoint point1; + final MeiliPoint point2; + + const MeiliGeoBoundingBoxOperatorExpression( + this.point1, + this.point2, + ); + + @override + String transform() { + return '_geoBoundingBox([${point1.lat},${point1.lng}],[${point2.lat},${point2.lng}])'; + } +} + +class MeiliExistsOperatorExpression extends MeiliOperatorExpressionBase { + final MeiliAttributeExpression attribute; + + const MeiliExistsOperatorExpression(this.attribute); + + @override + String transform() { + return "${attribute.transform()} EXISTS"; + } +} + +class MeiliNotExistsOperatorExpression extends MeiliOperatorExpressionBase { + final MeiliAttributeExpression attribute; + + const MeiliNotExistsOperatorExpression(this.attribute); + + @override + String transform() { + return "${attribute.transform()} NOT EXISTS"; + } +} + +@RequiredMeiliServerVersion('1.2.0') +class MeiliIsNullOperatorExpression extends MeiliOperatorExpressionBase { + final MeiliAttributeExpression attribute; + + const MeiliIsNullOperatorExpression(this.attribute); + + @override + String transform() { + return "${attribute.transform()} IS NULL"; + } +} + +@RequiredMeiliServerVersion('1.2.0') +class MeiliIsNotNullOperatorExpression extends MeiliOperatorExpressionBase { + final MeiliAttributeExpression attribute; + + const MeiliIsNotNullOperatorExpression(this.attribute); + + @override + String transform() { + return "${attribute.transform()} IS NOT NULL"; + } +} + +@RequiredMeiliServerVersion('1.2.0') +class MeiliIsEmptyOperatorExpression extends MeiliOperatorExpressionBase { + final MeiliAttributeExpression attribute; + + const MeiliIsEmptyOperatorExpression(this.attribute); + + @override + String transform() { + return "${attribute.transform()} IS EMPTY"; + } +} + +@RequiredMeiliServerVersion('1.2.0') +class MeiliIsNotEmptyOperatorExpression extends MeiliOperatorExpressionBase { + final MeiliAttributeExpression attribute; + + const MeiliIsNotEmptyOperatorExpression(this.attribute); + + @override + String transform() { + return "${attribute.transform()} IS NOT EMPTY"; + } +} + +class MeiliNotOperatorExpression extends MeiliOperatorExpressionBase { + final MeiliOperatorExpressionBase operator; + + const MeiliNotOperatorExpression(this.operator) + : assert(operator is! MeiliEmptyExpression, + "Cannot negate (NOT) an empty operator"); + + @override + String transform() { + return "NOT ${operator.transform()}"; + } +} + +class MeiliInOperatorExpression extends MeiliOperatorExpressionBase { + final MeiliAttributeExpression attribute; + final List values; + + const MeiliInOperatorExpression({ + required this.attribute, + required this.values, + }); + + @override + String transform() { + //TODO(ahmednfwela): escape commas in values ? + return "${attribute.transform()} IN [${values.map((e) => e.transform()).join(',')}]"; + } +} + +/// Represents an operator that has a value as an operand +abstract class MeiliValueOperandOperatorExpressionBase + extends MeiliOperatorExpressionBase { + final MeiliAttributeExpression property; + final MeiliValueExpressionBase value; + + const MeiliValueOperandOperatorExpressionBase({ + required this.property, + required this.value, + }); + + String get operator; + + @override + String transform() { + return '${property.transform()} $operator ${value.transform()}'; + } +} + +class MeiliEqualsOperatorExpression + extends MeiliValueOperandOperatorExpressionBase { + const MeiliEqualsOperatorExpression({ + required super.property, + required super.value, + }); + + @override + final String operator = "="; +} + +class MeiliNotEqualsOperatorExpression + extends MeiliValueOperandOperatorExpressionBase { + const MeiliNotEqualsOperatorExpression({ + required super.property, + required super.value, + }); + + @override + final String operator = "!="; +} + +class MeiliGreaterThanOperatorExpression + extends MeiliValueOperandOperatorExpressionBase { + const MeiliGreaterThanOperatorExpression({ + required super.property, + required super.value, + }); + + @override + final String operator = ">"; +} + +class MeiliGreaterThanEqualsOperatorExpression + extends MeiliValueOperandOperatorExpressionBase { + const MeiliGreaterThanEqualsOperatorExpression({ + required super.property, + required super.value, + }); + + @override + final String operator = ">="; +} + +class MeiliLessThanOperatorExpression + extends MeiliValueOperandOperatorExpressionBase { + const MeiliLessThanOperatorExpression({ + required super.property, + required super.value, + }); + + @override + final String operator = "<"; +} + +class MeiliLessThanEqualsOperatorExpression + extends MeiliValueOperandOperatorExpressionBase { + const MeiliLessThanEqualsOperatorExpression({ + required super.property, + required super.value, + }); + + @override + final String operator = "<="; +} diff --git a/lib/src/filter_builder/values.dart b/lib/src/filter_builder/values.dart index c1a8a19f..db1f6d42 100644 --- a/lib/src/filter_builder/values.dart +++ b/lib/src/filter_builder/values.dart @@ -29,8 +29,10 @@ class MeiliDateTimeValueExpression extends MeiliValueExpressionBase { "DateTime passed to Meili must be in UTC to avoid inconsistency accross multiple devices", ); + /// Unix epoch time is seconds since epoch @override - String transform() => value.millisecondsSinceEpoch.toString(); + String transform() => + (value.millisecondsSinceEpoch / 1000).floor().toString(); @override bool operator ==(Object other) { diff --git a/lib/src/tenant_token/exceptions.dart b/lib/src/tenant_token/exceptions.dart index 53acbb35..e3cefd0a 100644 --- a/lib/src/tenant_token/exceptions.dart +++ b/lib/src/tenant_token/exceptions.dart @@ -1,4 +1,4 @@ -part of tenant_token; +part of '../tenant_token.dart'; class ExpiredSignatureException implements Exception { const ExpiredSignatureException(); diff --git a/lib/src/tenant_token/generator.dart b/lib/src/tenant_token/generator.dart index 2e9802c0..c0771039 100644 --- a/lib/src/tenant_token/generator.dart +++ b/lib/src/tenant_token/generator.dart @@ -1,4 +1,4 @@ -part of tenant_token; +part of '../tenant_token.dart'; final _jsonEncoder = json.fuse(utf8.fuse(base64Url)); diff --git a/lib/src/version.dart b/lib/src/version.dart index 84e9c1de..519bf9f7 100644 --- a/lib/src/version.dart +++ b/lib/src/version.dart @@ -1,5 +1,5 @@ class Version { - static const String current = '0.14.0'; + static const String current = '0.15.0'; static String get qualifiedVersion { return "Meilisearch Dart (v$current)"; diff --git a/pubspec.yaml b/pubspec.yaml index d295d3ee..5dbe7b7b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: meilisearch description: Meilisearch Dart is the Meilisearch API client for Dart and Flutter developers. -version: 0.14.0 +version: 0.15.0 homepage: https://meilisearch.com repository: https://github.com/meilisearch/meilisearch-dart issue_tracker: https://github.com/meilisearch/meilisearch-dart/issues @@ -17,8 +17,8 @@ dependencies: dev_dependencies: test: ^1.0.0 - dart_jsonwebtoken: ^2.3.2 - lints: ^2.1.0 + dart_jsonwebtoken: ^2.12.2 + lints: ">=2.1.0 <4.0.0" json_serializable: ^6.7.1 build_runner: ^2.4.6 diff --git a/test/code_samples.dart b/test/code_samples.dart new file mode 100644 index 00000000..3de892be --- /dev/null +++ b/test/code_samples.dart @@ -0,0 +1,888 @@ +// ignore_for_file: unused_element + +import 'dart:io'; + +import 'package:meilisearch/meilisearch.dart'; +import 'package:test/test.dart'; + +import 'utils/client.dart'; + +void main() { + // this file hosts some code samples referenced in + // .code-samples.meilisearch.yaml + // it's subject to tests, lint rules, deprecation notices, etc... + group('code samples', () { + setUpClient(); + + test('excerpts', () async { + void a1() async { + // #docregion typo_tolerance_guide_1 + final toUpdate = TypoTolerance(enabled: false); + await client.index('movies').updateTypoTolerance(toUpdate); + // #enddocregion + } + + void a2() async { + // #docregion typo_tolerance_guide_2 + final toUpdate = TypoTolerance( + disableOnAttributes: ['title'], + ); + await client.index('movies').updateTypoTolerance(toUpdate); + // #enddocregion + } + + void a3() async { + // #docregion typo_tolerance_guide_3 + final toUpdate = TypoTolerance( + disableOnWords: ['shrek'], + ); + await client.index('movies').updateTypoTolerance(toUpdate); + // #enddocregion + } + + void a4() async { + // #docregion typo_tolerance_guide_4 + final toUpdate = TypoTolerance( + minWordSizeForTypos: MinWordSizeForTypos( + oneTypo: 4, + twoTypos: 10, + ), + ); + await client.index('movies').updateTypoTolerance(toUpdate); + // #enddocregion + } + + void a8() async { + // #docregion getting_started_add_meteorites + final json = await File('meteorites.json').readAsString(); + + await client.index('meteorites').addDocumentsJson(json); + // #enddocregion + } + + void a10() async { + // #docregion add_movies_json_1 + // import 'dart:io'; + // import 'dart:convert'; + final json = await File('movies.json').readAsString(); + await client.index('movies').addDocumentsJson(json); + // #enddocregion + } + + void a11() async { + // #docregion security_guide_delete_key_1 + var client = MeiliSearchClient('http://localhost:7700', 'masterKey'); + await client.deleteKey('ac5cd97d-5a4b-4226-a868-2d0eb6d197ab'); + // #enddocregion + } + + void a12() async { + // #docregion security_guide_list_keys_1 + var client = MeiliSearchClient('http://localhost:7700', 'masterKey'); + await client.getKeys(); + // #enddocregion + } + + void a13() async { + // #docregion security_guide_create_key_1 + var client = MeiliSearchClient('http://localhost:7700', 'masterKey'); + await client.createKey( + description: 'Search patient records key', + actions: ['search'], + indexes: ['patient_medical_records'], + expiresAt: DateTime(2023, 01, 01), + ); + // #enddocregion + } + + void a14() async { + // #docregion authorization_header_1 + var client = MeiliSearchClient('http://localhost:7700', 'masterKey'); + await client.getKeys(); + // #enddocregion + } + + void a15() async { + // #docregion security_guide_search_key_1 + var client = MeiliSearchClient('http://localhost:7700', 'apiKey'); + await client.index('patient_medical_records').search(''); + // #enddocregion + } + + void a16() async { + // #docregion security_guide_update_key_1 + var client = MeiliSearchClient('http://localhost:7700', 'masterKey'); + await client.updateKey( + '74c9c733-3368-4738-bbe5-1d18a5fecb37', + description: 'Default Search API Key', + ); + // #enddocregion + } + + // #docregion date_guide_index_1 + //import 'dart:io'; + //import 'dart:convert'; + + final json = await File('games.json').readAsString(); + + await client.index('games').addDocumentsJson(json); + // #enddocregion + + // #docregion date_guide_filterable_attributes_1 + await client + .index('games') + .updateFilterableAttributes(['release_timestamp']); + // #enddocregion + + // #docregion date_guide_filter_1 + await client.index('games').search( + '', + SearchQuery( + filterExpression: Meili.and([ + Meili.gte( + 'release_timestamp'.toMeiliAttribute(), + Meili.value(DateTime(2017, 12, 31, 23, 0)), + ), + Meili.lt( + 'release_timestamp'.toMeiliAttribute(), + Meili.value(DateTime(2022, 12, 31, 23, 0)), + ), + ]), + ), + ); + // #enddocregion + + // #docregion date_guide_sortable_attributes_1 + await client + .index('games') + .updateSortableAttributes(['release_timestamp']); + // #enddocregion + + // #docregion date_guide_sort_1 + await client + .index('games') + .search('', SearchQuery(sort: ['release_timestamp:desc'])); + // #enddocregion + + // #docregion get_all_tasks_paginating_1 + await client.getTasks(params: TasksQuery(limit: 2, from: 10)); + // #enddocregion + + // #docregion get_all_tasks_paginating_2 + await client.getTasks(params: TasksQuery(limit: 2, from: 8)); + // #enddocregion + + // #docregion get_pagination_settings_1 + await client.index('movies').getPagination(); + // #enddocregion + + // #docregion update_pagination_settings_1 + await client + .index('books') + .updatePagination(Pagination(maxTotalHits: 100)); + // #enddocregion + + // #docregion reset_pagination_settings_1 + await client.index('movies').resetPagination(); + // #enddocregion + + // #docregion get_faceting_settings_1 + await client.index('movies').getFaceting(); + // #enddocregion + + // #docregion update_faceting_settings_1 + await client.index('books').updateFaceting(Faceting( + maxValuesPerFacet: 2, + sortFacetValuesBy: { + '*': FacetingSortTypes.alpha, + 'genres': FacetingSortTypes.count + })); + // #enddocregion + + // #docregion reset_faceting_settings_1 + await client.index('movies').resetFaceting(); + // #enddocregion + + // #docregion get_one_index_1 + await client.getIndex('movies'); + // #enddocregion + + // #docregion list_all_indexes_1 + await client.getIndexes(params: IndexesQuery(limit: 3)); + // #enddocregion + + // #docregion create_an_index_1 + await client.createIndex('movies', primaryKey: 'id'); + // #enddocregion + + // #docregion update_an_index_1 + await client.index('movies').update(primaryKey: 'id'); + // #enddocregion + + // #docregion delete_an_index_1 + await client.index('movies').delete(); + // #enddocregion + + // #docregion get_one_document_1 + await client.index('movies').getDocument(25684, + fields: ['id', 'title', 'poster', 'release_date']); + // #enddocregion + + // #docregion add_or_replace_documents_1 + await client.index('movies').addDocuments([ + { + 'id': 287947, + 'title': 'Shazam', + 'poster': + 'https://image.tmdb.org/t/p/w1280/xnopI5Xtky18MPhK40cZAGAOVeV.jpg', + 'overview': + 'A boy is given the ability to become an adult superhero in times of need with a single magic word.', + 'release_date': '2019-03-23' + } + ]); + // #enddocregion + + // #docregion add_or_update_documents_1 + await client.index('movies').updateDocuments([ + { + 'id': 287947, + 'title': 'Shazam ⚡️', + 'genres': 'comedy', + } + ]); + // #enddocregion + + // #docregion delete_all_documents_1 + await client.index('movies').deleteAllDocuments(); + // #enddocregion + + // #docregion delete_one_document_1 + await client.index('movies').deleteDocument(25684); + // #enddocregion + + // #docregion delete_documents_by_batch_1 + await client.index('movies').deleteDocuments( + DeleteDocumentsQuery( + ids: [23488, 153738, 437035, 363869], + ), + ); + // #enddocregion + + // #docregion search_post_1 + await client.index('movies').search('American ninja'); + // #enddocregion + + // #docregion get_task_1 + await client.getTask(1); + // #enddocregion + + // #docregion get_all_tasks_1 + await client.getTasks(); + // #enddocregion + + // #docregion get_settings_1 + await client.index('movies').getSettings(); + // #enddocregion + + // #docregion update_settings_1 + await client.index('movies').updateSettings( + IndexSettings( + rankingRules: [ + 'words', + 'typo', + 'proximity', + 'attribute', + 'sort', + 'exactness', + 'release_date:desc', + 'rank:desc' + ], + distinctAttribute: 'movie_id', + searchableAttributes: ['title', 'overview', 'genres'], + displayedAttributes: [ + 'title', + 'overview', + 'genres', + 'release_date' + ], + stopWords: ['the', 'a', 'an'], + sortableAttributes: ['title', 'release_date'], + synonyms: { + 'wolverine': ['xmen', 'logan'], + 'logan': ['wolverine'], + }, + ), + ); + // #enddocregion + + // #docregion reset_settings_1 + await client.index('movies').resetSettings(); + // #enddocregion + + // #docregion get_synonyms_1 + await client.index('movies').getSynonyms(); + // #enddocregion + + // #docregion update_synonyms_1 + await client.index('movies').updateSynonyms({ + 'wolverine': ['xmen', 'logan'], + 'logan': ['wolverine', 'xmen'], + 'wow': ['world of warcraft'], + }); + // #enddocregion + + // #docregion reset_synonyms_1 + await client.index('movies').resetSynonyms(); + // #enddocregion + + // #docregion get_stop_words_1 + await client.index('movies').getStopWords(); + // #enddocregion + + // #docregion update_stop_words_1 + await client.index('movies').updateStopWords(['of', 'the', 'to']); + // #enddocregion + + // #docregion reset_stop_words_1 + await client.index('movies').resetStopWords(); + // #enddocregion + + // #docregion get_ranking_rules_1 + await client.index('movies').getRankingRules(); + // #enddocregion + + // #docregion update_ranking_rules_1 + await client.index('movies').updateRankingRules([ + 'words', + 'typo', + 'proximity', + 'attribute', + 'sort', + 'exactness', + 'release_date:asc', + 'rank:desc', + ]); + // #enddocregion + + // #docregion reset_ranking_rules_1 + await client.index('movies').resetRankingRules(); + // #enddocregion + + // #docregion get_distinct_attribute_1 + await client.index('shoes').getDistinctAttribute(); + // #enddocregion + + // #docregion update_distinct_attribute_1 + await client.index('shoes').updateDistinctAttribute('skuid'); + // #enddocregion + + // #docregion reset_distinct_attribute_1 + await client.index('shoes').resetDistinctAttribute(); + // #enddocregion + + // #docregion get_filterable_attributes_1 + await client.index('movies').getFilterableAttributes(); + // #enddocregion + + // #docregion update_filterable_attributes_1 + await client + .index('movies') + .updateFilterableAttributes(['genres', 'director']); + // #enddocregion + + // #docregion reset_filterable_attributes_1 + await client.index('movies').resetFilterableAttributes(); + // #enddocregion + + // #docregion get_searchable_attributes_1 + await client.index('movies').getSearchableAttributes(); + // #enddocregion + + // #docregion update_searchable_attributes_1 + await client + .index('movies') + .updateSearchableAttributes(['title', 'overview', 'genres']); + // #enddocregion + + // #docregion reset_searchable_attributes_1 + await client.index('movies').resetSearchableAttributes(); + // #enddocregion + + // #docregion get_displayed_attributes_1 + await client.index('movies').getDisplayedAttributes(); + // #enddocregion + + // #docregion update_displayed_attributes_1 + await client.index('movies').updateDisplayedAttributes([ + 'title', + 'overview', + 'genres', + 'release_date', + ]); + // #enddocregion + + // #docregion reset_displayed_attributes_1 + await client.index('movies').resetDisplayedAttributes(); + // #enddocregion + + // #docregion get_typo_tolerance_1 + await client.index('books').getTypoTolerance(); + // #enddocregion + + // #docregion update_typo_tolerance_1 + final toUpdate = TypoTolerance( + minWordSizeForTypos: MinWordSizeForTypos( + oneTypo: 4, + twoTypos: 10, + ), + disableOnAttributes: ['title'], + ); + await client.index('books').updateTypoTolerance(toUpdate); + // #enddocregion + + // #docregion reset_typo_tolerance_1 + await client.index('books').resetTypoTolerance(); + // #enddocregion + + // #docregion get_index_stats_1 + await client.index('movies').getStats(); + // #enddocregion + + // #docregion get_indexes_stats_1 + await client.getStats(); + // #enddocregion + + // #docregion get_health_1 + await client.health(); + // #enddocregion + + // #docregion get_version_1 + await client.getVersion(); + // #enddocregion + + // #docregion distinct_attribute_guide_1 + await client.index('jackets').updateDistinctAttribute('product_id'); + // #enddocregion + + // #docregion field_properties_guide_searchable_1 + await client + .index('movies') + .updateSearchableAttributes(['title', 'overview', 'genres']); + // #enddocregion + + // #docregion field_properties_guide_displayed_1 + await client.index('movies').updateDisplayedAttributes([ + 'title', + 'overview', + 'genres', + 'release_date', + ]); + // #enddocregion + + // #docregion filtering_guide_1 + await client.index('movie_ratings').search( + 'Avengers', + SearchQuery( + filterExpression: Meili.gt( + Meili.attr('release_date'), + DateTime.utc(1995, 3, 18).toMeiliValue(), + ), + ), + ); + // #enddocregion + + // #docregion filtering_guide_2 + await client.index('movie_ratings').search( + 'Batman', + SearchQuery( + filterExpression: Meili.and([ + Meili.attr('release_date') + .gt(DateTime.utc(1995, 3, 18).toMeiliValue()), + Meili.or([ + 'director'.toMeiliAttribute().eq('Tim Burton'.toMeiliValue()), + 'director' + .toMeiliAttribute() + .eq('Christopher Nolan'.toMeiliValue()), + ]), + ]), + ), + ); + // #enddocregion + + // #docregion filtering_guide_3 + await client.index('movie_ratings').search( + 'Planet of the Apes', + SearchQuery( + filterExpression: Meili.and([ + Meili.attr('release_date') + .gt(DateTime.utc(2020, 1, 1, 13, 15, 50).toMeiliValue()), + Meili.not( + Meili.attr('director').eq("Tim Burton".toMeiliValue()), + ), + ]), + ), + ); + // #enddocregion + + // #docregion search_parameter_guide_query_1 + await client.index('movies').search('shifu'); + // #enddocregion + + // #docregion search_parameter_guide_offset_1 + await client.index('movies').search('shifu', SearchQuery(offset: 1)); + // #enddocregion + + // #docregion search_parameter_guide_limit_1 + await client.index('movies').search('shifu', SearchQuery(limit: 2)); + // #enddocregion + + // #docregion search_parameter_guide_matching_strategy_1 + await client.index('movies').search( + 'big fat liar', SearchQuery(matchingStrategy: MatchingStrategy.last)); + // #enddocregion + + // #docregion search_parameter_guide_matching_strategy_2 + await client.index('movies').search( + 'big fat liar', SearchQuery(matchingStrategy: MatchingStrategy.all)); + // #enddocregion + + // #docregion search_parameter_guide_retrieve_1 + await client.index('movies').search( + 'shifu', SearchQuery(attributesToRetrieve: ['overview', 'title'])); + // #enddocregion + + // #docregion search_parameter_guide_crop_1 + await client.index('movies').search( + 'shifu', SearchQuery(attributesToCrop: ['overview'], cropLength: 5)); + // #enddocregion + + // #docregion search_parameter_guide_highlight_1 + await client.index('movies').search( + 'winter feast', SearchQuery(attributesToHighlight: ['overview'])); + // #enddocregion + + // #docregion search_parameter_guide_show_matches_position_1 + await client + .index('movies') + .search('winter feast', SearchQuery(showMatchesPosition: true)); + // #enddocregion + + // #docregion primary_field_guide_create_index_primary_key + await client.createIndex('books', primaryKey: 'reference_number'); + // #enddocregion + + // #docregion primary_field_guide_update_document_primary_key + await client.updateIndex('books', 'title'); + // #enddocregion + + // #docregion primary_field_guide_add_document_primary_key + await client.index('movies').addDocuments([ + { + 'reference_number': 287947, + 'title': 'Diary of a Wimpy Kid', + 'author': 'Jeff Kinney', + 'genres': ['comedy', 'humor'], + 'price': 5.00 + } + ], primaryKey: 'reference_number'); + // #enddocregion + + // #docregion getting_started_update_ranking_rules + await client.index('movies').updateRankingRules([ + 'exactness', + 'words', + 'typo', + 'proximity', + 'attribute', + 'sort', + 'release_date:asc', + 'rank:desc', + ]); + // #enddocregion + + // #docregion getting_started_update_searchable_attributes + await client.index('movies').updateSearchableAttributes(['title']); + // #enddocregion + + // #docregion getting_started_update_stop_words + await client.index('movies').updateStopWords(['the']); + // #enddocregion + + // #docregion getting_started_check_task_status + await client.getTask(0); + // #enddocregion + + // #docregion getting_started_synonyms + await client.index('movies').updateSynonyms({ + 'winnie': ['piglet'], + 'piglet': ['winnie'], + }); + // #enddocregion + + // #docregion getting_started_update_displayed_attributes + await client + .index('movies') + .updateDisplayedAttributes(['title', 'overview', 'poster']); + // #enddocregion + + // #docregion getting_started_configure_settings + await client.index('meteorites').updateSettings(IndexSettings( + filterableAttributes: ['mass', '_geo'], + sortableAttributes: ['mass', '_geo'])); + // #enddocregion + + // #docregion getting_started_geo_radius + await client.index('meteorites').search( + '', + SearchQuery( + filterExpression: Meili.geoRadius( + (lat: 46.9480, lng: 7.4474), + 210000, + ), + ), + ); + // #enddocregion + + // #docregion getting_started_geo_point + await client.index('meteorites').search( + '', SearchQuery(sort: ['_geoPoint(48.8583701, 2.2922926):asc'])); + // #enddocregion + + // #docregion getting_started_sorting + await client.index('meteorites').search( + '', + SearchQuery( + sort: ['mass:asc'], + filterExpression: Meili.attr('mass').lt(200.toMeiliValue()), + ), + ); + // #enddocregion + + // #docregion getting_started_filtering + await client + .index('meteorites') + .search('', SearchQuery(filter: 'mass < 200')); + // #enddocregion + + // #docregion getting_started_faceting + await client.index('books').updateFaceting(Faceting( + maxValuesPerFacet: 2, + sortFacetValuesBy: {'*': FacetingSortTypes.count})); + // #enddocregion + + void a9() async { + // #docregion getting_started_typo_tolerance + final toUpdate = TypoTolerance( + minWordSizeForTypos: MinWordSizeForTypos(oneTypo: 4), + ); + await client.index('movies').updateTypoTolerance(toUpdate); + // #enddocregion + } + + // #docregion getting_started_pagination + await client + .index('books') + .updatePagination(Pagination(maxTotalHits: 500)); + // #enddocregion + + // #docregion filtering_update_settings_1 + await client.index('movies').updateFilterableAttributes([ + 'director', + 'genres', + ]); + // #enddocregion + + // #docregion faceted_search_walkthrough_filter_1 + await client.index('movies').search( + 'thriller', + SearchQuery(filter: [ + ['genres = Horror', 'genres = Mystery'], + 'director = "Jordan Peele"' + ])); + // #enddocregion + + // #docregion post_dump_1 + await client.createDump(); + // #enddocregion + + // #docregion phrase_search_1 + await client.index('movies').search('"african american" horror'); + // #enddocregion + + // #docregion sorting_guide_update_sortable_attributes_1 + await client.index('books').updateSortableAttributes(['author', 'price']); + // #enddocregion + + // #docregion sorting_guide_update_ranking_rules_1 + await client.index('books').updateRankingRules( + ['words', 'sort', 'typo', 'proximity', 'attribute', 'exactness']); + // #enddocregion + + // #docregion sorting_guide_sort_parameter_1 + await client + .index('books') + .search('science fiction', SearchQuery(sort: ['price:asc'])); + // #enddocregion + + // #docregion sorting_guide_sort_parameter_2 + await client + .index('books') + .search('butler', SearchQuery(sort: ['author:desc'])); + // #enddocregion + + // #docregion get_sortable_attributes_1 + await client.index('books').getSortableAttributes(); + // #enddocregion + + // #docregion update_sortable_attributes_1 + await client.index('books').updateSortableAttributes(['price', 'author']); + // #enddocregion + + // #docregion reset_sortable_attributes_1 + await client.index('books').resetSortableAttributes(); + // #enddocregion + + // #docregion search_parameter_guide_sort_1 + await client + .index('books') + .search('science fiction', SearchQuery(sort: ['price:asc'])); + // #enddocregion + + // #docregion geosearch_guide_filter_settings_1 + await client.index('restaurants').updateFilterableAttributes(['_geo']); + // #enddocregion + + // #docregion geosearch_guide_filter_usage_1 + await client.index('restaurants').search( + '', + SearchQuery( + filterExpression: Meili.geoRadius( + (lat: 45.472735, lng: 9.184019), + 2000, + ), + ), + ); + // #enddocregion + + // #docregion geosearch_guide_filter_usage_2 + await client.index('restaurants').search( + '', + SearchQuery( + filterExpression: Meili.and([ + Meili.geoRadius( + (lat: 45.472735, lng: 9.184019), + 2000, + ), + Meili.attr('type').eq('pizza'.toMeiliValue()) + ]), + ), + ); + // #enddocregion + + // #docregion geosearch_guide_sort_settings_1 + await client.index('restaurants').updateSortableAttributes(['_geo']); + // #enddocregion + + // #docregion geosearch_guide_sort_usage_1 + await client.index('restaurants').search( + '', SearchQuery(sort: ['_geoPoint(48.8561446, 2.2978204):asc'])); + // #enddocregion + + // #docregion geosearch_guide_sort_usage_2 + await client.index('restaurants').search( + '', + SearchQuery( + sort: ['_geoPoint(48.8561446, 2.2978204):asc', 'rating:desc'])); + // #enddocregion + + // #docregion get_one_key_1 + await client.getKey('6062abda-a5aa-4414-ac91-ecd7944c0f8d'); + // #enddocregion + + // #docregion get_all_keys_1 + await client.getKeys(params: KeysQuery(limit: 3)); + // #enddocregion + + // #docregion create_a_key_1 + await client.createKey( + description: 'Add documents: Products API key', + actions: ['documents.add'], + indexes: ['products'], + expiresAt: DateTime(2042, 04, 02)); + // #enddocregion + + // #docregion update_a_key_1 + await client.updateKey( + '6062abda-a5aa-4414-ac91-ecd7944c0f8d', + description: 'Manage documents: Products/Reviews API key', + name: 'Products/Reviews API key', + ); + // #enddocregion + + // #docregion delete_a_key_1 + await client.deleteKey('6062abda-a5aa-4414-ac91-ecd7944c0f8d'); + // #enddocregion + + // #docregion search_parameter_guide_crop_marker_1 + await client.index('movies').search( + 'shifu', + SearchQuery( + attributesToCrop: ['overview'], + cropMarker: '[…]', + ), + ); + // #enddocregion + + // #docregion search_parameter_guide_highlight_tag_1 + await client.index('movies').search( + 'winter feast', + SearchQuery( + attributesToHighlight: ['overview'], + highlightPreTag: '', + highlightPostTag: '', + ), + ); + // #enddocregion + + // #docregion geosearch_guide_filter_usage_3 + await client.index('restaurants').search( + '', + SearchQuery( + filter: + '_geoBoundingBox([45.494181, 9.214024], [45.449484, 9.179175])')); + }); + // #enddocregion + // skip this test, since it's only used for generating code samples + }, skip: true); + +// unformatted examples +/* + // #docregion getting_started_search_md + ```dart + await client.index('movies').search('botman'); + ``` + + [About this SDK](https://github.com/meilisearch/meilisearch-dart/) + // #enddocregion + + // #docregion getting_started_add_documents_md + ```bash + dart pub add meilisearch + ``` + + ```dart + import 'package:meilisearch/meilisearch.dart'; + import 'dart:io'; + import 'dart:convert'; + + var client = MeiliSearchClient('http://localhost:7700', 'aSampleMasterKey'); + + final json = await File('movies.json').readAsString(); + + await client.index('movies').addDocumentsJson(json); + ``` + + [About this SDK](https://github.com/meilisearch/meilisearch-dart/) + // #enddocregion +*/ +} diff --git a/test/documents_test.dart b/test/documents_test.dart index b0c7a8ee..8023a8f1 100644 --- a/test/documents_test.dart +++ b/test/documents_test.dart @@ -557,4 +557,49 @@ void main() { }); }); }); + + test( + 'documents code samples', + () async { + // #docregion delete_documents_by_filter_1 + await client.index('movies').deleteDocuments( + DeleteDocumentsQuery( + filterExpression: Meili.or([ + Meili.attr('genres').eq(Meili.value('action')), + Meili.attr('genres').eq(Meili.value('adventure')), + ]), + ), + ); + // #enddocregion + + // #docregion get_documents_1 + await client.index('movies').getDocuments( + params: DocumentsQuery( + limit: 2, + filter: Meili.attr('genres').eq('action'.toMeiliValue()), + ), + ); + // #enddocregion + + // #docregion get_documents_post_1 + await client.index('movies').getDocuments( + params: DocumentsQuery( + filterExpression: Meili.and([ + 'language'.toMeiliAttribute().eq('English'.toMeiliValue()), + Meili.and([ + 'rating'.toMeiliAttribute().gt(3.toMeiliValue()), + Meili.or([ + 'genres'.toMeiliAttribute().eq('Adventure'.toMeiliValue()), + 'genres'.toMeiliAttribute().eq('Fiction'.toMeiliValue()), + ]), + ]), + ]), + fields: ['title', 'genres', 'rating', 'language'], + limit: 3, + ), + ); + // #enddocregion + }, + skip: true, + ); } diff --git a/test/exceptions_test.dart b/test/exceptions_test.dart index 3981a7d1..6975cbff 100644 --- a/test/exceptions_test.dart +++ b/test/exceptions_test.dart @@ -25,7 +25,7 @@ void main() { throwsA(isA().having( (error) => error.toString(), // Actual 'toString() method', // Description of the check - 'MeiliSearchApiError - message: The request returned an invalid status code of 404.', // Expected + contains('404'), // Expected )), ); }); diff --git a/test/filter_builder_test.dart b/test/filter_builder_test.dart index 86431fce..fe962c83 100644 --- a/test/filter_builder_test.dart +++ b/test/filter_builder_test.dart @@ -75,7 +75,7 @@ void main() { }); test("Dates", () { final testData = [ - [DateTime.utc(1999, 12, 14, 18, 53, 56), '945197636000'], + [DateTime.utc(1999, 12, 14, 18, 53, 56), '945197636'], ]; for (var element in testData) { diff --git a/test/models/test_client.dart b/test/models/test_client.dart index a4014528..5f7a036d 100644 --- a/test/models/test_client.dart +++ b/test/models/test_client.dart @@ -1,144 +1,130 @@ -import 'package:collection/collection.dart'; -import 'package:dio/dio.dart'; -import 'package:meilisearch/meilisearch.dart'; - -class TestMeiliSearchClient extends MeiliSearchClient { - TestMeiliSearchClient( - String serverUrl, [ - String? apiKey, - Duration? connectTimeout, - HttpClientAdapter? adapter, - List? interceptors, - ]) : super( - serverUrl, - apiKey, - connectTimeout, - adapter, - interceptors, - ); - - factory TestMeiliSearchClient.withCustomDio( - String serverUrl, { - String? apiKey, - Duration? connectTimeout, - HttpClientAdapter? adapter, - List? interceptors, - }) => - TestMeiliSearchClient( - serverUrl, - apiKey, - connectTimeout, - adapter, - interceptors, - ); - - final usedIndexes = {}; - final usedKeys = {}; - - @override - MeiliSearchIndex index(String uid, {bool deleteWhenDone = true}) { - if (deleteWhenDone) { - usedIndexes.add(uid); - } - return super.index(uid); - } - - @override - Future createIndex(String uid, {String? primaryKey}) { - usedIndexes.add(uid); - return super.createIndex(uid, primaryKey: primaryKey); - } - - @override - Future deleteIndex(String uid) { - usedIndexes.remove(uid); - return super.deleteIndex(uid); - } - - @override - Future> getRawIndex( - String uid, { - bool deleteWhenDone = true, - }) { - return super.getRawIndex(uid).then((value) { - if (deleteWhenDone) { - usedIndexes.add(uid); - } - return value; - }).onError((error, stackTrace) { - usedIndexes.remove(uid); - throw error!; - }); - } - - @override - Future swapIndexes( - List param, { - bool deleteWhenDone = true, - }) { - if (deleteWhenDone) { - usedIndexes.addAll(param.map((e) => e.indexes).flattened); - } - return super.swapIndexes(param); - } - - @override - Future createKey({ - required List indexes, - required List actions, - DateTime? expiresAt, - String? description, - String? uid, - bool deleteWhenDone = true, - }) { - return super - .createKey( - expiresAt: expiresAt, - description: description, - uid: uid, - indexes: indexes, - actions: actions, - ) - .then((value) { - if (deleteWhenDone) { - usedKeys.add(value.key); - } - return value; - }); - } - - @override - Future deleteKey(String key) { - usedKeys.remove(key); - return super.deleteKey(key); - } - - Future disposeUsedResources() async { - final indexesCopy = usedIndexes.toList(); - await Future.wait([ - _deleteUsedIndexes(), - _deleteUsedKeys(), - ]); - if (indexesCopy.isNotEmpty) { - await _deleteTasksForDeletedIndexes(indexesCopy); - } - } - - Future _deleteUsedIndexes() async { - await Future.wait( - usedIndexes.toSet().map((e) => deleteIndex(e)), - ); - } - - Future _deleteUsedKeys() async { - await Future.wait( - usedKeys.toSet().map( - (e) => deleteKey(e).onError((error, stackTrace) => false), - ), - ); - } - - Future _deleteTasksForDeletedIndexes(List indexes) async { - await deleteTasks(params: DeleteTasksQuery(indexUids: indexes)); - } -} +import 'package:collection/collection.dart'; +import 'package:dio/dio.dart'; +import 'package:meilisearch/meilisearch.dart'; + +class TestMeiliSearchClient extends MeiliSearchClient { + TestMeiliSearchClient( + super.serverUrl, [ + super.apiKey, + super.connectTimeout, + super.adapter, + super.interceptors, + ]); + + factory TestMeiliSearchClient.withCustomDio( + String serverUrl, { + String? apiKey, + Duration? connectTimeout, + HttpClientAdapter? adapter, + List? interceptors, + }) => + TestMeiliSearchClient( + serverUrl, + apiKey, + connectTimeout, + adapter, + interceptors, + ); + + final usedIndexes = {}; + final usedKeys = {}; + + @override + MeiliSearchIndex index(String uid, {bool deleteWhenDone = true}) { + if (deleteWhenDone) { + usedIndexes.add(uid); + } + return super.index(uid); + } + + @override + Future createIndex(String uid, {String? primaryKey}) { + usedIndexes.add(uid); + return super.createIndex(uid, primaryKey: primaryKey); + } + + @override + Future deleteIndex(String uid) { + usedIndexes.remove(uid); + return super.deleteIndex(uid); + } + + @override + Future> getRawIndex( + String uid, { + bool deleteWhenDone = true, + }) { + return super.getRawIndex(uid).then((value) { + if (deleteWhenDone) { + usedIndexes.add(uid); + } + return value; + }).onError((error, stackTrace) { + usedIndexes.remove(uid); + throw error!; + }); + } + + @override + Future swapIndexes( + List param, { + bool deleteWhenDone = true, + }) { + if (deleteWhenDone) { + usedIndexes.addAll(param.map((e) => e.indexes).flattened); + } + return super.swapIndexes(param); + } + + @override + Future createKey({ + required List indexes, + required List actions, + DateTime? expiresAt, + String? description, + String? uid, + bool deleteWhenDone = true, + }) { + return super + .createKey( + expiresAt: expiresAt, + description: description, + uid: uid, + indexes: indexes, + actions: actions, + ) + .then((value) { + if (deleteWhenDone) { + usedKeys.add(value.key); + } + return value; + }); + } + + @override + Future deleteKey(String key) { + usedKeys.remove(key); + return super.deleteKey(key); + } + + Future disposeUsedResources() async { + await Future.wait([ + _deleteUsedIndexes(), + _deleteUsedKeys(), + ]); + } + + Future _deleteUsedIndexes() async { + await Future.wait( + usedIndexes.toSet().map((e) => deleteIndex(e)), + ); + } + + Future _deleteUsedKeys() async { + await Future.wait( + usedKeys.toSet().map( + (e) => deleteKey(e).onError((error, stackTrace) => false), + ), + ); + } +} diff --git a/test/multi_index_search_test.dart b/test/multi_index_search_test.dart index 7aabbc7b..64abd3f7 100644 --- a/test/multi_index_search_test.dart +++ b/test/multi_index_search_test.dart @@ -46,4 +46,14 @@ void main() { expect(result.results.last.hits.length, 2); }); }); + + test('code samples', () async { + // #docregion multi_search_1 + await client.multiSearch(MultiSearchQuery(queries: [ + IndexSearchQuery(query: 'pooh', indexUid: 'movies', limit: 5), + IndexSearchQuery(query: 'nemo', indexUid: 'movies', limit: 5), + IndexSearchQuery(query: 'us', indexUid: 'movies_ratings'), + ])); + // #enddocregion + }, skip: true); } diff --git a/test/search_test.dart b/test/search_test.dart index 0fbb14f5..939c897b 100644 --- a/test/search_test.dart +++ b/test/search_test.dart @@ -1,5 +1,4 @@ import 'package:meilisearch/meilisearch.dart'; -import 'package:meilisearch/src/results/experimental_features.dart'; import 'package:test/test.dart'; import 'utils/books.dart'; @@ -449,159 +448,258 @@ void main() { }); }); - group('Experimental', () { - setUpClient(); - late String uid; - late MeiliSearchIndex index; - late ExperimentalFeatures features; - setUp(() async { - features = await client.http.updateExperimentalFeatures( - UpdateExperimentalFeatures( - scoreDetails: true, - vectorStore: true, - ), - ); - expect(features.scoreDetails, true); - expect(features.vectorStore, true); - - uid = randomUid(); - index = await createIndexWithData(uid: uid, data: vectorBooks); - }); - - test('vector search', () async { - final vector = [0, 1, 2]; - final res = await index - .search( - null, - SearchQuery( - vector: vector, - ), - ) - .asSearchResult() - .mapToContainer(); - - expect(res.vector, vector); - expect( - res.hits, - everyElement( - isA>>() - .having( - (p0) => p0.vectors, - 'vectors', - isNotNull, - ) - .having( - (p0) => p0.semanticScore, - 'semanticScore', - isNotNull, - ), - ), - ); - }); - - test('normal search', () async { - final res = await index - .search( - 'The', - SearchQuery( - showRankingScore: true, - showRankingScoreDetails: true, - attributesToHighlight: ['*'], - showMatchesPosition: true, + // Commented because of https://github.com/meilisearch/meilisearch-dart/issues/369 + // group('Experimental', () { + // setUpClient(); + // late String uid; + // late MeiliSearchIndex index; + // late ExperimentalFeatures features; + // setUp(() async { + // features = await client.http.updateExperimentalFeatures( + // UpdateExperimentalFeatures( + // scoreDetails: true, + // vectorStore: true, + // ), + // ); + // expect(features.scoreDetails, true); + // expect(features.vectorStore, true); + + // uid = randomUid(); + // index = await createIndexWithData(uid: uid, data: vectorBooks); + // }); + + // test('vector search', () async { + // final vector = [0, 1, 2]; + // final res = await index + // .search( + // null, + // SearchQuery( + // vector: vector, + // ), + // ) + // .asSearchResult() + // .mapToContainer(); + + // expect(res.vector, vector); + // expect( + // res.hits, + // everyElement( + // isA>>() + // .having( + // (p0) => p0.vectors, + // 'vectors', + // isNotNull, + // ) + // .having( + // (p0) => p0.semanticScore, + // 'semanticScore', + // isNotNull, + // ), + // ), + // ); + // }); + + // test('normal search', () async { + // final res = await index + // .search( + // 'The', + // SearchQuery( + // showRankingScore: true, + // showRankingScoreDetails: true, + // attributesToHighlight: ['*'], + // showMatchesPosition: true, + // ), + // ) + // .asSearchResult() + // .mapToContainer(); + + // final attributeMatcher = isA() + // .having((p0) => p0.src, 'src', allOf(isNotNull, isNotEmpty)) + // .having((p0) => p0.score, 'score', isNotNull) + // .having((p0) => p0.order, 'order', isNotNull) + // .having((p0) => p0.queryWordDistanceScore, 'queryWordDistanceScore', + // isNotNull) + // .having((p0) => p0.attributeRankingOrderScore, + // 'attributeRankingOrderScore', isNotNull); + + // final wordsMatcher = isA() + // .having((p0) => p0.src, 'src', allOf(isNotNull, isNotEmpty)) + // .having((p0) => p0.score, 'score', isNotNull) + // .having((p0) => p0.order, 'order', isNotNull) + // .having((p0) => p0.matchingWords, 'matchingWords', isNotNull) + // .having((p0) => p0.maxMatchingWords, 'maxMatchingWords', isNotNull); + + // final exactnessMatcher = isA() + // .having((p0) => p0.src, 'src', allOf(isNotNull, isNotEmpty)) + // .having((p0) => p0.score, 'score', isNotNull) + // .having((p0) => p0.order, 'order', isNotNull) + // .having( + // (p0) => p0.matchType, + // 'matchType', + // allOf(isNotNull, isNotEmpty), + // ); + + // final typoMatcher = isA() + // .having((p0) => p0.src, 'src', allOf(isNotNull, isNotEmpty)) + // .having((p0) => p0.score, 'score', isNotNull) + // .having((p0) => p0.order, 'order', isNotNull) + // .having((p0) => p0.typoCount, 'typoCount', isNotNull) + // .having((p0) => p0.maxTypoCount, 'maxTypoCount', isNotNull); + + // final proximityMatcher = isA() + // .having((p0) => p0.src, 'src', allOf(isNotNull, isNotEmpty)) + // .having((p0) => p0.score, 'score', isNotNull) + // .having((p0) => p0.order, 'order', isNotNull); + + // final rankingScoreDetailsMatcher = isA() + // .having((p0) => p0.src, 'src', allOf(isNotNull, isNotEmpty)) + // .having((p0) => p0.attribute, 'attribute', attributeMatcher) + // .having((p0) => p0.words, 'words', wordsMatcher) + // .having((p0) => p0.exactness, 'exactness', exactnessMatcher) + // .having((p0) => p0.typo, 'typo', typoMatcher) + // .having((p0) => p0.proximity, 'proximity', proximityMatcher) + // .having( + // (p0) => p0.customRules, 'customRules', allOf(isNotNull, isEmpty)); + + // expect(res.hits.length, 2); + + // expect( + // res.hits, + // everyElement( + // isA>>() + // .having( + // (p0) => p0.formatted, + // 'formatted', + // allOf(isNotNull, isNotEmpty, contains('id')), + // ) + // .having( + // (p0) => p0.matchesPosition, + // 'matchesPosition', + // allOf(isNotNull, isNotEmpty, containsPair('title', isNotEmpty)), + // ) + // .having( + // (p0) => p0.parsed, + // 'parsed', + // isNotEmpty, + // ) + // .having( + // (p0) => p0.src, + // 'src', + // isNotEmpty, + // ) + // .having( + // (p0) => p0.rankingScore, + // 'rankingScore', + // isNotNull, + // ) + // .having( + // (p0) => p0.rankingScoreDetails, + // 'rankingScoreDetails', + // rankingScoreDetailsMatcher, + // ) + // .having( + // (p0) => p0.vectors, 'vectors', allOf(isNotNull, isNotEmpty)) + // .having((p0) => p0.semanticScore, 'semanticScore', isNull), + // ), + // ); + // }); + // }); + + test('search code samples', () async { + // #docregion search_get_1 + await client.index('movies').search('American ninja'); + // #enddocregion + + // #docregion search_parameter_guide_show_ranking_score_1 + await client + .index('movies') + .search('dragon', SearchQuery(showRankingScore: true)); + // #enddocregion + }, skip: true); + + test('facet search code samples', () async { + // #docregion facet_search_1 + await client.index('books').facetSearch( + FacetSearchQuery( + facetQuery: 'fiction', + facetName: 'genres', + filter: 'rating > 3', + ), + ); + // #enddocregion + + // #docregion facet_search_2 + await client.index('books').updateFaceting( + Faceting( + sortFacetValuesBy: { + 'genres': FacetingSortTypes.count, + }, + ), + ); + // #enddocregion + + // #docregion facet_search_3 + await client.index('books').facetSearch( + FacetSearchQuery( + facetQuery: 'c', + facetName: 'genres', + ), + ); + // #enddocregion + + // #docregion search_parameter_guide_attributes_to_search_on_1 + await client.index('books').facetSearch( + FacetSearchQuery( + facetQuery: 'c', + facetName: 'genres', + ), + ); + // #enddocregion + + // #docregion search_parameter_guide_facet_stats_1 + await client + .index('movie_ratings') + .search('Batman', SearchQuery(facets: ['genres', 'rating'])); + // #enddocregion + + // #docregion faceted_search_1 + await client + .index('books') + .search('', SearchQuery(facets: ['genres', 'rating', 'language'])); + // #enddocregion + + // #docregion filtering_guide_nested_1 + await client.index('movie_ratings').search( + 'thriller', + SearchQuery( + filterExpression: Meili.gte( + //or Meili.attr('rating.users') + //or 'rating.users'.toMeiliAttribute() + Meili.attrFromParts(['rating', 'users']), + Meili.value(90), ), - ) - .asSearchResult() - .mapToContainer(); - - final attributeMatcher = isA() - .having((p0) => p0.src, 'src', allOf(isNotNull, isNotEmpty)) - .having((p0) => p0.score, 'score', isNotNull) - .having((p0) => p0.order, 'order', isNotNull) - .having((p0) => p0.queryWordDistanceScore, 'queryWordDistanceScore', - isNotNull) - .having((p0) => p0.attributeRankingOrderScore, - 'attributeRankingOrderScore', isNotNull); - - final wordsMatcher = isA() - .having((p0) => p0.src, 'src', allOf(isNotNull, isNotEmpty)) - .having((p0) => p0.score, 'score', isNotNull) - .having((p0) => p0.order, 'order', isNotNull) - .having((p0) => p0.matchingWords, 'matchingWords', isNotNull) - .having((p0) => p0.maxMatchingWords, 'maxMatchingWords', isNotNull); - - final exactnessMatcher = isA() - .having((p0) => p0.src, 'src', allOf(isNotNull, isNotEmpty)) - .having((p0) => p0.score, 'score', isNotNull) - .having((p0) => p0.order, 'order', isNotNull) - .having( - (p0) => p0.matchType, - 'matchType', - allOf(isNotNull, isNotEmpty), - ); - - final typoMatcher = isA() - .having((p0) => p0.src, 'src', allOf(isNotNull, isNotEmpty)) - .having((p0) => p0.score, 'score', isNotNull) - .having((p0) => p0.order, 'order', isNotNull) - .having((p0) => p0.typoCount, 'typoCount', isNotNull) - .having((p0) => p0.maxTypoCount, 'maxTypoCount', isNotNull); - - final proximityMatcher = isA() - .having((p0) => p0.src, 'src', allOf(isNotNull, isNotEmpty)) - .having((p0) => p0.score, 'score', isNotNull) - .having((p0) => p0.order, 'order', isNotNull); - - final rankingScoreDetailsMatcher = isA() - .having((p0) => p0.src, 'src', allOf(isNotNull, isNotEmpty)) - .having((p0) => p0.attribute, 'attribute', attributeMatcher) - .having((p0) => p0.words, 'words', wordsMatcher) - .having((p0) => p0.exactness, 'exactness', exactnessMatcher) - .having((p0) => p0.typo, 'typo', typoMatcher) - .having((p0) => p0.proximity, 'proximity', proximityMatcher) - .having( - (p0) => p0.customRules, 'customRules', allOf(isNotNull, isEmpty)); - - expect(res.hits.length, 2); - - expect( - res.hits, - everyElement( - isA>>() - .having( - (p0) => p0.formatted, - 'formatted', - allOf(isNotNull, isNotEmpty, contains('id')), - ) - .having( - (p0) => p0.matchesPosition, - 'matchesPosition', - allOf(isNotNull, isNotEmpty, containsPair('title', isNotEmpty)), - ) - .having( - (p0) => p0.parsed, - 'parsed', - isNotEmpty, - ) - .having( - (p0) => p0.src, - 'src', - isNotEmpty, - ) - .having( - (p0) => p0.rankingScore, - 'rankingScore', - isNotNull, - ) - .having( - (p0) => p0.rankingScoreDetails, - 'rankingScoreDetails', - rankingScoreDetailsMatcher, - ) - .having( - (p0) => p0.vectors, 'vectors', allOf(isNotNull, isNotEmpty)) - .having((p0) => p0.semanticScore, 'semanticScore', isNull), - ), - ); - }); - }); + ), + ); + // #enddocregion + + // #docregion sorting_guide_sort_nested_1 + await client + .index('movie_ratings') + .search('thriller', SearchQuery(sort: ['rating.users:asc'])); + // #enddocregion + + // #docregion search_parameter_guide_page_1 + await client + .index('movies') + .search('', SearchQuery(page: 2)) + .asPaginatedResult(); + // #enddocregion + + // #docregion search_parameter_guide_hitsperpage_1 + await client + .index('movies') + .search('', SearchQuery(hitsPerPage: 15)) + .asPaginatedResult(); + // #enddocregion + }, skip: true); } diff --git a/test/settings_test.dart b/test/settings_test.dart index 0c457e49..c6d98621 100644 --- a/test/settings_test.dart +++ b/test/settings_test.dart @@ -400,4 +400,19 @@ void main() { }); }); }); + + test('code samples', () async { + // #docregion faceted_search_update_settings_1 + await client + .index('movie_ratings') + .updateFilterableAttributes(['genres', 'rating', 'language']); + // #enddocregion + + // #docregion synonyms_guide_1 + await client.index('movies').updateSynonyms({ + 'great': ['fantastic'], + 'fantastic': ['great'], + }); + // #enddocregion + }); } diff --git a/test/swaps_test.dart b/test/swaps_test.dart index f7f4cbda..6ffb2ee0 100644 --- a/test/swaps_test.dart +++ b/test/swaps_test.dart @@ -9,10 +9,15 @@ void main() { setUpClient(); test('swaps indexes from input', () async { - var books = ['books', 'books_new']; - var movies = ['movies', 'movies_new']; + var books = [randomUid('books'), randomUid('books_new')]; + var movies = [randomUid('movies'), randomUid('movies_new')]; var swaps = [SwapIndex(books), SwapIndex(movies)]; + // first create the indexes to be swapped + for (var index in books + movies) { + await client.createIndex(index).waitFor(client: client); + } + var response = await client .swapIndexes( swaps, @@ -20,14 +25,25 @@ void main() { ) .waitFor( client: client, - throwFailed: false, + throwFailed: true, ); expect(response.type, 'indexSwap'); + expect(response.error, null); + expect(response.status, 'succeeded'); expect(response.details!['swaps'], [ {'indexes': books}, {'indexes': movies} ]); }); }); + + test('code samples', () async { + // #docregion swap_indexes_1 + await client.swapIndexes([ + SwapIndex(['indexA', 'indexB']), + SwapIndex(['indexX', 'indexY']), + ]); + // #enddocregion + }, skip: true); } diff --git a/test/tasks_test.dart b/test/tasks_test.dart index 7a94e869..f521aeba 100644 --- a/test/tasks_test.dart +++ b/test/tasks_test.dart @@ -66,4 +66,60 @@ void main() { ); }); }); + + test( + 'code samples', + () async { + // #docregion async_guide_filter_by_date_1 + await client.getTasks( + params: TasksQuery( + afterEnqueuedAt: DateTime(2020, 10, 11, 11, 49, 53), + ), + ); + // #enddocregion + // #docregion async_guide_multiple_filters_1 + await client.getTasks( + params: TasksQuery( + indexUids: ['movies'], + types: ['documentAdditionOrUpdate', 'documentDeletion'], + statuses: ['processing'], + ), + ); + // #enddocregion + // #docregion async_guide_filter_by_ids_1 + await client.getTasks( + params: TasksQuery( + uids: [5, 10, 13], + ), + ); + // #enddocregion + // #docregion async_guide_filter_by_statuses_1 + await client.getTasks( + params: TasksQuery( + statuses: ['failed', 'canceled'], + ), + ); + // #enddocregion + // #docregion async_guide_filter_by_types_1 + await client.getTasks( + params: TasksQuery( + types: ['dumpCreation', 'indexSwap'], + ), + ); + // #enddocregion + // #docregion async_guide_filter_by_index_uids_1 + await client.getTasks(params: TasksQuery(indexUids: ['movies'])); + // #enddocregion + // #docregion delete_tasks_1 + await client.deleteTasks(params: DeleteTasksQuery(uids: [1, 2])); + // #enddocregion + // #docregion cancel_tasks_1 + await client.cancelTasks(params: CancelTasksQuery(uids: [1, 2])); + // #enddocregion + // #docregion async_guide_canceled_by_1 + await client.getTasks(params: TasksQuery(canceledBy: [9, 15])); + // #enddocregion + }, + skip: true, + ); } diff --git a/tool/.gitignore b/tool/.gitignore new file mode 100644 index 00000000..3a857904 --- /dev/null +++ b/tool/.gitignore @@ -0,0 +1,3 @@ +# https://dart.dev/guides/libraries/private-files +# Created by `dart pub` +.dart_tool/ diff --git a/tool/analysis_options.yaml b/tool/analysis_options.yaml new file mode 100644 index 00000000..c0a95325 --- /dev/null +++ b/tool/analysis_options.yaml @@ -0,0 +1,18 @@ +# This file configures the static analysis results for your project (errors, +# warnings, and lints). +# +# This enables the 'recommended' set of lints from `package:lints`. +# This set helps identify many issues that may lead to problems when running +# or consuming Dart code, and enforces writing Dart using a single, idiomatic +# style and format. +# +# If you want a smaller set of lints you can change this to specify +# 'package:lints/core.yaml'. These are just the most critical lints +# (the recommended set includes the core lints). +# The core lints are also what is used by pub.dev for scoring packages. + +include: package:lints/recommended.yaml + +linter: + rules: + avoid_print: false diff --git a/tool/bin/meili.dart b/tool/bin/meili.dart new file mode 100644 index 00000000..201457bf --- /dev/null +++ b/tool/bin/meili.dart @@ -0,0 +1 @@ +export 'package:meili_tool/src/main.dart'; diff --git a/tool/lib/src/command_base.dart b/tool/lib/src/command_base.dart new file mode 100644 index 00000000..bd7853dc --- /dev/null +++ b/tool/lib/src/command_base.dart @@ -0,0 +1,37 @@ +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:meili_tool/src/result.dart'; +import 'package:platform/platform.dart'; +import 'package:path/path.dart' as p; + +abstract class MeiliCommandBase extends Command { + final Directory packageDirectory; + + MeiliCommandBase( + this.packageDirectory, { + this.platform = const LocalPlatform(), + }); + + /// The current platform. + /// + /// This can be overridden for testing. + final Platform platform; + + /// A context that matches the default for [platform]. + p.Context get path => platform.isWindows ? p.windows : p.posix; + // Returns the relative path from [from] to [entity] in Posix style. + /// + /// This should be used when, for example, printing package-relative paths in + /// status or error messages. + String getRelativePosixPath( + FileSystemEntity entity, { + required Directory from, + }) => + p.posix.joinAll(path.split(path.relative(entity.path, from: from.path))); + + String get indentation => ' '; + + bool getBoolArg(String key) { + return (argResults![key] as bool?) ?? false; + } +} diff --git a/tool/lib/src/core.dart b/tool/lib/src/core.dart new file mode 100644 index 00000000..9835d0a0 --- /dev/null +++ b/tool/lib/src/core.dart @@ -0,0 +1,16 @@ +/// Error thrown when a command needs to exit with a non-zero exit code. +/// +/// While there is no specific definition of the meaning of different non-zero +/// exit codes for this tool, commands should follow the general convention: +/// 1: The command ran correctly, but found errors. +/// 2: The command failed to run because the arguments were invalid. +/// >2: The command failed to run correctly for some other reason. Ideally, +/// each such failure should have a unique exit code within the context of +/// that command. +class ToolExit extends Error { + /// Creates a tool exit with the given [exitCode]. + ToolExit(this.exitCode); + + /// The code that the process should exit with. + final int exitCode; +} diff --git a/tool/lib/src/main.dart b/tool/lib/src/main.dart new file mode 100644 index 00000000..ce3dc16e --- /dev/null +++ b/tool/lib/src/main.dart @@ -0,0 +1,53 @@ +import 'dart:io' as io; + +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:file/local.dart'; +import 'package:meili_tool/src/output_utils.dart'; +import 'package:meili_tool/src/result.dart'; + +import 'core.dart'; +import 'update_samples_command.dart'; + +void main(List arguments) { + const FileSystem fileSystem = LocalFileSystem(); + final Directory scriptDir = + fileSystem.file(io.Platform.script.toFilePath()).parent; + final Directory toolsDir = + scriptDir.basename == 'bin' ? scriptDir.parent : scriptDir.parent.parent; + + final Directory meilisearchDirectory = toolsDir.parent; + + final commandRunner = CommandRunner( + 'dart run ./tool/bin/meili.dart', 'Productivity utils for meilisearch.') + ..addCommand(UpdateSamplesCommand(meilisearchDirectory)); + + commandRunner.run(arguments).then((value) { + if (value == null) { + print('MUST output either a success or fail.'); + assert(false); + io.exit(255); + } + switch (value.state) { + case RunState.succeeded: + printSuccess('Success!'); + break; + case RunState.failed: + printError('Failed!'); + if (value.details.isNotEmpty) { + printError(value.details.join('\n')); + } + io.exit(255); + } + }).catchError((Object e) { + final ToolExit toolExit = e as ToolExit; + int exitCode = toolExit.exitCode; + // This should never happen; this check is here to guarantee that a ToolExit + // never accidentally has code 0 thus causing CI to pass. + if (exitCode == 0) { + assert(false); + exitCode = 255; + } + io.exit(exitCode); + }, test: (Object e) => e is ToolExit); +} diff --git a/tool/lib/src/output_utils.dart b/tool/lib/src/output_utils.dart new file mode 100644 index 00000000..7fd39f68 --- /dev/null +++ b/tool/lib/src/output_utils.dart @@ -0,0 +1,44 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io'; + +import 'package:colorize/colorize.dart'; +import 'package:meta/meta.dart'; + +export 'package:colorize/colorize.dart' show Styles; + +/// True if color should be applied. +/// +/// Defaults to autodetecting stdout. +@visibleForTesting +bool useColorForOutput = stdout.supportsAnsiEscapes; + +String _colorizeIfAppropriate(String string, Styles color) { + if (!useColorForOutput) { + return string; + } + return Colorize(string).apply(color).toString(); +} + +/// Prints [message] in green, if the environment supports color. +void printSuccess(String message) { + print(_colorizeIfAppropriate(message, Styles.GREEN)); +} + +/// Prints [message] in yellow, if the environment supports color. +void printWarning(String message) { + print(_colorizeIfAppropriate(message, Styles.YELLOW)); +} + +/// Prints [message] in red, if the environment supports color. +void printError(String message) { + print(_colorizeIfAppropriate(message, Styles.RED)); +} + +/// Returns [message] with escapes to print it in [color], if the environment +/// supports color. +String colorizeString(String message, Styles color) { + return _colorizeIfAppropriate(message, color); +} diff --git a/tool/lib/src/result.dart b/tool/lib/src/result.dart new file mode 100644 index 00000000..331bf433 --- /dev/null +++ b/tool/lib/src/result.dart @@ -0,0 +1,34 @@ +/// Possible outcomes of a command run for a package. +enum RunState { + /// The command succeeded for the package. + succeeded, + + /// The command failed for the package. + failed, +} + +/// The result of a [runForPackage] call. +class PackageResult { + /// A successful result. + PackageResult.success() : this._(RunState.succeeded); + + /// A run that failed. + /// + /// If [errors] are provided, they will be listed in the summary, otherwise + /// the summary will simply show that the package failed. + PackageResult.fail([List errors = const []]) + : this._(RunState.failed, errors); + + const PackageResult._(this.state, [this.details = const []]); + + /// The state the package run completed with. + final RunState state; + + /// Information about the result: + /// - For `succeeded`, this is empty. + /// - For `skipped`, it contains a single entry describing why the run was + /// skipped. + /// - For `failed`, it contains zero or more specific error details to be + /// shown in the summary. + final List details; +} diff --git a/tool/lib/src/update_samples_command.dart b/tool/lib/src/update_samples_command.dart new file mode 100644 index 00000000..54620088 --- /dev/null +++ b/tool/lib/src/update_samples_command.dart @@ -0,0 +1,269 @@ +// Source: https://github.com/flutter/packages/blob/d0411e450a8d94fcb221e8d8eacd3b1f8ca0e2fc/script/tool/lib/src/update_excerpts_command.dart +// but modified to accept yaml files. + +import 'dart:async'; +import 'package:file/file.dart'; +import 'package:http/http.dart' as http; +import 'package:meili_tool/src/command_base.dart'; +import 'package:meili_tool/src/result.dart'; +import 'package:yaml/yaml.dart'; +import 'package:yaml_edit/yaml_edit.dart'; + +class _SourceFile { + final File file; + final List contents; + Map? result; + + _SourceFile({ + required this.file, + required this.contents, + }); +} + +class UpdateSamplesCommand extends MeiliCommandBase { + static const String _failOnChangeFlag = 'fail-on-change'; + static const String _checkRemoteRepoFlag = 'check-remote-repository'; + static const String _generateMissingExcerpts = 'generate-missing-excerpts'; + + UpdateSamplesCommand( + super.packageDirectory, { + super.platform, + }) { + argParser.addFlag( + _failOnChangeFlag, + help: 'Fail if the command does anything. ' + '(Used in CI to ensure excerpts are up to date.)', + ); + argParser.addFlag( + _checkRemoteRepoFlag, + hide: true, + help: + 'Check the remote code samples to see if there are missing/useless keys', + ); + argParser.addFlag( + _generateMissingExcerpts, + hide: true, + help: 'Generate entries that are found in code samples, but not in code', + ); + } + + @override + String get description => + 'Updates .code-samples.meilisearch.yaml, based on code from code files'; + + @override + String get name => 'update-samples'; + + static const docregion = '#docregion'; + static const enddocregion = '#enddocregion'; + final startRegionRegex = RegExp(RegExp.escape(docregion) + r'\s+(?\w+)'); + + @override + Future run() async { + try { + final failOnChange = getBoolArg(_failOnChangeFlag); + final checkRemoteRepo = getBoolArg(_checkRemoteRepoFlag); + final generateMissingExcerpts = getBoolArg(_generateMissingExcerpts); + //read the samples yaml file + final changedKeys = {}; + final File samplesFile = + packageDirectory.childFile('.code-samples.meilisearch.yaml'); + final samplesContentRaw = await samplesFile.readAsString(); + final samplesYaml = loadYaml(samplesContentRaw); + if (samplesYaml is! YamlMap) { + print(samplesYaml.runtimeType); + return PackageResult.fail(['samples yaml must be an YamlMap']); + } + + final newSamplesYaml = YamlEditor(samplesContentRaw); + final foundCodeSamples = {}; + final missingSamples = {}; + final sourceFiles = await _discoverSourceFiles(); + for (var sourceFile in sourceFiles) { + final newValues = _runInFile(sourceFile); + foundCodeSamples.addAll(newValues); + sourceFile.result = newValues; + for (var element in newValues.entries) { + final existingValue = samplesYaml[element.key]; + if (existingValue != null) { + if (existingValue == element.value) { + continue; + } else { + changedKeys[element.key] = element.value; + } + } else { + changedKeys[element.key] = element.value; + } + } + if (failOnChange && changedKeys.isNotEmpty) { + return PackageResult.fail([ + 'found changed keys: ${changedKeys.keys.toList()}', + ]); + } + + if (!failOnChange) { + for (var changedEntry in changedKeys.entries) { + newSamplesYaml.update([changedEntry.key], changedEntry.value); + } + } + } + + for (var entry in samplesYaml.entries) { + if (foundCodeSamples.containsKey(entry.key)) { + continue; + } + missingSamples[entry.key] = entry.value; + } + if (generateMissingExcerpts) { + final targetFile = packageDirectory + .childDirectory('test') + .childFile('missing_samples.dart'); + final sb = StringBuffer(); + + sb.writeln(r"import 'package:meilisearch/meilisearch.dart';"); + sb.writeln('late MeiliSearchClient client;'); + sb.writeln('void main() async {'); + for (var element in missingSamples.entries) { + sb.writeln('// #docregion ${element.key}'); + sb.writeln(element.value); + sb.writeln('// #enddocregion'); + sb.writeln(); + } + sb.writeln('}'); + await targetFile.writeAsString(sb.toString()); + } + + // for now don't check remote repository + if (checkRemoteRepo) { + final fullSamplesYaml = await getFullCorrectSamples(); + final missingEntries = fullSamplesYaml.entries + .where((element) => !samplesYaml.containsKey(element.key)); + final oldEntries = samplesYaml.entries + .where((element) => !fullSamplesYaml.containsKey(element.key)); + if (failOnChange) { + if (missingEntries.isNotEmpty || oldEntries.isNotEmpty) { + return PackageResult.fail([ + if (missingEntries.isNotEmpty) + 'found the following missing entries: ${missingEntries.map((e) => e.key).join('\n')}', + if (oldEntries.isNotEmpty) + 'found the following useless entries: ${oldEntries.map((e) => e.key).join('\n')}', + ]); + } + } else { + for (var element in missingEntries) { + newSamplesYaml.update([element.key], element.value); + } + for (var element in oldEntries) { + newSamplesYaml.remove([element.key]); + } + } + } + + if (!failOnChange && !generateMissingExcerpts) { + await samplesFile.writeAsString(newSamplesYaml.toString()); + } + return PackageResult.success(); + } on PackageResult catch (e) { + return e; + } + } + + Future getFullCorrectSamples() async { + final uri = Uri.parse( + 'https://raw.githubusercontent.com/meilisearch/documentation/main/.code-samples.meilisearch.yaml'); + final data = await http.get(uri); + final parsed = loadYaml(data.body, sourceUrl: uri); + return parsed as YamlMap; + } + + Map _runInFile(_SourceFile file) { + int lineNumber = 0; + String? currentKey; + final keys = []; + final res = {}; + final currentKeyLines = >[]; + for (var line in file.contents) { + lineNumber++; + if (currentKey == null) { + final capture = startRegionRegex.firstMatch(line); + if (capture == null) { + continue; + } + final key = capture.namedGroup('key'); + if (key == null) { + throw PackageResult.fail(['found a #docregion with no key']); + } + if (keys.contains(key)) { + throw PackageResult.fail(['found duplicate keys $key']); + } + keys.add(key); + currentKey = key; + } else { + if (line.contains(enddocregion)) { + final sb = StringBuffer(); + final unindentedLines = + unindentLines(currentKeyLines.map((e) => e.value).toList()) + .join('\n'); + sb.write(unindentedLines); + //add to results. + res[currentKey] = sb.toString(); + + currentKey = null; + currentKeyLines.clear(); + } else { + currentKeyLines.add(MapEntry(lineNumber, line)); + } + } + } + return res; + } + + List unindentLines(List src) { + if (src.isEmpty) { + return src; + } + final ogFirst = src.first; + final trimmedFirst = ogFirst.trimLeft(); + final firstIndentation = ogFirst.length - trimmedFirst.length; + final res = []; + for (var element in src) { + final trimmedLine = element.trimLeft(); + if (trimmedLine.isEmpty) { + continue; + } + var indentation = element.length - trimmedLine.length; + indentation -= firstIndentation; + res.add('${" " * indentation}$trimmedLine'); + } + + return res; + } + + Future> _discoverSourceFiles() async { + final libDir = packageDirectory.childDirectory('lib'); + final testsDir = packageDirectory.childDirectory('test'); + //look in dart files and generate a new yaml file based on the referenced code. + final allDartFiles = [ + ...libDir.listSync(recursive: true), + ...testsDir.listSync(recursive: true), + ].where((element) => element.basename.toLowerCase().endsWith('.dart')); + + final sourceFiles = <_SourceFile>[]; + for (var dartFile in allDartFiles) { + if (dartFile is! File) { + continue; + } + final fileContents = await dartFile.readAsLines(); + if (!fileContents.any((line) => line.contains(docregion))) { + continue; + } + sourceFiles.add( + _SourceFile( + file: dartFile, + contents: fileContents, + ), + ); + } + return sourceFiles; + } +} diff --git a/tool/pubspec.yaml b/tool/pubspec.yaml new file mode 100644 index 00000000..be5bf8b8 --- /dev/null +++ b/tool/pubspec.yaml @@ -0,0 +1,30 @@ +name: meili_tool +description: | + Productivity tools for meilisearch dart repository, + most of this is inspired from the flutter packages repository https://github.com/flutter/packages/. +version: 1.0.0 +repository: https://github.com/meilisearch/meilisearch-dart + +environment: + sdk: '>=3.0.0 <4.0.0' + +# Add regular dependencies here. +dependencies: + lints: ^2.0.0 + test: ^1.21.0 + args: ^2.4.2 + cli_util: ^0.4.0 + file: ^7.0.0 + path: ^1.8.3 + platform: ^3.1.2 + collection: ^1.15.0 + colorize: ^3.0.0 + meta: ^1.10.0 + yaml: ^3.1.2 + yaml_edit: ^2.1.1 + http: ^1.1.0 + +dev_dependencies: + build_runner: ^2.0.3 + matcher: ^0.12.10 + mockito: '>=5.3.2 <=5.4.0'