From 7174273ae75804527a8f4f993159653cc9780aff Mon Sep 17 00:00:00 2001 From: nhathiwala Date: Thu, 18 Nov 2021 19:39:04 +0530 Subject: [PATCH] Implemented Fallback Request & Synonyms --- .github/workflows/dart.yml | 30 +-- CHANGELOG.md | 79 +++++-- lib/algolia.dart | 2 + lib/src/algolia.dart | 100 +++++++- lib/src/batch.dart | 12 +- lib/src/event_snapshot.dart | 2 +- lib/src/index_reference.dart | 68 +++--- lib/src/index_settings.dart | 20 +- lib/src/object_reference.dart | 51 ++--- lib/src/query.dart | 11 +- lib/src/synonyms_reference.dart | 394 ++++++++++++++++++++++++++++++++ lib/src/task.dart | 8 +- lib/src/util/enum_util.dart | 13 ++ pubspec.yaml | 2 +- test/algolia_test.dart | 88 ++++++- 15 files changed, 744 insertions(+), 136 deletions(-) create mode 100644 lib/src/synonyms_reference.dart create mode 100644 lib/src/util/enum_util.dart diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml index 636e4e3..11655e1 100644 --- a/.github/workflows/dart.yml +++ b/.github/workflows/dart.yml @@ -2,7 +2,7 @@ # They are provided by a third-party and are governed by # separate terms of service, privacy policy, and support # documentation. -name: CI +name: Health on: push: branches: [ master ] @@ -63,17 +63,17 @@ jobs: run: dart test - - name: Setup credentials - run: | - mkdir -p ~/.pub-cache - cat < ~/.pub-cache/credentials.json - { - "accessToken":"${{ secrets.OAUTH_ACCESS_TOKEN }}", - "refreshToken":"${{ secrets.OAUTH_REFRESH_TOKEN }}", - "tokenEndpoint":"https://accounts.google.com/o/oauth2/token", - "scopes": [ "openid", "https://www.googleapis.com/auth/userinfo.email" ], - "expiration": 1570721159347 - } - EOF - - name: Publish package - run: pub publish -f \ No newline at end of file + # - name: Setup credentials + # run: | + # mkdir -p ~/.pub-cache + # cat < ~/.pub-cache/credentials.json + # { + # "accessToken":"${{ secrets.OAUTH_ACCESS_TOKEN }}", + # "refreshToken":"${{ secrets.OAUTH_REFRESH_TOKEN }}", + # "tokenEndpoint":"https://accounts.google.com/o/oauth2/token", + # "scopes": [ "openid", "https://www.googleapis.com/auth/userinfo.email" ], + # "expiration": 1570721159347 + # } + # EOF + # - name: Publish package + # run: pub publish -f \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index b0ed598..3803ea7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,35 @@ -## [1.0.3] - Bug fixes +# Changelog +## 1.0.4 +### Implemented Fallback Request + +- [Added] You can now worry less about retry action action on failure, as it just do it for you. In order to guarantee a very high availability, we implemented recommended retry strategy for all API calls on all your read and write actions. (Currently, fallback request is not supported for insight.) +- [Added] `AlgoliaSynonymsReference` now easily set synonyms with just few lines of code + ```dart + // single + algolia.index('contacts').synonyms.save(AlgoliaSynonyms( + objectID: '1', + type: SynonymsType.synonym, + synonyms: ['iphne', 'iphone', 'ipone'], + forwardToReplicas: true, + )); + + // batch + algolia.index('contacts').synonyms.batch([ + AlgoliaSynonyms( + objectID: '1', + type: SynonymsType.synonym, + synonyms: ['iphne', 'iphone', 'ipone'], + forwardToReplicas: true, + ), + ]); + ``` +## 1.0.3 +### Bug fixes - [Bug] queryId from QuerySnapshot was made nullable. -## [1.0.2] - Added support for Facet Values & Insights +## 1.0.2 +### Added support for Facet Values & Insights - [Added] Facet values `AlgoliaFacetValueSnapshot`: Now you can get list of all facet value to implement advance filtering options. @@ -25,7 +52,8 @@ - [Improved] Improved concurrency of snapshot interface by making constructor base multiple mapped value to a getter parameters. -## [1.0.1] - Bug-fixes with improved debugging stability +## 1.0.1 +### Bug-fixes with improved debugging stability - [Bug] faulty assert resolved for checking empty values [#40](https://github.com/knoxpo/dart_algolia/issues/40) - [Bug] Fixed all enum valued query and setting methods. @@ -39,11 +67,13 @@ - [Added] Improved stability for debugging use ``.toString()`` to get working variables of the interface (applicable for all Algolia classes). - [Added] ``.toMap()`` to all data dictionary classes. -## [1.0.0+1] - Stable release with Null-Safety +## 1.0.0+1 +### Stable release with Null-Safety - Implemented ``analysis_options.yaml`` -## [1.0.0] - Stable release with Null-Safety +## 1.0.0 +### Stable release with Null-Safety - [Bug] [#26](https://github.com/knoxpo/dart_algolia/issues/26) - [Added] Add support of ``Null-safety`` @@ -65,16 +95,19 @@ - [Added] Add new error handling class ``AlgoliaError`` - [Upgrade] Bumped up ``http`` version -## [0.1.7] - Bug fixes and added a new property +## 0.1.7 +### Bug fixes and added a new property - [Bug] [#14](https://github.com/knoxpo/dart_algolia/issues/14) Solved few health suggestion, to improve the health of the code. - [Added] Add support for ``facets_stats`` property returned by Algolia query -## [0.1.6+1] - Improve library health +## 0.1.6+1 +### Improve library health - [Bug] Solved few health suggestion, to improve the health of the code. -## [0.1.6] - Added Multi-Query +## 0.1.6 +### Added Multi-Query - [Added] PR implementation of ``multipleQueries`` @@ -94,17 +127,20 @@ await algolia.multipleQueries.addQueries([queryA, queryB]).getObjects(); ``` -## [0.1.5] - Added New Functionalities +## 0.1.5 +### Added New Functionalities - [Bug] Solved a technical reported bug [#11](https://github.com/knoxpo/dart_algolia/issues/11) - [Added] Copy, Move Index functionalities. - [Added] PR implementation of ``replaceAllObjects()`` -## [0.1.4+3] - Improve library health +## 0.1.4+3 +### Improve library health - [Bug] Solved few health suggestion, to improve the health of the code. -## [0.1.4+2] - Added few advance query references and solved bugs +## 0.1.4+2 +### Added few advance query references and solved bugs - [Bug] `.setFacetFilter(dynamic value)` can now accept String or List value. - [Added] AttributeForDistinct (Advance) @@ -112,25 +148,30 @@ - [Added] GetRankingInfo (Advance) - [Added] ClickAnalytics (Advance) -## [0.1.4+1] - Added support facets +## 0.1.4+1 +### Added support facets - Added `facets` to ``AlgoliaQuerySnapshot`` to list facets name with hits count. -## [0.1.3+2] - Implementation & bug solved +## 0.1.3+2 +### Implementation & bug solved - highlightResult [Bug] (commit ref: 0d76d24fe8aa347a0933920afe5ded43bdcbd68b) - snippetResult [Implementation] (commit ref: 0d76d24fe8aa347a0933920afe5ded43bdcbd68b) -## [0.1.3+1] - Added support to manage index settings +## 0.1.3+1 +### Added support to manage index settings - Updated `example.dart`: Added index settings example. - Updated index `.setSettings()` response to `AlgoliaTask`. -## [0.1.3] - Added support to manage index settings +## 0.1.3 +### Added support to manage index settings - Added support to manage index settings (Get & Set), limited to 24 settings parameters, more to be added in newer releases. -## [0.1.2] - Added new query params +## 0.1.2 +### Added new query params - OptionalFilter (Filtering) - NumericFilter (Filtering) @@ -151,11 +192,13 @@ - DisableTypoToleranceOnWords (Typo) - SeparatorsToIndex (Typo) -## [0.1.1] - Added example +## 0.1.1 +### Added example - Bug fixes. - Removed Flutter direct dependency to support universal dart projects. -## [0.1.0] - Initial Release +## 0.1.0 +### Initial Release - Initial release. diff --git a/lib/algolia.dart b/lib/algolia.dart index e9906df..e433d5e 100644 --- a/lib/algolia.dart +++ b/lib/algolia.dart @@ -16,7 +16,9 @@ part 'src/index_settings.dart'; part 'src/index_snapshot.dart'; part 'src/object_reference.dart'; part 'src/object_snapshot.dart'; +part 'src/synonyms_reference.dart'; part 'src/query.dart'; part 'src/query_snapshot.dart'; part 'src/task.dart'; part 'src/util/json_encode.dart'; +part 'src/util/enum_util.dart'; diff --git a/lib/src/algolia.dart b/lib/src/algolia.dart index f3c5441..a8daf63 100644 --- a/lib/src/algolia.dart +++ b/lib/src/algolia.dart @@ -1,5 +1,13 @@ part of algolia; +enum ApiRequestType { + get, + put, + post, + delete, + patch, +} + class Algolia { const Algolia.init({ required this.applicationId, @@ -24,6 +32,10 @@ class Algolia { ); String get _host => 'https://$applicationId-dsn.algolia.net/1/'; + String get _hostWrite => 'https://$applicationId.algolia.net/1/'; + String get _hostFallback1 => 'https://$applicationId-1.algolianet.com/1/'; + String get _hostFallback2 => 'https://$applicationId-2.algolianet.com/1/'; + String get _hostFallback3 => 'https://$applicationId-3.algolianet.com/1/'; String get _insightsHost => 'https://insights.algolia.io/1/'; Map get _headers { @@ -36,6 +48,87 @@ class Algolia { return map; } + Future _apiCall(ApiRequestType requestType, String url, + {dynamic data}) async { + // ignore: prefer_function_declarations_over_variables + final action = (int retry) { + String host = _hostWrite; + if (requestType == ApiRequestType.get && retry == 0) { + host = _host; + } else if (retry == 1) { + host = _hostFallback1; + } else if (retry == 2) { + host = _hostFallback2; + } else if (retry == 3) { + host = _hostFallback3; + } + switch (requestType) { + case ApiRequestType.get: + return http.get( + Uri.parse('$host$url'), + headers: _headers, + ); + case ApiRequestType.post: + return http.post( + Uri.parse('$host$url'), + headers: _headers, + encoding: Encoding.getByName('utf-8'), + body: data != null + ? utf8.encode(json.encode(data, toEncodable: jsonEncodeHelper)) + : null, + ); + case ApiRequestType.put: + return http.put( + Uri.parse('$host$url'), + headers: _headers, + encoding: Encoding.getByName('utf-8'), + body: data != null + ? utf8.encode(json.encode(data, toEncodable: jsonEncodeHelper)) + : null, + ); + case ApiRequestType.patch: + return http.patch( + Uri.parse('$host$url'), + headers: _headers, + encoding: Encoding.getByName('utf-8'), + body: data != null + ? utf8.encode(json.encode(data, toEncodable: jsonEncodeHelper)) + : null, + ); + case ApiRequestType.delete: + return http.delete( + Uri.parse('$host$url'), + headers: _headers, + encoding: Encoding.getByName('utf-8'), + body: data != null + ? utf8.encode(json.encode(data, toEncodable: jsonEncodeHelper)) + : null, + ); + } + }; + try { + var response = await action(0); + return json.decode(response.body); + } catch (error) { + try { + var response = await action(1); + return response; + } catch (error) { + try { + var response = await action(2); + return response; + } catch (error) { + try { + var response = await action(3); + return response; + } catch (error) { + throw {'error': error}; + } + } + } + } + } + Algolia setHeader(String key, String value) { var map = extraHeaders; map[key] = value; @@ -54,10 +147,9 @@ class Algolia { AlgoliaMultiIndexesReference._(this); Future getIndices() async { - var _url = '${_host}indexes'; - var response = await http.get( - Uri.parse(_url), - headers: _headers, + var response = await _apiCall( + ApiRequestType.get, + 'indexes', ); Map body = json.decode(response.body); diff --git a/lib/src/batch.dart b/lib/src/batch.dart index f8dabb3..4d10249 100644 --- a/lib/src/batch.dart +++ b/lib/src/batch.dart @@ -63,14 +63,10 @@ class AlgoliaBatch { var actions = _actions.map((a) => a.toMap()).toList(); - var url = '${algolia._host}indexes/$_index/batch'; - - var response = await http.post( - Uri.parse(url), - headers: algolia._headers, - body: utf8.encode(json - .encode({'requests': actions}, toEncodable: jsonEncodeHelper)), - encoding: Encoding.getByName('utf-8'), + var response = await algolia._apiCall( + ApiRequestType.post, + 'indexes/$_index/batch', + data: {'requests': actions}, ); Map body = json.decode(response.body); diff --git a/lib/src/event_snapshot.dart b/lib/src/event_snapshot.dart index 6193c47..abf6789 100644 --- a/lib/src/event_snapshot.dart +++ b/lib/src/event_snapshot.dart @@ -15,7 +15,7 @@ extension AlgoliaEventTypeExtention on AlgoliaEventType { /// /// Initital implemention by [@algirdasmac](https://github.com/algirdasmac) committed on 15 Jun /// [PR](https://github.com/knoxpo/dart_algolia/pull/56/commits/8dc068ed16f7cf0c6747ec28d0e17fcf7b433f7f) -/// +/// class AlgoliaEvent { AlgoliaEvent({ required this.eventType, diff --git a/lib/src/index_reference.dart b/lib/src/index_reference.dart index 541bd8d..cbdd725 100644 --- a/lib/src/index_reference.dart +++ b/lib/src/index_reference.dart @@ -30,6 +30,16 @@ class AlgoliaIndexReference extends AlgoliaQuery { /// AlgoliaIndexSettings get settings => AlgoliaIndexSettings._(algolia, _index); + /// + /// **Synonyms** + /// + /// Synonyms were originally set via the index settings, and a Get + /// settings call would return all synonyms as part of the + /// settings JSON data. + /// + AlgoliaSynonymsReference get synonyms => + AlgoliaSynonymsReference._(algolia, _index); + AlgoliaObjectReference object([String? path]) { String? objectId; if (path == null) { @@ -76,12 +86,10 @@ class AlgoliaIndexReference extends AlgoliaQuery { String facetQuery = '', int maxFacetHits = 10, }) async { - var url = - '${algolia._host}indexes/$encodedIndex/facets/${Uri.encodeFull(facetName)}/query'; - var response = await http.post( - Uri.parse(url), - headers: algolia._headers, - body: { + var response = await algolia._apiCall( + ApiRequestType.post, + 'indexes/$encodedIndex/facets/${Uri.encodeFull(facetName)}/query', + data: { 'params': params, 'facetQuery': facetQuery, 'maxFacetHits': maxFacetHits, @@ -166,15 +174,12 @@ class AlgoliaIndexReference extends AlgoliaQuery { /// Future> getObjectsByIds( [List objectIds = const []]) async { - var url = '${algolia._host}indexes/*/objects'; final objects = List.generate(objectIds.length, (int i) => {'indexName': index, 'objectID': objectIds[i]}); - final requests = {'requests': objects}; - var response = await http.post( - Uri.parse(url), - headers: algolia._headers, - body: utf8.encode(json.encode(requests, toEncodable: jsonEncodeHelper)), - encoding: Encoding.getByName('utf-8'), + var response = await algolia._apiCall( + ApiRequestType.post, + 'indexes/*/objects', + data: {'requests': objects}, ); Map body = json.decode(response.body); @@ -193,11 +198,9 @@ class AlgoliaIndexReference extends AlgoliaQuery { /// Clear the index referred to by this [AlgoliaIndexReference]. /// Future clearIndex() async { - var url = '${algolia._host}indexes/$encodedIndex/clear'; - var response = await http.post( - Uri.parse(url), - headers: algolia._headers, - encoding: Encoding.getByName('utf-8'), + var response = await algolia._apiCall( + ApiRequestType.post, + 'indexes/$encodedIndex/clear', ); Map body = json.decode(response.body); @@ -236,7 +239,6 @@ class AlgoliaIndexReference extends AlgoliaQuery { required bool copy, List? scopes, }) async { - var url = '${algolia._host}indexes/$encodedIndex/operation'; final data = { 'operation': copy ? 'copy' : 'move', 'destination': destination, @@ -244,11 +246,10 @@ class AlgoliaIndexReference extends AlgoliaQuery { if (scopes != null) { data['scope'] = scopes.map((s) => _scopeToString(s)).toList(); } - var response = await http.post( - Uri.parse(url), - headers: algolia._headers, - encoding: Encoding.getByName('utf-8'), - body: utf8.encode(json.encode(data, toEncodable: jsonEncodeHelper)), + var response = await algolia._apiCall( + ApiRequestType.post, + 'indexes/$encodedIndex/operation', + data: data, ); Map body = json.decode(response.body); @@ -294,10 +295,9 @@ class AlgoliaIndexReference extends AlgoliaQuery { /// Delete the index referred to by this [AlgoliaIndexReference]. /// Future deleteIndex() async { - var url = '${algolia._host}indexes/$encodedIndex'; - var response = await http.delete( - Uri.parse(url), - headers: algolia._headers, + var response = await algolia._apiCall( + ApiRequestType.delete, + 'indexes/$encodedIndex', ); Map body = json.decode(response.body); if (!(response.statusCode >= 200 && response.statusCode < 300)) { @@ -373,15 +373,13 @@ class AlgoliaMultiIndexesReference { 'params': _encodeMap(q.parameters), }); } - var url = '${_algolia._host}indexes/*/queries'; - var response = await http.post( - Uri.parse(url), - headers: _algolia._headers, - body: utf8.encode(json.encode({ + var response = await _algolia._apiCall( + ApiRequestType.post, + 'indexes/*/queries', + data: { 'requests': requests, 'strategy': 'none', - }, toEncodable: jsonEncodeHelper)), - encoding: Encoding.getByName('utf-8'), + }, ); Map body = json.decode(response.body); diff --git a/lib/src/index_settings.dart b/lib/src/index_settings.dart index 7c5c1d8..c5fbc09 100644 --- a/lib/src/index_settings.dart +++ b/lib/src/index_settings.dart @@ -18,10 +18,9 @@ class AlgoliaIndexSettings extends AlgoliaSettings { super._(algolia, indexName); Future> getSettings() async { - var url = '${algolia._host}indexes/$_index/settings'; - var response = await http.get( - Uri.parse(url), - headers: algolia._headers, + var response = await algolia._apiCall( + ApiRequestType.get, + 'indexes/$encodedIndex/settings', ); Map body = json.decode(response.body); @@ -45,6 +44,7 @@ class AlgoliaSettings { final Algolia algolia; final String _index; final Map _parameters; + String get encodedIndex => Uri.encodeFull(_index); AlgoliaSettings _copyWithParameters(Map parameters) { return AlgoliaSettings._( @@ -69,14 +69,10 @@ class AlgoliaSettings { Future setSettings() async { assert( _parameters.keys.isNotEmpty, 'No setting parameter to update found.'); - - var url = '${algolia._host}indexes/$_index/settings'; - var response = await http.put( - Uri.parse(url), - headers: algolia._headers, - body: - utf8.encode(json.encode(_parameters, toEncodable: jsonEncodeHelper)), - encoding: Encoding.getByName('utf-8'), + var response = await algolia._apiCall( + ApiRequestType.put, + 'indexes/$encodedIndex/settings', + data: _parameters, ); Map body = json.decode(response.body); if (!(response.statusCode >= 200 && response.statusCode < 300)) { diff --git a/lib/src/object_reference.dart b/lib/src/object_reference.dart index fed0793..b5d62c9 100644 --- a/lib/src/object_reference.dart +++ b/lib/src/object_reference.dart @@ -23,11 +23,9 @@ class AlgoliaObjectReference { Future getObject() async { assert(_index != null, 'You can\'t get an object without an index.'); assert(_objectId != null, 'You can\'t get an object without an objectID.'); - - var url = '${algolia._host}indexes/$encodedIndex/$encodedObjectID'; - var response = await http.get( - Uri.parse(url), - headers: algolia._headers, + var response = await algolia._apiCall( + ApiRequestType.get, + 'indexes/$encodedIndex/$encodedObjectID', ); Map body = json.decode(response.body); if (!(response.statusCode >= 200 && response.statusCode < 300)) { @@ -43,17 +41,15 @@ class AlgoliaObjectReference { assert(_index != null && _index != '*' && _index != '', 'IndexName is required, but it has `*` multiple flag or `null`.'); - var url = '${algolia._host}indexes/$encodedIndex'; + var url = 'indexes/$encodedIndex'; if (_objectId != null) { url += '/$encodedObjectID'; } - - var response = await http.post( - Uri.parse(url), - headers: algolia._headers, - body: utf8.encode(json.encode(data, toEncodable: jsonEncodeHelper)), - encoding: Encoding.getByName('utf-8'), + var response = await algolia._apiCall( + ApiRequestType.post, + url, + data: data, ); Map body = json.decode(response.body); @@ -75,16 +71,16 @@ class AlgoliaObjectReference { Future updateData(Map data) async { assert(_index != null && _index != '*' && _index != '', 'IndexName is required, but it has `*` multiple flag or `null`.'); - var url = '${algolia._host}indexes/$encodedIndex'; + var url = 'indexes/$encodedIndex'; if (_objectId != null) { url = '$url/$encodedObjectID'; } data['objectID'] = _objectId; - var response = await http.put( - Uri.parse(url), - headers: algolia._headers, - body: utf8.encode(json.encode(data, toEncodable: jsonEncodeHelper)), - encoding: Encoding.getByName('utf-8'), + + var response = await algolia._apiCall( + ApiRequestType.put, + url, + data: data, ); Map body = json.decode(response.body); @@ -118,17 +114,16 @@ class AlgoliaObjectReference { assert(_index != null && _index != '*' && _index != '', 'IndexName is required, but it has `*` multiple flag or `null`.'); - var url = '${algolia._host}indexes/$encodedIndex'; + var url = 'indexes/$encodedIndex'; if (_objectId != null) { url = '$url/$encodedObjectID/partial'; } data['objectID'] = _objectId; data['createIfNotExists'] = createIfNotExists; - var response = await http.put( - Uri.parse(url), - headers: algolia._headers, - body: utf8.encode(json.encode(data, toEncodable: jsonEncodeHelper)), - encoding: Encoding.getByName('utf-8'), + var response = await algolia._apiCall( + ApiRequestType.put, + url, + data: data, ); Map body = json.decode(response.body); if (!(response.statusCode >= 200 && response.statusCode < 300)) { @@ -145,13 +140,13 @@ class AlgoliaObjectReference { assert( _objectId != null, 'You can\'t delete an object without an objectID.'); - var url = '${algolia._host}indexes/$encodedIndex'; + var url = 'indexes/$encodedIndex'; if (_objectId != null) { url = '$url/$encodedObjectID'; } - var response = await http.delete( - Uri.parse(url), - headers: algolia._headers, + var response = await algolia._apiCall( + ApiRequestType.delete, + url, ); Map body = json.decode(response.body); if (!(response.statusCode >= 200 && response.statusCode < 300)) { diff --git a/lib/src/query.dart b/lib/src/query.dart index 2b9f125..d99f47e 100644 --- a/lib/src/query.dart +++ b/lib/src/query.dart @@ -73,13 +73,10 @@ class AlgoliaQuery { 'attributesToRetrieve': const ['*'] }); } - var url = '${algolia._host}indexes/$_index/query'; - var response = await http.post( - Uri.parse(url), - headers: algolia._headers, - body: - utf8.encode(json.encode(_parameters, toEncodable: jsonEncodeHelper)), - encoding: Encoding.getByName('utf-8'), + var response = await algolia._apiCall( + ApiRequestType.post, + 'indexes/$_index/query', + data: _parameters, ); Map body = json.decode(response.body); if (!(response.statusCode >= 200 && response.statusCode < 300)) { diff --git a/lib/src/synonyms_reference.dart b/lib/src/synonyms_reference.dart new file mode 100644 index 0000000..d1b7aba --- /dev/null +++ b/lib/src/synonyms_reference.dart @@ -0,0 +1,394 @@ +part of algolia; + +enum SynonymsType { + synonym, + onewaysynonym, + altcorrection1, + altcorrection2, + placeholder, +} + +extension ExtensionDietaryType on SynonymsType { + String toMap() { + return toString().split('.').last; + } + + String get label { + return toMap().split(RegExp(r'(?=[A-Z])')).join(' '); + } +} + +/// +/// **AlgoliaSynonymsReference** +/// +/// A AlgoliaSynonymsReference object can be used for adding object's synonyms, getting +/// synonyms references, manage the index synonyms for objects. +/// +class AlgoliaSynonymsReference { + const AlgoliaSynonymsReference._(this.algolia, this.index); + final String index; + final Algolia algolia; + + /// + /// ID of the referenced index. + /// + String get encodedIndex => Uri.encodeFull(index); + + /// + /// **Search synonyms** + /// + /// Search or browse all synonyms, optionally filtering them by type. + /// + /// - `query`: Search for specific synonyms matching this string. Use an + /// empty string (default) to browse all synonyms. + /// + /// - `type`: Only search for specific types of synonyms. Multiple types + /// can be specified using a comma-separated list. Possible values are: + /// synonym, onewaysynonym, altcorrection1, altcorrection2, placeholder. + /// + /// - `page`: Number of the page to retrieve (zero-based). + /// + /// - `hitsPerPage`: Maximum number of synonym objects to retrieve. + /// + Future> search({ + String? query, + SynonymsType? type, + int page = 0, + int hitsPerPage = 100, + }) async { + var data = { + 'query': query, + 'type': type, + 'page': page, + 'hitsPerPage': hitsPerPage, + }; + data.removeWhere((key, value) => value == null); + var response = await algolia._apiCall( + ApiRequestType.post, + 'indexes/$encodedIndex/synonyms/search', + data: data, + ); + Map body = json.decode(response.body); + if (!(response.statusCode >= 200 && response.statusCode < 500)) { + throw AlgoliaError._(body, response.statusCode); + } + return body; + } + + /// + /// **Save Synonym** + /// + /// Create a new synonym object or update the existing synonym object with + /// the given object ID. + /// The body of the request must be a JSON object representing the + /// synonyms. It must contain the following attributes: + /// - `objectID` (string): Unique identifier of the synonym object to be created or updated. + /// - `type` (string): Type of the synonym object (see below). + /// + /// The rest of the body depends on the type of synonyms to add: + /// --- + /// ### `synonym` Multi-way synonyms (a.k.a. “regular synonyms”). + /// A set of words or phrases that are all substitutable to one another. Any query containing + /// one of them can match records containing any of them. The body must contain the following + /// fields: + /// - `synonyms` (array of strings): Words or phrases to be considered equivalent. + /// --- + /// ### `onewaysynonym` One-way synonym. + /// Alternative matches for a given input. If the input appears + /// inside a query, it will match records containing any of the defined synonyms. The opposite + /// is not true: if a synonym appears in a query, it will not match records containing the + /// input, nor the other synonyms. The body must contain the following fields: + /// - `input` (string): Word or phrase to appear in query strings. + /// - `synonyms` (array of strings): Words or phrases to be matched in records. + /// --- + /// ### `altcorrection1`, `altcorrection2` Alternative corrections. + /// Same as a one-way synonym, except that when matched, they will count as 1 (respectively 2) + /// typos in the ranking formula. The body must contain the following fields: + /// - `word` (string): Word or phrase to appear in query strings. + /// - `corrections` (array of strings): Words to be matched in records. Phrases (multiple-word + /// synonyms) are not supported. + /// --- + /// ### `placeholder` Placeholder: + /// A placeholder is a special text token that is placed inside records and can match many + /// inputs. The body must contain the following fields: + /// - `placeholder` (string): Token to be put inside records. + /// - `replacements` (array of strings): List of query words that will match the token. + /// + /// --- + /// `forwardToReplicas`: (URL parameter) Replicate the new/updated synonym set to all replica + /// indices. [default: false] + /// + Future save(AlgoliaSynonyms synonyms) async { + var response = await algolia._apiCall( + ApiRequestType.put, + 'indexes/$encodedIndex/synonyms/${synonyms.objectID}?forwardToReplicas=${synonyms.forwardToReplicas}', + data: synonyms.toMap(), + ); + Map body = json.decode(response.body); + + if (!(response.statusCode >= 200 && response.statusCode < 300)) { + throw AlgoliaError._(body, response.statusCode); + } + return AlgoliaTask._(algolia, index, body); + } + + /// + /// **Save synonyms (Batch)** + /// + /// Create/update multiple synonym objects at once, potentially replacing the entire list of + /// synonyms if `replaceExistingSynonyms` is true. + /// + Future batch(List synonyms) async { + var response = await algolia._apiCall( + ApiRequestType.post, + 'indexes/$encodedIndex/synonyms/batch', + data: synonyms.map((e) => e.toMap()).toList(), + ); + Map body = json.decode(response.body); + if (!(response.statusCode >= 200 && response.statusCode < 300)) { + throw AlgoliaError._(body, response.statusCode); + } + return AlgoliaTask._(algolia, index, body); + } + + /// + /// **Get synonym** + /// + /// Fetch a synonym object identified by its `objectID`. + /// + Future> getByObjectId(String objectID) async { + var response = await algolia._apiCall( + ApiRequestType.get, + 'indexes/$encodedIndex/synonyms/$objectID', + ); + Map body = json.decode(response.body); + + if (!(response.statusCode >= 200 && response.statusCode < 300)) { + throw AlgoliaError._(body, response.statusCode); + } + return body; + } + + /// + /// **Clear all synonyms** + /// + /// Delete all synonyms from the index. + /// + Future clear() async { + var response = await algolia._apiCall( + ApiRequestType.post, + 'indexes/$encodedIndex/synonyms/clear', + ); + Map body = json.decode(response.body); + + if (!(response.statusCode >= 200 && response.statusCode < 300)) { + throw AlgoliaError._(body, response.statusCode); + } + return AlgoliaTask._(algolia, index, body); + } + + /// + /// **Delete synonym** + /// + /// Delete a single synonyms set, identified by the given `objectID`. + /// + Future delete(String objectID) async { + var response = await algolia._apiCall( + ApiRequestType.post, + 'indexes/$encodedIndex/synonyms/$objectID', + ); + Map body = json.decode(response.body); + + if (!(response.statusCode >= 200 && response.statusCode < 300)) { + throw AlgoliaError._(body, response.statusCode); + } + return AlgoliaTask._(algolia, index, body); + } +} + +/// +/// The body of the request must be a JSON object representing the +/// synonyms. It must contain the following attributes: +/// - `objectID` (string): Unique identifier of the synonym object to be created or updated. +/// - `type` (string): Type of the synonym object (see below). +/// +/// The rest of the body depends on the type of synonyms to add: +/// --- +/// ### `synonym` Multi-way synonyms (a.k.a. “regular synonyms”). +/// A set of words or phrases that are all substitutable to one another. Any query containing +/// one of them can match records containing any of them. The body must contain the following +/// fields: +/// - `synonyms` (array of strings): Words or phrases to be considered equivalent. +/// --- +/// ### `onewaysynonym` One-way synonym. +/// Alternative matches for a given input. If the input appears +/// inside a query, it will match records containing any of the defined synonyms. The opposite +/// is not true: if a synonym appears in a query, it will not match records containing the +/// input, nor the other synonyms. The body must contain the following fields: +/// - `input` (string): Word or phrase to appear in query strings. +/// - `synonyms` (array of strings): Words or phrases to be matched in records. +/// --- +/// ### `altcorrection1`, `altcorrection2` Alternative corrections. +/// Same as a one-way synonym, except that when matched, they will count as 1 (respectively 2) +/// typos in the ranking formula. The body must contain the following fields: +/// - `word` (string): Word or phrase to appear in query strings. +/// - `corrections` (array of strings): Words to be matched in records. Phrases (multiple-word +/// synonyms) are not supported. +/// --- +/// ### `placeholder` Placeholder: +/// A placeholder is a special text token that is placed inside records and can match many +/// inputs. The body must contain the following fields: +/// - `placeholder` (string): Token to be put inside records. +/// - `replacements` (array of strings): List of query words that will match the token. +/// +/// --- +/// `forwardToReplicas`: (URL parameter) Replicate the new/updated synonym set to all replica +/// indices. [default: false] +/// +class AlgoliaSynonyms { + final String objectID; + final SynonymsType type; + final List? synonyms; + final List? corrections; + final List? replacements; + final String? input; + final String? word; + final String? placeholder; + final bool forwardToReplicas; + const AlgoliaSynonyms({ + required this.objectID, + required this.type, + this.synonyms, + this.corrections, + this.replacements, + this.input, + this.word, + this.placeholder, + this.forwardToReplicas = false, + }) : assert( + (type == SynonymsType.synonym && synonyms != null) || + type != SynonymsType.synonym, + '`synonyms` (array of strings): Words or phrases to be considered equivalent.'), + assert( + (type == SynonymsType.onewaysynonym && + synonyms != null && + input != null) || + type != SynonymsType.onewaysynonym, + ' - `input` (string): Word or phrase to appear in query strings. \n - `synonyms` (array of strings): Words or phrases to be matched in records.'), + assert( + (type == SynonymsType.altcorrection1 && + corrections != null && + word != null) || + type != SynonymsType.altcorrection1, + '- `word` (string): Word or phrase to appear in query strings. \n - `corrections` (array of strings): Words to be matched in records. Phrases (multiple-word synonyms) are not supported.'), + assert( + (type == SynonymsType.altcorrection2 && + corrections != null && + word != null) || + type != SynonymsType.altcorrection2, + '- `word` (string): Word or phrase to appear in query strings. \n - `corrections` (array of strings): Words to be matched in records. Phrases (multiple-word synonyms) are not supported.'), + assert( + (type == SynonymsType.placeholder && + replacements != null && + placeholder != null) || + type != SynonymsType.placeholder, + '- `placeholder` (string): Token to be put inside records. \n - `replacements` (array of strings): List of query words that will match the token.'); + + AlgoliaSynonyms copyWith({ + String? objectID, + SynonymsType? type, + List? synonyms, + List? corrections, + List? replacements, + String? input, + String? word, + String? placeholder, + bool? forwardToReplicas, + }) { + return AlgoliaSynonyms( + objectID: objectID ?? this.objectID, + type: type ?? this.type, + synonyms: synonyms ?? this.synonyms, + corrections: corrections ?? this.corrections, + replacements: replacements ?? this.replacements, + input: input ?? this.input, + word: word ?? this.word, + placeholder: placeholder ?? this.placeholder, + forwardToReplicas: forwardToReplicas ?? this.forwardToReplicas, + ); + } + + Map toMap() { + var val = { + 'objectID': objectID, + 'type': type.toMap(), + 'synonyms': synonyms, + 'corrections': corrections, + 'replacements': replacements, + 'input': input, + 'word': word, + 'placeholder': placeholder, + }; + val.removeWhere((key, value) => value == null); + return val; + } + + factory AlgoliaSynonyms.fromMap(Map map) { + return AlgoliaSynonyms( + objectID: map['objectID'], + type: EnumUtil.fromStringEnum( + SynonymsType.values, map['type']), + synonyms: + map['synonyms'] != null ? List.from(map['synonyms']) : null, + corrections: map['corrections'] != null + ? List.from(map['corrections']) + : null, + replacements: map['replacements'] != null + ? List.from(map['replacements']) + : null, + input: map['input'], + word: map['word'], + placeholder: map['placeholder'], + forwardToReplicas: map['forwardToReplicas'], + ); + } + + String toJson() => json.encode(toMap()); + + factory AlgoliaSynonyms.fromJson(String source) => + AlgoliaSynonyms.fromMap(json.decode(source)); + + @override + String toString() { + return 'AlgoliaSynonyms(objectID: $objectID, type: $type, synonyms: $synonyms, corrections: $corrections, replacements: $replacements, input: $input, word: $word, placeholder: $placeholder, forwardToReplicas: $forwardToReplicas)'; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is AlgoliaSynonyms && + other.objectID == objectID && + other.type == type && + other.synonyms == synonyms && + other.corrections == corrections && + other.replacements == replacements && + other.input == input && + other.word == word && + other.placeholder == placeholder && + other.forwardToReplicas == forwardToReplicas; + } + + @override + int get hashCode { + return objectID.hashCode ^ + type.hashCode ^ + synonyms.hashCode ^ + corrections.hashCode ^ + replacements.hashCode ^ + input.hashCode ^ + word.hashCode ^ + placeholder.hashCode ^ + forwardToReplicas.hashCode; + } +} diff --git a/lib/src/task.dart b/lib/src/task.dart index 67a2084..d704306 100644 --- a/lib/src/task.dart +++ b/lib/src/task.dart @@ -28,11 +28,9 @@ class AlgoliaTask { } Future taskStatus() async { - var url = '${algolia._host}indexes/$_index/task/$taskID'; - - var response = await http.get( - Uri.parse(url), - headers: algolia._headers, + var response = await algolia._apiCall( + ApiRequestType.get, + 'indexes/$_index/task/$taskID', ); Map body = json.decode(response.body); if (!(response.statusCode >= 200 && response.statusCode < 300)) { diff --git a/lib/src/util/enum_util.dart b/lib/src/util/enum_util.dart new file mode 100644 index 0000000..77d441d --- /dev/null +++ b/lib/src/util/enum_util.dart @@ -0,0 +1,13 @@ +part of algolia; + +class EnumUtil { + static T fromStringEnum(Iterable values, String stringType) { + return values.firstWhere( + (f) => f.toString().split('.').last.toString() == stringType, + ); + } + + static String toStringEnum(T enumType) { + return enumType.toString().split('.').last; + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 9db995d..d12f581 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: algolia description: > Algolia is a pure dart SDK, wrapped around Algolia REST API for easy implementation for your Flutter or Dart projects. -version: 1.0.3 +version: 1.0.4 repository: https://github.com/knoxpo/dart_algolia homepage: https://knoxpo.com/knoxpo/dart_algolia diff --git a/test/algolia_test.dart b/test/algolia_test.dart index 32e2708..0263e43 100644 --- a/test/algolia_test.dart +++ b/test/algolia_test.dart @@ -130,6 +130,19 @@ void main() async { ids.addAll([batchIds[3], batchIds[4]]); }); + test('Perform Copy index', () async { + var task = await algolia.instance + .index('contacts') + .copyIndex(destination: 'contacts_alt'); + + await task.waitTask(); + + // Checking if has [AlgoliaTask] + expect(task.runtimeType, AlgoliaTask); + print(task.data); + print('\n\n'); + }); + test('Perform Multiple Queries', () async { var queryA = algolia.instance.index('contacts').query('john'); var queryB = algolia.instance.index('contacts_alt').query('jo'); @@ -1471,6 +1484,76 @@ void main() async { }); }); + group('Algolia Synonyms', () { + test('Perform Adding Object to existing Index.', () async { + var addData = { + 'objectID': '1', + 'name': 'John Smith', + 'contact': '+1 609 123456', + 'email': 'johan@example.com', + 'isDelete': false, + 'status': 'published', + 'createdAt': DateTime.now(), + 'modifiedAt': DateTime.now(), + 'price': 200, + }; + taskAdded = await algolia.instance.index('contacts').addObject(addData); + await taskAdded.waitTask(); + + // Checking if has [AlgoliaTask] + expect(taskAdded.runtimeType, AlgoliaTask); + print(taskAdded.data); + print('\n\n'); + }); + test('Perform adding synonyms to Algolia Object. (single)', () async { + var synonyms = AlgoliaSynonyms( + objectID: '1', + type: SynonymsType.synonym, + synonyms: ['iphne', 'iphone', 'ipone'], + forwardToReplicas: true, + ); + try { + // single + AlgoliaTask task = + await algolia.index('contacts').synonyms.save(synonyms); + expect(task.runtimeType, AlgoliaTask); + print(task.data); + print('\n\n'); + } on AlgoliaError catch (err) { + print(err.error.toString()); + expect(err.runtimeType, AlgoliaError); + } + print('\n\n'); + }); + test('Perform adding synonyms to Algolia Object. (batch)', () async { + var synonyms = AlgoliaSynonyms( + objectID: '1', + type: SynonymsType.synonym, + synonyms: ['iphne', 'iphone', 'ipone'], + forwardToReplicas: true, + ); + try { + // single + AlgoliaTask task = + await algolia.index('contacts').synonyms.batch([synonyms]); + expect(task.runtimeType, AlgoliaTask); + print(task.data); + print('\n\n'); + } on AlgoliaError catch (err) { + print(err.error.toString()); + expect(err.runtimeType, AlgoliaError); + } + print('\n\n'); + }); + test('Perform delete Object to existing Index.', () async { + taskAdded = + await algolia.instance.index('contacts').object('1').deleteObject(); + await taskAdded.waitTask(); + + // Checking if has [AlgoliaTask] + expect(taskAdded.runtimeType, AlgoliaTask); + }); + }); group('insights', () { test('Perform pushing event to Algolia Insights.', () async { var event = AlgoliaEvent( @@ -1480,8 +1563,9 @@ void main() async { userToken: 'user123', ); try { - await algolia.instance - .pushEvents([event]).then((_) => print('Event push completed')); + await algolia.instance.pushEvents([event]); + print('Event push completed'); + print('\n\n'); } on AlgoliaError catch (err) { print(err.error.toString()); expect(err.runtimeType, AlgoliaError);