diff --git a/.gitignore b/.gitignore index a8f80d2..818160f 100644 --- a/.gitignore +++ b/.gitignore @@ -85,4 +85,6 @@ pubspec.lock *.mocks.dart # Ignore test coverage -coverage/ \ No newline at end of file +coverage/ + +/typesense-data \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c339249..b5db72a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,9 +1,87 @@ # Contribution -[Feedback][feedback] and [Pull Requests][pr] are most welcome! +Thanks for your interest in contributing! [Feedback][feedback] and [Pull Requests][pr] are most welcome! -Also, you can join our [Slack Community][invite] and say hello 👋️! +Also, you can join our [Slack Community][invite] and say hello 👋️! [feedback]: https://github.com/typesense/typesense-dart/issues/new/choose [pr]: https://github.com/typesense/typesense-dart/pulls -[invite]: https://join.slack.com/t/typesense-community/shared_invite/zt-mx4nbsbn-AuOL89O7iBtvkz136egSJg \ No newline at end of file +[invite]: https://join.slack.com/t/typesense-community/shared_invite/zt-mx4nbsbn-AuOL89O7iBtvkz136egSJg + +## Prerequisites + +- Dart SDK 3.6.1+ +- A running Typesense server for testing (Docker is recommended) + +You can override the default testing server connection via the following environment variables: + +| Variable | Default | +| -------------------- | ----------- | +| `TYPESENSE_HOST` | `127.0.0.1` | +| `TYPESENSE_PORT` | `8108` | +| `TYPESENSE_PROTOCOL` | `http` | +| `TYPESENSE_API_KEY` | `xyz` | + +## Getting started + +Clone the repository: + +```sh +git clone https://github.com/typesense/typesense-dart.git +cd typesense-dart +``` + +Install dependencies: + +```sh +dart pub get +``` + +## Running tests + +Generate test mocks: + +```shell +dart run build_runner build +``` + +Start the local Typesense server (via Docker): + +```sh +docker compose up -d +``` + +Run all tests: + +```sh +dart test +``` + +Stop the Typesense server: + +```sh +docker compose down +``` + +## Formatting + +```sh +dart format . +``` + +## Static analysis + +```sh +dart analyze +``` + +## Before submitting + +Please ensure: + +- All tests pass +- Code is formatted `dart format .` +- `dart analyze` reports no issues +- New features include tests + +Thank you! diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..24b0fa7 --- /dev/null +++ b/compose.yml @@ -0,0 +1,9 @@ +services: + typesense: + image: typesense/typesense:30.2 + restart: on-failure + ports: + - '8108:8108' + volumes: + - ./typesense-data:/data + command: '--data-dir /data --api-key=xyz --enable-cors' diff --git a/lib/src/curation_sets.dart b/lib/src/curation_sets.dart index aa5842b..91dcc6b 100644 --- a/lib/src/curation_sets.dart +++ b/lib/src/curation_sets.dart @@ -12,8 +12,8 @@ class CurationSets { Future> retrieve() async { final response = await _apiCall.getList(resourcepath); return response - .map((item) => - CurationSetsListEntrySchema.fromJson(Map.from(item))) + .map((item) => CurationSetsListEntrySchema.fromJson( + Map.from(item))) .toList(); } diff --git a/lib/src/models/analytics.dart b/lib/src/models/analytics.dart index 4b8c18f..7178f3a 100644 --- a/lib/src/models/analytics.dart +++ b/lib/src/models/analytics.dart @@ -60,8 +60,8 @@ class AnalyticsEventsRetrieveSchema { factory AnalyticsEventsRetrieveSchema.fromJson(Map json) => AnalyticsEventsRetrieveSchema( events: (json['events'] as List? ?? []) - .map((item) => - AnalyticsEventItemSchema.fromJson(Map.from(item as Map))) + .map((item) => AnalyticsEventItemSchema.fromJson( + Map.from(item as Map))) .toList(), ); } @@ -96,7 +96,8 @@ class AnalyticsStatus { this.docCounterEvents, }); - factory AnalyticsStatus.fromJson(Map json) => AnalyticsStatus( + factory AnalyticsStatus.fromJson(Map json) => + AnalyticsStatus( popularPrefixQueries: json['popular_prefix_queries'] as int?, nohitsPrefixQueries: json['nohits_prefix_queries'] as int?, logPrefixQueries: json['log_prefix_queries'] as int?, @@ -260,8 +261,8 @@ class AnalyticsRulesRetrieveSchema { factory AnalyticsRulesRetrieveSchema.fromJson(Map json) => AnalyticsRulesRetrieveSchema( rules: (json['rules'] as List? ?? []) - .map((item) => - AnalyticsRuleSchema.fromJson(Map.from(item as Map))) + .map((item) => AnalyticsRuleSchema.fromJson( + Map.from(item as Map))) .toList(), ); } diff --git a/lib/src/models/conversations.dart b/lib/src/models/conversations.dart index 8b926ad..521ad6f 100644 --- a/lib/src/models/conversations.dart +++ b/lib/src/models/conversations.dart @@ -56,8 +56,8 @@ class ConversationsRetrieveSchema { factory ConversationsRetrieveSchema.fromJson(Map json) => ConversationsRetrieveSchema( conversations: (json['conversations'] as List? ?? []) - .map((item) => - ConversationSchema.fromJson(Map.from(item as Map))) + .map((item) => ConversationSchema.fromJson( + Map.from(item as Map))) .toList(), ); } diff --git a/lib/src/models/curation_sets.dart b/lib/src/models/curation_sets.dart index 8a8f663..fcca1d2 100644 --- a/lib/src/models/curation_sets.dart +++ b/lib/src/models/curation_sets.dart @@ -191,8 +191,10 @@ class CurationObjectSchema { Map toJson() => { 'id': id, if (rule != null) 'rule': rule!.toJson(), - if (includes != null) 'includes': includes!.map((e) => e.toJson()).toList(), - if (excludes != null) 'excludes': excludes!.map((e) => e.toJson()).toList(), + if (includes != null) + 'includes': includes!.map((e) => e.toJson()).toList(), + if (excludes != null) + 'excludes': excludes!.map((e) => e.toJson()).toList(), if (filterBy != null) 'filter_by': filterBy, if (sortBy != null) 'sort_by': sortBy, if (replaceQuery != null) 'replace_query': replaceQuery, @@ -230,8 +232,8 @@ class CurationSetSchema { CurationSetSchema( name: json['name'] as String?, items: (json['items'] as List? ?? []) - .map((item) => - CurationObjectSchema.fromJson(Map.from(item as Map))) + .map((item) => CurationObjectSchema.fromJson( + Map.from(item as Map))) .toList(), ); } @@ -249,8 +251,8 @@ class CurationSetsListEntrySchema { CurationSetsListEntrySchema( name: json['name'] as String, items: (json['items'] as List? ?? []) - .map((item) => - CurationObjectSchema.fromJson(Map.from(item as Map))) + .map((item) => CurationObjectSchema.fromJson( + Map.from(item as Map))) .toList(), ); } @@ -271,7 +273,8 @@ class CurationItemDeleteResponseSchema { CurationItemDeleteResponseSchema({required this.id}); - factory CurationItemDeleteResponseSchema.fromJson(Map json) => + factory CurationItemDeleteResponseSchema.fromJson( + Map json) => CurationItemDeleteResponseSchema( id: json['id'] as String, ); diff --git a/lib/src/models/stopwords.dart b/lib/src/models/stopwords.dart index b80536b..a7014a3 100644 --- a/lib/src/models/stopwords.dart +++ b/lib/src/models/stopwords.dart @@ -29,8 +29,7 @@ class StopwordSchema { factory StopwordSchema.fromJson(Map json) { final dynamic stopwordsValue = json['stopwords']; if (!json.containsKey('id') && stopwordsValue is Map) { - return StopwordSchema.fromJson( - Map.from(stopwordsValue)); + return StopwordSchema.fromJson(Map.from(stopwordsValue)); } return StopwordSchema( diff --git a/lib/src/services/api_call.dart b/lib/src/services/api_call.dart index 3acaa2f..535d2cc 100644 --- a/lib/src/services/api_call.dart +++ b/lib/src/services/api_call.dart @@ -200,10 +200,9 @@ class ApiCall extends BaseApiCall> { /// raised. @override @override - Map decode(String responseBody) => - responseBody.isEmpty - ? {} - : json.decode(responseBody) as Map; + Map decode(String responseBody) => responseBody.isEmpty + ? {} + : json.decode(responseBody) as Map; List decodeList(String responseBody) => responseBody.isEmpty ? [] : json.decode(responseBody) as List; diff --git a/lib/src/services/request_cache.dart b/lib/src/services/request_cache.dart index 75fef58..d02709b 100644 --- a/lib/src/services/request_cache.dart +++ b/lib/src/services/request_cache.dart @@ -60,7 +60,6 @@ class RequestCache { bool _isCacheValidList(_CachedListResult cache) => cache.validTill.difference(DateTime.now()) > Duration.zero; - } class _CachedResult { diff --git a/lib/src/synonyms.dart b/lib/src/synonyms.dart index 91adb84..1ba10ea 100644 --- a/lib/src/synonyms.dart +++ b/lib/src/synonyms.dart @@ -28,8 +28,8 @@ class Synonyms { Future> upsert( String synonymId, Map params) async { _warnDeprecated(); - final response = await _apiCall.put('$_endpointPath/$synonymId', - bodyParameters: params); + final response = + await _apiCall.put('$_endpointPath/$synonymId', bodyParameters: params); return Map.from(response); } diff --git a/test/analytics_integration_test.dart b/test/analytics_integration_test.dart index e6bfd08..cf3cc55 100644 --- a/test/analytics_integration_test.dart +++ b/test/analytics_integration_test.dart @@ -9,8 +9,7 @@ void main() { final host = env['TYPESENSE_HOST'] ?? '127.0.0.1'; final port = int.tryParse(env['TYPESENSE_PORT'] ?? '8108') ?? 8108; final protocolValue = env['TYPESENSE_PROTOCOL'] ?? 'http'; - final protocol = - protocolValue == 'https' ? Protocol.https : Protocol.http; + final protocol = protocolValue == 'https' ? Protocol.https : Protocol.http; late Client client; const companiesCollection = 'companies'; @@ -48,28 +47,28 @@ void main() { test('analytics rules create, update, retrieve, delete', () async { final created = await client.analytics.rules().create( - AnalyticsRuleCreateSchema( - name: ruleName, - type: 'nohits_queries', - collection: companiesCollection, - eventType: 'search', - params: AnalyticsRuleParams( - destinationCollection: queriesCollection, - limit: 1000, - ), - ), - ); + AnalyticsRuleCreateSchema( + name: ruleName, + type: 'nohits_queries', + collection: companiesCollection, + eventType: 'search', + params: AnalyticsRuleParams( + destinationCollection: queriesCollection, + limit: 1000, + ), + ), + ); expect(created, isA()); final updated = await client.analytics.rules().upsert( - ruleName, - AnalyticsRuleUpsertSchema( - params: AnalyticsRuleParams( - destinationCollection: queriesCollection, - limit: 500, - ), - ), - ); + ruleName, + AnalyticsRuleUpsertSchema( + params: AnalyticsRuleParams( + destinationCollection: queriesCollection, + limit: 500, + ), + ), + ); expect(updated, isA()); expect(updated.name, equals(ruleName)); @@ -132,14 +131,14 @@ void main() { } catch (_) {} await client.analytics.rules().create( - AnalyticsRuleCreateSchema( - name: ruleName, - type: 'log', - collection: companiesCollection, - eventType: 'click', - params: AnalyticsRuleParams(), - ), - ); + AnalyticsRuleCreateSchema( + name: ruleName, + type: 'log', + collection: companiesCollection, + eventType: 'click', + params: AnalyticsRuleParams(), + ), + ); final event = AnalyticsEventCreateSchema( name: ruleName, @@ -153,10 +152,10 @@ void main() { expect(created.ok, isTrue); final retrieved = await client.analytics.events().retrieve( - userId: 'user-1', - name: ruleName, - n: 10, - ); + userId: 'user-1', + name: ruleName, + n: 10, + ); expect(retrieved, isA()); final status = await client.analytics.events().status(); diff --git a/test/conversations_models_integration_test.dart b/test/conversations_models_integration_test.dart index f210f11..a422a2e 100644 --- a/test/conversations_models_integration_test.dart +++ b/test/conversations_models_integration_test.dart @@ -9,8 +9,7 @@ void main() { final host = env['TYPESENSE_HOST'] ?? '127.0.0.1'; final port = int.tryParse(env['TYPESENSE_PORT'] ?? '8108') ?? 8108; final protocolValue = env['TYPESENSE_PROTOCOL'] ?? 'http'; - final protocol = - protocolValue == 'https' ? Protocol.https : Protocol.http; + final protocol = protocolValue == 'https' ? Protocol.https : Protocol.http; final openAiApiKey = env['OPENAI_API_KEY']; final historyCollection = env['CONVERSATION_HISTORY_COLLECTION'] ?? 'conversations_history'; @@ -61,8 +60,7 @@ void main() { final created = await client.conversationsModels.create( ConversationModelCreateSchema( id: modelId, - modelName: - env['CONVERSATION_MODEL_NAME'] ?? 'openai/gpt-3.5-turbo', + modelName: env['CONVERSATION_MODEL_NAME'] ?? 'openai/gpt-3.5-turbo', apiKey: openAiApiKey, maxBytes: 16384, historyCollection: historyCollection, @@ -76,16 +74,15 @@ void main() { expect(retrieved.id, equals(modelId)); final updated = await client.conversationModel(modelId).update( - ConversationModelCreateSchema( - id: modelId, - modelName: - env['CONVERSATION_MODEL_NAME'] ?? 'openai/gpt-3.5-turbo', - apiKey: openAiApiKey, - maxBytes: 16384, - systemPrompt: 'This is meant for testing purposes', - historyCollection: historyCollection, - ), - ); + ConversationModelCreateSchema( + id: modelId, + modelName: env['CONVERSATION_MODEL_NAME'] ?? 'openai/gpt-3.5-turbo', + apiKey: openAiApiKey, + maxBytes: 16384, + systemPrompt: 'This is meant for testing purposes', + historyCollection: historyCollection, + ), + ); expect(updated, isA()); final list = await client.conversationsModels.retrieve(); diff --git a/test/curation_sets_integration_test.dart b/test/curation_sets_integration_test.dart index c31099c..df51d4e 100644 --- a/test/curation_sets_integration_test.dart +++ b/test/curation_sets_integration_test.dart @@ -9,8 +9,7 @@ void main() { final host = env['TYPESENSE_HOST'] ?? '127.0.0.1'; final port = int.tryParse(env['TYPESENSE_PORT'] ?? '8108') ?? 8108; final protocolValue = env['TYPESENSE_PROTOCOL'] ?? 'http'; - final protocol = - protocolValue == 'https' ? Protocol.https : Protocol.http; + final protocol = protocolValue == 'https' ? Protocol.https : Protocol.http; late Client client; late String setName; @@ -43,27 +42,27 @@ void main() { test('upsert and retrieve curation set', () async { final upserted = await client.curationSet(setName).upsert( - CurationSetUpsertSchema( - items: [ - CurationObjectSchema( - id: itemId, - rule: CurationRuleSchema( - query: 'stark', - match: 'exact', - ), - includes: [ - CurationIncludeSchema( - id: 'doc_1', - position: 1, + CurationSetUpsertSchema( + items: [ + CurationObjectSchema( + id: itemId, + rule: CurationRuleSchema( + query: 'stark', + match: 'exact', + ), + includes: [ + CurationIncludeSchema( + id: 'doc_1', + position: 1, + ), + ], + metadata: { + 'source': 'dart-tests', + }, ), ], - metadata: { - 'source': 'dart-tests', - }, ), - ], - ), - ); + ); expect(upserted, isA()); expect(upserted.items.any((item) => item.id == itemId), isTrue); @@ -75,37 +74,37 @@ void main() { test('list, items, and item CRUD', () async { await client.curationSet(setName).upsert( - CurationSetUpsertSchema( - items: [ - CurationObjectSchema( - id: itemId, - rule: CurationRuleSchema( - query: 'lannister', - match: 'contains', - ), - excludes: [ - CurationExcludeSchema( - id: 'doc_2', + CurationSetUpsertSchema( + items: [ + CurationObjectSchema( + id: itemId, + rule: CurationRuleSchema( + query: 'lannister', + match: 'contains', + ), + excludes: [ + CurationExcludeSchema( + id: 'doc_2', + ), + ], ), ], ), - ], - ), - ); - + ); final allSets = await client.curationSets.retrieve(); expect(allSets, isA>()); expect(allSets.any((set) => set.name == setName), isTrue); final items = await client.curationSet(setName).listItems( - limit: 10, - offset: 0, - ); + limit: 10, + offset: 0, + ); expect(items, isA>()); expect(items.any((item) => item.id == itemId), isTrue); - final retrievedItem = await client.curationSet(setName).item(itemId).retrieve(); + final retrievedItem = + await client.curationSet(setName).item(itemId).retrieve(); expect(retrievedItem, isA()); expect(retrievedItem.id, equals(itemId)); diff --git a/test/nl_search_models_integration_test.dart b/test/nl_search_models_integration_test.dart index 03e201d..640b396 100644 --- a/test/nl_search_models_integration_test.dart +++ b/test/nl_search_models_integration_test.dart @@ -9,8 +9,7 @@ void main() { final host = env['TYPESENSE_HOST'] ?? '127.0.0.1'; final port = int.tryParse(env['TYPESENSE_PORT'] ?? '8108') ?? 8108; final protocolValue = env['TYPESENSE_PROTOCOL'] ?? 'http'; - final protocol = - protocolValue == 'https' ? Protocol.https : Protocol.http; + final protocol = protocolValue == 'https' ? Protocol.https : Protocol.http; final openAiApiKey = env['OPENAI_API_KEY']; final skipReason = (openAiApiKey == null || openAiApiKey.isEmpty) @@ -35,7 +34,8 @@ void main() { }, ); client = Client(config); - modelId = 'nl_search_model_dart_test_${DateTime.now().millisecondsSinceEpoch}'; + modelId = + 'nl_search_model_dart_test_${DateTime.now().millisecondsSinceEpoch}'; }); tearDownAll(() async { @@ -57,8 +57,7 @@ void main() { final created = await client.nlSearchModels.create( NLSearchModelCreateSchema( id: modelId, - modelName: - env['NL_SEARCH_MODEL_NAME'] ?? 'openai/gpt-3.5-turbo', + modelName: env['NL_SEARCH_MODEL_NAME'] ?? 'openai/gpt-3.5-turbo', apiKey: openAiApiKey, maxBytes: 16384, systemPrompt: 'This is meant for testing purposes', @@ -72,14 +71,13 @@ void main() { expect(retrieved.id, equals(modelId)); final updated = await client.nlSearchModel(modelId).update( - NLSearchModelUpdateSchema( - modelName: - env['NL_SEARCH_MODEL_NAME'] ?? 'openai/gpt-3.5-turbo', - apiKey: openAiApiKey, - maxBytes: 16384, - systemPrompt: 'This is a new system prompt for NL search', - ), - ); + NLSearchModelUpdateSchema( + modelName: env['NL_SEARCH_MODEL_NAME'] ?? 'openai/gpt-3.5-turbo', + apiKey: openAiApiKey, + maxBytes: 16384, + systemPrompt: 'This is a new system prompt for NL search', + ), + ); expect(updated, isA()); expect(updated.systemPrompt, equals('This is a new system prompt for NL search')); diff --git a/test/stemming_integration_test.dart b/test/stemming_integration_test.dart index 4422872..cb9b33d 100644 --- a/test/stemming_integration_test.dart +++ b/test/stemming_integration_test.dart @@ -9,8 +9,7 @@ void main() { final host = env['TYPESENSE_HOST'] ?? '127.0.0.1'; final port = int.tryParse(env['TYPESENSE_PORT'] ?? '8108') ?? 8108; final protocolValue = env['TYPESENSE_PROTOCOL'] ?? 'http'; - final protocol = - protocolValue == 'https' ? Protocol.https : Protocol.http; + final protocol = protocolValue == 'https' ? Protocol.https : Protocol.http; late Client client; late String dictionaryId; @@ -46,7 +45,8 @@ void main() { expect(list, isA()); expect(list.dictionaries.contains(dictionaryId), isTrue); - final dictionary = await client.stemming.dictionary(dictionaryId).retrieve(); + final dictionary = + await client.stemming.dictionary(dictionaryId).retrieve(); expect(dictionary, isA()); expect(dictionary.id, equals(dictionaryId)); }); diff --git a/test/stopwords_integration_test.dart b/test/stopwords_integration_test.dart index 94eff6a..f040d86 100644 --- a/test/stopwords_integration_test.dart +++ b/test/stopwords_integration_test.dart @@ -9,9 +9,7 @@ void main() { final host = env['TYPESENSE_HOST'] ?? '127.0.0.1'; final port = int.tryParse(env['TYPESENSE_PORT'] ?? '8108') ?? 8108; final protocolValue = env['TYPESENSE_PROTOCOL'] ?? 'http'; - final protocol = - protocolValue == 'https' ? Protocol.https : Protocol.http; - + final protocol = protocolValue == 'https' ? Protocol.https : Protocol.http; late Client client; late String stopwordId; @@ -28,8 +26,7 @@ void main() { }, ); client = Client(config); - stopwordId = - 'stopwords_dart_test_${DateTime.now().millisecondsSinceEpoch}'; + stopwordId = 'stopwords_dart_test_${DateTime.now().millisecondsSinceEpoch}'; }); tearDownAll(() async { diff --git a/test/synonym_sets_integration_test.dart b/test/synonym_sets_integration_test.dart index 697367a..d61b8e4 100644 --- a/test/synonym_sets_integration_test.dart +++ b/test/synonym_sets_integration_test.dart @@ -9,8 +9,7 @@ void main() { final host = env['TYPESENSE_HOST'] ?? '127.0.0.1'; final port = int.tryParse(env['TYPESENSE_PORT'] ?? '8108') ?? 8108; final protocolValue = env['TYPESENSE_PROTOCOL'] ?? 'http'; - final protocol = - protocolValue == 'https' ? Protocol.https : Protocol.http; + final protocol = protocolValue == 'https' ? Protocol.https : Protocol.http; late Client client; late String setName; @@ -43,15 +42,15 @@ void main() { test('upsert and retrieve synonym set', () async { final upserted = await client.synonymSet(setName).upsert( - SynonymSetCreateSchema( - items: [ - SynonymItemSchema( - id: itemId, - synonyms: ['sneakers', 'shoes'], + SynonymSetCreateSchema( + items: [ + SynonymItemSchema( + id: itemId, + synonyms: ['sneakers', 'shoes'], + ), + ], ), - ], - ), - ); + ); expect(upserted, isA()); expect(upserted.items.any((item) => item.id == itemId), isTrue); @@ -63,24 +62,24 @@ void main() { test('list, items, and item CRUD', () async { await client.synonymSet(setName).upsert( - SynonymSetCreateSchema( - items: [ - SynonymItemSchema( - id: itemId, - synonyms: ['nike', 'footwear'], + SynonymSetCreateSchema( + items: [ + SynonymItemSchema( + id: itemId, + synonyms: ['nike', 'footwear'], + ), + ], ), - ], - ), - ); + ); final allSets = await client.synonymSets.retrieve(); expect(allSets, isA>()); expect(allSets.any((set) => set.name == setName), isTrue); final items = await client.synonymSet(setName).listItems( - limit: 10, - offset: 0, - ); + limit: 10, + offset: 0, + ); expect(items, isA>()); expect(items.any((item) => item.id == itemId), isTrue); @@ -89,8 +88,7 @@ void main() { expect(retrievedItem, isA()); expect(retrievedItem.id, equals(itemId)); - final deleted = - await client.synonymSet(setName).item(itemId).delete(); + final deleted = await client.synonymSet(setName).item(itemId).delete(); expect(deleted, isA()); expect(deleted.id, equals(itemId)); }); diff --git a/test/test_utils.dart b/test/test_utils.dart index 21efe92..c6d752c 100644 --- a/test/test_utils.dart +++ b/test/test_utils.dart @@ -115,8 +115,7 @@ class ConfigurationFactory { @GenerateMocks([], customMocks: [MockSpec(as: #MockApiCall)]) class GenerateMockApiCall extends ApiCall { - GenerateMockApiCall( - super.config, super.nodePool, super.requestCache); + GenerateMockApiCall(super.config, super.nodePool, super.requestCache); } @GenerateMocks([], customMocks: [