diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml index a050edc..9cfadb7 100644 --- a/.github/workflows/dart.yml +++ b/.github/workflows/dart.yml @@ -16,6 +16,26 @@ jobs: runs-on: ubuntu-latest steps: + - name: Start Typesense + run: | + docker run -d \ + -p 8108:8108 \ + --name typesense \ + -v /tmp/typesense-data:/data \ + -v /tmp/typesense-analytics-data:/analytics-data \ + typesense/typesense:30.0.rca37 \ + --api-key=xyz \ + --data-dir=/data \ + --enable-search-analytics=true \ + --analytics-dir=/analytics-data \ + --analytics-flush-interval=60 \ + --analytics-minute-rate-limit=50 \ + --enable-cors + + - name: Wait for Typesense + run: | + timeout 20 bash -c 'while [[ "$(curl -s -o /dev/null -w ''%{http_code}'' localhost:8108/health)" != "200" ]]; do sleep 1; done' || false + - uses: actions/checkout@v3 # Note: This workflow uses the latest stable version of the Dart SDK. diff --git a/lib/src/analytics.dart b/lib/src/analytics.dart new file mode 100644 index 0000000..90812df --- /dev/null +++ b/lib/src/analytics.dart @@ -0,0 +1,27 @@ +import 'services/api_call.dart'; +import 'analytics_rules.dart'; +import 'analytics_rule.dart'; +import 'analytics_events.dart'; + +class Analytics { + final ApiCall _apiCall; + final AnalyticsRules _rules; + final AnalyticsEvents _events; + final _individualRules = {}; + + Analytics(ApiCall apiCall) + : _apiCall = apiCall, + _rules = AnalyticsRules(apiCall), + _events = AnalyticsEvents(apiCall); + + AnalyticsRules rules() => _rules; + + AnalyticsRule rule(String name) { + if (!_individualRules.containsKey(name)) { + _individualRules[name] = AnalyticsRule(name, _apiCall); + } + return _individualRules[name]!; + } + + AnalyticsEvents events() => _events; +} diff --git a/lib/src/analytics_events.dart b/lib/src/analytics_events.dart new file mode 100644 index 0000000..d00026a --- /dev/null +++ b/lib/src/analytics_events.dart @@ -0,0 +1,44 @@ +import 'models/models.dart'; +import 'services/api_call.dart'; + +class AnalyticsEvents { + final ApiCall _apiCall; + static const String eventsPath = '/analytics/events'; + static const String flushPath = '/analytics/flush'; + static const String statusPath = '/analytics/status'; + + AnalyticsEvents(ApiCall apiCall) : _apiCall = apiCall; + + Future create( + AnalyticsEventCreateSchema event) async { + final response = + await _apiCall.post(eventsPath, bodyParameters: event.toJson()); + return AnalyticsEventCreateResponse.fromJson(response); + } + + Future retrieve({ + required String userId, + required String name, + required int n, + }) async { + final response = await _apiCall.get( + eventsPath, + queryParams: { + 'user_id': userId, + 'name': name, + 'n': n.toString(), + }, + ); + return AnalyticsEventsRetrieveSchema.fromJson(response); + } + + Future flush() async { + final response = await _apiCall.post(flushPath, bodyParameters: {}); + return AnalyticsEventCreateResponse.fromJson(response); + } + + Future status() async { + final response = await _apiCall.get(statusPath); + return AnalyticsStatus.fromJson(response); + } +} diff --git a/lib/src/analytics_rule.dart b/lib/src/analytics_rule.dart new file mode 100644 index 0000000..70c4063 --- /dev/null +++ b/lib/src/analytics_rule.dart @@ -0,0 +1,25 @@ +import 'models/models.dart'; +import 'services/api_call.dart'; +import 'analytics_rules.dart'; + +class AnalyticsRule { + final String _name; + final ApiCall _apiCall; + + AnalyticsRule(String name, ApiCall apiCall) + : _name = name, + _apiCall = apiCall; + + Future retrieve() async { + final response = await _apiCall.get(_endpointPath); + return AnalyticsRuleSchema.fromJson(response); + } + + Future delete() async { + final response = await _apiCall.delete(_endpointPath); + return AnalyticsRuleDeleteSchema.fromJson(response); + } + + String get _endpointPath => + '${AnalyticsRules.resourcepath}/${Uri.encodeComponent(_name)}'; +} diff --git a/lib/src/analytics_rules.dart b/lib/src/analytics_rules.dart new file mode 100644 index 0000000..c7d8952 --- /dev/null +++ b/lib/src/analytics_rules.dart @@ -0,0 +1,72 @@ +import 'dart:convert'; + +import 'models/models.dart'; +import 'services/api_call.dart'; +import 'analytics_rule.dart'; + +class AnalyticsRules { + final ApiCall _apiCall; + static const String resourcepath = '/analytics/rules'; + final _individualRules = {}; + + AnalyticsRules(ApiCall apiCall) : _apiCall = apiCall; + + /// Creates a single analytics rule. + Future create(AnalyticsRuleCreateSchema rule) async { + final response = await _apiCall.post( + resourcepath, + bodyParameters: rule.toJson(), + ); + return AnalyticsRuleSchema.fromJson( + Map.from(response as Map), + ); + } + + /// Creates multiple analytics rules. + Future> createMany( + List rules) async { + final body = rules.map((item) => item.toJson()).toList(); + final encodedBody = json.encode(body); + final response = await _apiCall.sendList((node) => node.client!.post( + _apiCall.getRequestUri(node, resourcepath), + headers: _apiCall.defaultHeaders, + body: encodedBody, + )); + return response.map((item) { + return AnalyticsRuleSchema.fromJson( + Map.from(item as Map), + ); + }).toList(); + } + + Future> retrieve({String? ruleTag}) async { + final query = {}; + if (ruleTag != null) { + query['rule_tag'] = ruleTag; + } + final response = await _apiCall.getList( + resourcepath, + queryParams: query.isEmpty ? null : query, + ); + return response + .map((item) => + AnalyticsRuleSchema.fromJson(Map.from(item))) + .toList(); + } + + Future upsert( + String ruleName, AnalyticsRuleUpsertSchema update) async { + final response = await _apiCall.put( + '$resourcepath/${Uri.encodeComponent(ruleName)}', + bodyParameters: update.toJson(), + ); + return AnalyticsRuleSchema.fromJson(response); + } + + AnalyticsRule operator [](String ruleName) { + if (!_individualRules.containsKey(ruleName)) { + _individualRules[ruleName] = AnalyticsRule(ruleName, _apiCall); + } + return _individualRules[ruleName]!; + } +} diff --git a/lib/src/client.dart b/lib/src/client.dart index bb8928d..bffc30e 100644 --- a/lib/src/client.dart +++ b/lib/src/client.dart @@ -20,6 +20,20 @@ import 'presets.dart'; import 'metrics.dart'; import 'operations.dart'; import 'multi_search.dart'; +import 'stopword.dart'; +import 'stopwords.dart'; +import 'curation_sets.dart'; +import 'curation_set.dart'; +import 'synonym_sets.dart'; +import 'synonym_set.dart'; +import 'stemming.dart'; +import 'conversations_models.dart'; +import 'conversation_model.dart'; +import 'nl_search_models.dart'; +import 'nl_search_model.dart'; +import 'conversations.dart'; +import 'conversation.dart'; +import 'analytics.dart'; class Client { final Configuration config; @@ -35,10 +49,22 @@ class Client { final Metrics metrics; final Operations operations; final MultiSearch multiSearch; + final Stopwords stopwords; + final CurationSets curationSets; + final SynonymSets synonymSets; + final Stemming stemming; + final ConversationsModels conversationsModels; + final NLSearchModels nlSearchModels; + final Conversations conversations; + final Analytics analytics; final _individualCollections = HashMap(), _individualAliases = HashMap(), _individualKeys = HashMap(), - _individualPresets = HashMap(); + _individualPresets = HashMap(), + _individualStopwords = HashMap(), + _individualCurationSets = HashMap(), + _individualSynonymSets = HashMap(); + final _individualConversations = HashMap(); Client._( this.config, @@ -53,7 +79,15 @@ class Client { this.health, this.metrics, this.operations, - this.multiSearch); + this.multiSearch, + this.stopwords, + this.curationSets, + this.synonymSets, + this.stemming, + this.conversationsModels, + this.nlSearchModels, + this.conversations, + this.analytics); factory Client(Configuration config) { // ApiCall, DocumentsApiCall, and CollectionsApiCall share the same NodePool. @@ -79,7 +113,15 @@ class Client { Health(apiCall), Metrics(apiCall), Operations(apiCall), - MultiSearch(apiCall)); + MultiSearch(apiCall), + Stopwords(apiCall), + CurationSets(apiCall), + SynonymSets(apiCall), + Stemming(apiCall), + ConversationsModels(apiCall), + NLSearchModels(apiCall), + Conversations(apiCall), + Analytics(apiCall)); } /// Perform operation on an individual collection having [collectionName]. @@ -113,4 +155,43 @@ class Client { } return _individualPresets[presetName]!; } + + /// Perform operation on an individual stopwords set having [stopwordId]. + Stopword stopword(String stopwordId) { + if (!_individualStopwords.containsKey(stopwordId)) { + _individualStopwords[stopwordId] = Stopword(stopwordId, _apiCall); + } + return _individualStopwords[stopwordId]!; + } + + /// Perform operation on an individual curation set having [name]. + CurationSet curationSet(String name) { + if (!_individualCurationSets.containsKey(name)) { + _individualCurationSets[name] = CurationSet(name, _apiCall); + } + return _individualCurationSets[name]!; + } + + /// Perform operation on an individual synonym set having [name]. + SynonymSet synonymSet(String name) { + if (!_individualSynonymSets.containsKey(name)) { + _individualSynonymSets[name] = SynonymSet(name, _apiCall); + } + return _individualSynonymSets[name]!; + } + + ConversationModel conversationModel(String modelId) { + return conversationsModels[modelId]; + } + + NLSearchModel nlSearchModel(String modelId) { + return nlSearchModels[modelId]; + } + + Conversation conversation(String id) { + if (!_individualConversations.containsKey(id)) { + _individualConversations[id] = Conversation(id, _apiCall); + } + return _individualConversations[id]!; + } } diff --git a/lib/src/conversation.dart b/lib/src/conversation.dart new file mode 100644 index 0000000..6e351ea --- /dev/null +++ b/lib/src/conversation.dart @@ -0,0 +1,35 @@ +import 'models/models.dart'; +import 'services/api_call.dart'; +import 'conversations.dart'; + +class Conversation { + final String _id; + final ApiCall _apiCall; + + Conversation(String id, ApiCall apiCall) + : _id = id, + _apiCall = apiCall; + + Future> retrieve() async { + final response = await _apiCall.getList(_endpointPath); + return response + .map((item) => + ConversationSchema.fromJson(Map.from(item))) + .toList(); + } + + Future update( + ConversationUpdateSchema params) async { + final response = + await _apiCall.put(_endpointPath, bodyParameters: params.toJson()); + return ConversationUpdateSchema.fromJson(response); + } + + Future delete() async { + final response = await _apiCall.delete(_endpointPath); + return ConversationDeleteSchema.fromJson(response); + } + + String get _endpointPath => + '${Conversations.resourcepath}/${Uri.encodeComponent(_id)}'; +} diff --git a/lib/src/conversation_model.dart b/lib/src/conversation_model.dart new file mode 100644 index 0000000..e48a6b6 --- /dev/null +++ b/lib/src/conversation_model.dart @@ -0,0 +1,32 @@ +import 'models/models.dart'; +import 'services/api_call.dart'; +import 'conversations_models.dart'; + +class ConversationModel { + final String _id; + final ApiCall _apiCall; + + ConversationModel(String id, ApiCall apiCall) + : _id = id, + _apiCall = apiCall; + + Future update( + ConversationModelCreateSchema params) async { + final response = + await _apiCall.put(_endpointPath, bodyParameters: params.toJson()); + return ConversationModelCreateSchema.fromJson(response); + } + + Future retrieve() async { + final response = await _apiCall.get(_endpointPath); + return ConversationModelSchema.fromJson(response); + } + + Future delete() async { + final response = await _apiCall.delete(_endpointPath); + return ConversationModelDeleteSchema.fromJson(response); + } + + String get _endpointPath => + '${ConversationsModels.resourcepath}/${Uri.encodeComponent(_id)}'; +} diff --git a/lib/src/conversations.dart b/lib/src/conversations.dart new file mode 100644 index 0000000..1d859e5 --- /dev/null +++ b/lib/src/conversations.dart @@ -0,0 +1,29 @@ +import 'models/models.dart'; +import 'services/api_call.dart'; +import 'conversations_models.dart'; +import 'conversation_model.dart'; + +class Conversations { + final ApiCall _apiCall; + static const String resourcepath = '/conversations'; + final ConversationsModels _models; + final _individualModels = {}; + + Conversations(ApiCall apiCall) + : _apiCall = apiCall, + _models = ConversationsModels(apiCall); + + Future retrieve() async { + final response = await _apiCall.get(resourcepath); + return ConversationsRetrieveSchema.fromJson(response); + } + + ConversationsModels models() => _models; + + ConversationModel model(String id) { + if (!_individualModels.containsKey(id)) { + _individualModels[id] = ConversationModel(id, _apiCall); + } + return _individualModels[id]!; + } +} diff --git a/lib/src/conversations_models.dart b/lib/src/conversations_models.dart new file mode 100644 index 0000000..d40e8bf --- /dev/null +++ b/lib/src/conversations_models.dart @@ -0,0 +1,33 @@ +import 'models/models.dart'; +import 'services/api_call.dart'; +import 'conversation_model.dart'; + +class ConversationsModels { + final ApiCall _apiCall; + static const String resourcepath = '/conversations/models'; + final _individualModels = {}; + + ConversationsModels(ApiCall apiCall) : _apiCall = apiCall; + + Future create( + ConversationModelCreateSchema params) async { + final response = + await _apiCall.post(resourcepath, bodyParameters: params.toJson()); + return ConversationModelCreateSchema.fromJson(response); + } + + Future> retrieve() async { + final response = await _apiCall.getList(resourcepath); + return response + .map((item) => + ConversationModelSchema.fromJson(Map.from(item))) + .toList(); + } + + ConversationModel operator [](String modelId) { + if (!_individualModels.containsKey(modelId)) { + _individualModels[modelId] = ConversationModel(modelId, _apiCall); + } + return _individualModels[modelId]!; + } +} diff --git a/lib/src/curation_set.dart b/lib/src/curation_set.dart new file mode 100644 index 0000000..fd71e7a --- /dev/null +++ b/lib/src/curation_set.dart @@ -0,0 +1,73 @@ +import 'models/models.dart'; +import 'services/api_call.dart'; +import 'curation_sets.dart'; +import 'curation_set_items.dart'; +import 'curation_set_item.dart'; + +class CurationSet { + final String _name; + final ApiCall _apiCall; + final CurationSetItems _items; + final _individualItems = {}; + + CurationSet(String name, ApiCall apiCall) + : _name = name, + _apiCall = apiCall, + _items = CurationSetItems(name, apiCall); + + Future upsert(CurationSetUpsertSchema params) async { + final response = await _apiCall.put( + _endpointPath, + bodyParameters: params.toJson(), + ); + return CurationSetSchema.fromJson(Map.from(response)); + } + + Future retrieve() async { + final response = await _apiCall.get(_endpointPath); + return CurationSetSchema.fromJson(Map.from(response)); + } + + Future delete() async { + final response = await _apiCall.delete(_endpointPath); + return CurationSetDeleteResponseSchema.fromJson( + Map.from(response), + ); + } + + CurationSetItems items() => _items; + + CurationSetItem item(String itemId) { + if (!_individualItems.containsKey(itemId)) { + _individualItems[itemId] = CurationSetItem(_name, itemId, _apiCall); + } + return _individualItems[itemId]!; + } + + /// Retrieves items in the curation set with optional pagination. + Future> listItems({ + int? limit, + int? offset, + }) async { + return _items.retrieve(limit: limit, offset: offset); + } + + /// Retrieves a single curation set item by [itemId]. + Future getItem(String itemId) async { + return item(itemId).retrieve(); + } + + /// Creates/updates a curation set item by [itemId]. + Future upsertItem( + String itemId, CurationObjectSchema params) async { + return item(itemId).upsert(params); + } + + /// Deletes a curation set item by [itemId]. + Future deleteItem(String itemId) async { + return item(itemId).delete(); + } + + String get _endpointPath => + '${CurationSets.resourcepath}/${Uri.encodeComponent(_name)}'; +} diff --git a/lib/src/curation_set_item.dart b/lib/src/curation_set_item.dart new file mode 100644 index 0000000..3ff9104 --- /dev/null +++ b/lib/src/curation_set_item.dart @@ -0,0 +1,37 @@ +import 'models/models.dart'; +import 'services/api_call.dart'; +import 'curation_sets.dart'; + +class CurationSetItem { + final String _name; + final String _itemId; + final ApiCall _apiCall; + + CurationSetItem(String name, String itemId, ApiCall apiCall) + : _name = name, + _itemId = itemId, + _apiCall = apiCall; + + Future retrieve() async { + final response = await _apiCall.get(_endpointPath); + return CurationObjectSchema.fromJson(Map.from(response)); + } + + Future upsert(CurationObjectSchema params) async { + final response = await _apiCall.put( + _endpointPath, + bodyParameters: params.toJson(), + ); + return CurationObjectSchema.fromJson(Map.from(response)); + } + + Future delete() async { + final response = await _apiCall.delete(_endpointPath); + return CurationItemDeleteResponseSchema.fromJson( + Map.from(response), + ); + } + + String get _endpointPath => + '${CurationSets.resourcepath}/${Uri.encodeComponent(_name)}/items/${Uri.encodeComponent(_itemId)}'; +} diff --git a/lib/src/curation_set_items.dart b/lib/src/curation_set_items.dart new file mode 100644 index 0000000..3d85dd9 --- /dev/null +++ b/lib/src/curation_set_items.dart @@ -0,0 +1,36 @@ +import 'models/models.dart'; +import 'services/api_call.dart'; +import 'curation_sets.dart'; + +class CurationSetItems { + final String _name; + final ApiCall _apiCall; + + CurationSetItems(String name, ApiCall apiCall) + : _name = name, + _apiCall = apiCall; + + Future> retrieve({ + int? limit, + int? offset, + }) async { + final query = {}; + if (limit != null) { + query['limit'] = limit.toString(); + } + if (offset != null) { + query['offset'] = offset.toString(); + } + final response = await _apiCall.getList( + _endpointPath, + queryParams: query.isEmpty ? null : query, + ); + return response + .map((item) => + CurationObjectSchema.fromJson(Map.from(item))) + .toList(); + } + + String get _endpointPath => + '${CurationSets.resourcepath}/${Uri.encodeComponent(_name)}/items'; +} diff --git a/lib/src/curation_sets.dart b/lib/src/curation_sets.dart new file mode 100644 index 0000000..aa5842b --- /dev/null +++ b/lib/src/curation_sets.dart @@ -0,0 +1,21 @@ +import 'models/models.dart'; +import 'services/api_call.dart'; +import 'curation_set.dart'; + +class CurationSets { + final ApiCall _apiCall; + static const String resourcepath = '/curation_sets'; + + CurationSets(ApiCall apiCall) : _apiCall = apiCall; + + /// Retrieves all curation sets. + Future> retrieve() async { + final response = await _apiCall.getList(resourcepath); + return response + .map((item) => + CurationSetsListEntrySchema.fromJson(Map.from(item))) + .toList(); + } + + CurationSet operator [](String name) => CurationSet(name, _apiCall); +} diff --git a/lib/src/models/analytics.dart b/lib/src/models/analytics.dart new file mode 100644 index 0000000..4b8c18f --- /dev/null +++ b/lib/src/models/analytics.dart @@ -0,0 +1,267 @@ +part of 'models.dart'; + +class AnalyticsEventCreateSchema { + final String? type; + final String name; + final Map data; + + AnalyticsEventCreateSchema({ + this.type, + required this.name, + required this.data, + }); + + Map toJson() => { + if (type != null) 'type': type, + 'name': name, + 'data': data, + }; +} + +class AnalyticsEventItemSchema { + final String name; + final String? eventType; + final String? collection; + final int? timestamp; + final String? userId; + final String? docId; + final List? docIds; + final String? query; + + AnalyticsEventItemSchema({ + required this.name, + this.eventType, + this.collection, + this.timestamp, + this.userId, + this.docId, + this.docIds, + this.query, + }); + + factory AnalyticsEventItemSchema.fromJson(Map json) => + AnalyticsEventItemSchema( + name: json['name'] as String, + eventType: json['event_type'] as String?, + collection: json['collection'] as String?, + timestamp: json['timestamp'] as int?, + userId: json['user_id'] as String?, + docId: json['doc_id'] as String?, + docIds: (json['doc_ids'] as List?)?.cast(), + query: json['query'] as String?, + ); +} + +class AnalyticsEventsRetrieveSchema { + final List events; + + AnalyticsEventsRetrieveSchema({required this.events}); + + factory AnalyticsEventsRetrieveSchema.fromJson(Map json) => + AnalyticsEventsRetrieveSchema( + events: (json['events'] as List? ?? []) + .map((item) => + AnalyticsEventItemSchema.fromJson(Map.from(item as Map))) + .toList(), + ); +} + +class AnalyticsEventCreateResponse { + final bool ok; + + AnalyticsEventCreateResponse({required this.ok}); + + factory AnalyticsEventCreateResponse.fromJson(Map json) => + AnalyticsEventCreateResponse( + ok: json['ok'] as bool, + ); +} + +class AnalyticsStatus { + final int? popularPrefixQueries; + final int? nohitsPrefixQueries; + final int? logPrefixQueries; + final int? queryLogEvents; + final int? queryCounterEvents; + final int? docLogEvents; + final int? docCounterEvents; + + AnalyticsStatus({ + this.popularPrefixQueries, + this.nohitsPrefixQueries, + this.logPrefixQueries, + this.queryLogEvents, + this.queryCounterEvents, + this.docLogEvents, + this.docCounterEvents, + }); + + 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?, + queryLogEvents: json['query_log_events'] as int?, + queryCounterEvents: json['query_counter_events'] as int?, + docLogEvents: json['doc_log_events'] as int?, + docCounterEvents: json['doc_counter_events'] as int?, + ); +} + +class AnalyticsRuleParams { + final String? destinationCollection; + final int? limit; + final bool? captureSearchRequests; + final List? metaFields; + final bool? expandQuery; + final String? counterField; + final int? weight; + + AnalyticsRuleParams({ + this.destinationCollection, + this.limit, + this.captureSearchRequests, + this.metaFields, + this.expandQuery, + this.counterField, + this.weight, + }); + + Map toJson() => { + if (destinationCollection != null) + 'destination_collection': destinationCollection, + if (limit != null) 'limit': limit, + if (captureSearchRequests != null) + 'capture_search_requests': captureSearchRequests, + if (metaFields != null) 'meta_fields': metaFields, + if (expandQuery != null) 'expand_query': expandQuery, + if (counterField != null) 'counter_field': counterField, + if (weight != null) 'weight': weight, + }; + + factory AnalyticsRuleParams.fromJson(Map json) => + AnalyticsRuleParams( + destinationCollection: json['destination_collection'] as String?, + limit: json['limit'] as int?, + captureSearchRequests: json['capture_search_requests'] as bool?, + metaFields: (json['meta_fields'] as List?)?.cast(), + expandQuery: json['expand_query'] as bool?, + counterField: json['counter_field'] as String?, + weight: json['weight'] as int?, + ); +} + +class AnalyticsRuleCreateSchema { + final String name; + final String type; + final String collection; + final String eventType; + final String? ruleTag; + final AnalyticsRuleParams? params; + + AnalyticsRuleCreateSchema({ + required this.name, + required this.type, + required this.collection, + required this.eventType, + this.ruleTag, + this.params, + }); + + Map toJson() => { + 'name': name, + 'type': type, + 'collection': collection, + 'event_type': eventType, + if (ruleTag != null) 'rule_tag': ruleTag, + if (params != null) 'params': params!.toJson(), + }; + + factory AnalyticsRuleCreateSchema.fromJson(Map json) => + AnalyticsRuleCreateSchema( + name: json['name'] as String, + type: json['type'] as String, + collection: json['collection'] as String, + eventType: json['event_type'] as String, + ruleTag: json['rule_tag'] as String?, + params: json['params'] == null + ? null + : AnalyticsRuleParams.fromJson( + Map.from(json['params'] as Map)), + ); +} + +class AnalyticsRuleUpsertSchema { + final String? name; + final String? type; + final String? collection; + final String? eventType; + final String? ruleTag; + final AnalyticsRuleParams? params; + + AnalyticsRuleUpsertSchema({ + this.name, + this.type, + this.collection, + this.eventType, + this.ruleTag, + this.params, + }); + + Map toJson() => { + if (name != null) 'name': name, + if (type != null) 'type': type, + if (collection != null) 'collection': collection, + if (eventType != null) 'event_type': eventType, + if (ruleTag != null) 'rule_tag': ruleTag, + if (params != null) 'params': params!.toJson(), + }; +} + +class AnalyticsRuleSchema extends AnalyticsRuleCreateSchema { + AnalyticsRuleSchema({ + required super.name, + required super.type, + required super.collection, + required super.eventType, + super.ruleTag, + super.params, + }); + + factory AnalyticsRuleSchema.fromJson(Map json) => + AnalyticsRuleSchema( + name: json['name'] as String, + type: json['type'] as String, + collection: json['collection'] as String, + eventType: json['event_type'] as String, + ruleTag: json['rule_tag'] as String?, + params: json['params'] == null + ? null + : AnalyticsRuleParams.fromJson( + Map.from(json['params'] as Map)), + ); +} + +class AnalyticsRuleDeleteSchema { + final String name; + + AnalyticsRuleDeleteSchema({required this.name}); + + factory AnalyticsRuleDeleteSchema.fromJson(Map json) => + AnalyticsRuleDeleteSchema( + name: json['name'] as String, + ); +} + +class AnalyticsRulesRetrieveSchema { + final List rules; + + AnalyticsRulesRetrieveSchema({required this.rules}); + + factory AnalyticsRulesRetrieveSchema.fromJson(Map json) => + AnalyticsRulesRetrieveSchema( + rules: (json['rules'] as List? ?? []) + .map((item) => + AnalyticsRuleSchema.fromJson(Map.from(item as Map))) + .toList(), + ); +} diff --git a/lib/src/models/conversations.dart b/lib/src/models/conversations.dart new file mode 100644 index 0000000..8b926ad --- /dev/null +++ b/lib/src/models/conversations.dart @@ -0,0 +1,63 @@ +part of 'models.dart'; + +class ConversationSchema { + final int id; + final List conversation; + final int lastUpdated; + final int ttl; + + ConversationSchema({ + required this.id, + required this.conversation, + required this.lastUpdated, + required this.ttl, + }); + + factory ConversationSchema.fromJson(Map json) => + ConversationSchema( + id: json['id'] as int, + conversation: (json['conversation'] as List? ?? []).cast(), + lastUpdated: json['last_updated'] as int, + ttl: json['ttl'] as int, + ); +} + +class ConversationUpdateSchema { + final int ttl; + + ConversationUpdateSchema({required this.ttl}); + + Map toJson() => { + 'ttl': ttl, + }; + + factory ConversationUpdateSchema.fromJson(Map json) => + ConversationUpdateSchema( + ttl: json['ttl'] as int, + ); +} + +class ConversationDeleteSchema { + final int id; + + ConversationDeleteSchema({required this.id}); + + factory ConversationDeleteSchema.fromJson(Map json) => + ConversationDeleteSchema( + id: json['id'] as int, + ); +} + +class ConversationsRetrieveSchema { + final List conversations; + + ConversationsRetrieveSchema({required this.conversations}); + + factory ConversationsRetrieveSchema.fromJson(Map json) => + ConversationsRetrieveSchema( + conversations: (json['conversations'] as List? ?? []) + .map((item) => + ConversationSchema.fromJson(Map.from(item as Map))) + .toList(), + ); +} diff --git a/lib/src/models/conversations_model.dart b/lib/src/models/conversations_model.dart new file mode 100644 index 0000000..06ba4d3 --- /dev/null +++ b/lib/src/models/conversations_model.dart @@ -0,0 +1,109 @@ +part of 'models.dart'; + +class ConversationModelCreateSchema { + final String? id; + final String modelName; + final String? apiKey; + final String? systemPrompt; + final int maxBytes; + final String? historyCollection; + final String? accountId; + final String? url; + final int? ttl; + final String? vllmUrl; + final String? openaiUrl; + final String? openaiPath; + + ConversationModelCreateSchema({ + this.id, + required this.modelName, + this.apiKey, + this.systemPrompt, + required this.maxBytes, + this.historyCollection, + this.accountId, + this.url, + this.ttl, + this.vllmUrl, + this.openaiUrl, + this.openaiPath, + }); + + factory ConversationModelCreateSchema.fromJson(Map json) => + ConversationModelCreateSchema( + id: json['id'] as String?, + modelName: json['model_name'] as String, + apiKey: json['api_key'] as String?, + systemPrompt: json['system_prompt'] as String?, + maxBytes: json['max_bytes'] as int, + historyCollection: json['history_collection'] as String?, + accountId: json['account_id'] as String?, + url: json['url'] as String?, + ttl: json['ttl'] as int?, + vllmUrl: json['vllm_url'] as String?, + openaiUrl: json['openai_url'] as String?, + openaiPath: json['openai_path'] as String?, + ); + + Map toJson() => { + if (id != null) 'id': id, + 'model_name': modelName, + if (apiKey != null) 'api_key': apiKey, + if (systemPrompt != null) 'system_prompt': systemPrompt, + 'max_bytes': maxBytes, + if (historyCollection != null) 'history_collection': historyCollection, + if (accountId != null) 'account_id': accountId, + if (url != null) 'url': url, + if (ttl != null) 'ttl': ttl, + if (vllmUrl != null) 'vllm_url': vllmUrl, + if (openaiUrl != null) 'openai_url': openaiUrl, + if (openaiPath != null) 'openai_path': openaiPath, + }; +} + +class ConversationModelSchema extends ConversationModelCreateSchema { + ConversationModelSchema({ + required String id, + required super.modelName, + super.apiKey, + super.systemPrompt, + required super.maxBytes, + super.historyCollection, + super.accountId, + super.url, + super.ttl, + super.vllmUrl, + super.openaiUrl, + super.openaiPath, + }) : super(id: id); + + @override + String get id => super.id!; + + factory ConversationModelSchema.fromJson(Map json) => + ConversationModelSchema( + id: json['id'] as String, + modelName: json['model_name'] as String, + apiKey: json['api_key'] as String?, + systemPrompt: json['system_prompt'] as String?, + maxBytes: json['max_bytes'] as int, + historyCollection: json['history_collection'] as String?, + accountId: json['account_id'] as String?, + url: json['url'] as String?, + ttl: json['ttl'] as int?, + vllmUrl: json['vllm_url'] as String?, + openaiUrl: json['openai_url'] as String?, + openaiPath: json['openai_path'] as String?, + ); +} + +class ConversationModelDeleteSchema { + final String id; + + ConversationModelDeleteSchema({required this.id}); + + factory ConversationModelDeleteSchema.fromJson(Map json) => + ConversationModelDeleteSchema( + id: json['id'] as String, + ); +} diff --git a/lib/src/models/curation_sets.dart b/lib/src/models/curation_sets.dart new file mode 100644 index 0000000..8a8f663 --- /dev/null +++ b/lib/src/models/curation_sets.dart @@ -0,0 +1,278 @@ +part of 'models.dart'; + +class CurationIncludeSchema { + final String id; + final int position; + + CurationIncludeSchema({ + required this.id, + required this.position, + }); + + factory CurationIncludeSchema.fromJson(Map json) => + CurationIncludeSchema( + id: json['id'] as String, + position: json['position'] as int, + ); + + Map toJson() => { + 'id': id, + 'position': position, + }; +} + +class CurationExcludeSchema { + final String id; + + CurationExcludeSchema({required this.id}); + + factory CurationExcludeSchema.fromJson(Map json) => + CurationExcludeSchema( + id: json['id'] as String, + ); + + Map toJson() => { + 'id': id, + }; +} + +class CurationRuleSchema { + final String? query; + final String? match; + final String? filterBy; + final List? tags; + final bool? synonyms; + final bool? stem; + final String? stemmingDictionary; + + CurationRuleSchema({ + this.query, + this.match, + this.filterBy, + this.tags, + this.synonyms, + this.stem, + this.stemmingDictionary, + }); + + factory CurationRuleSchema.fromJson(Map json) => + CurationRuleSchema( + query: json['query'] as String?, + match: json['match'] as String?, + filterBy: json['filter_by'] as String?, + tags: (json['tags'] as List?)?.cast(), + synonyms: json['synonyms'] as bool?, + stem: json['stem'] as bool?, + stemmingDictionary: json['stemming_dictionary'] as String?, + ); + + Map toJson() => { + if (query != null) 'query': query, + if (match != null) 'match': match, + if (filterBy != null) 'filter_by': filterBy, + if (tags != null) 'tags': tags, + if (synonyms != null) 'synonyms': synonyms, + if (stem != null) 'stem': stem, + if (stemmingDictionary != null) + 'stemming_dictionary': stemmingDictionary, + }; +} + +class CurationDiversitySimilarityMetricSchema { + final String field; + final String method; + final double weight; + + CurationDiversitySimilarityMetricSchema({ + required this.field, + required this.method, + required this.weight, + }); + + factory CurationDiversitySimilarityMetricSchema.fromJson( + Map json) => + CurationDiversitySimilarityMetricSchema( + field: json['field'] as String, + method: json['method'] as String, + weight: (json['weight'] as num).toDouble(), + ); + + Map toJson() => { + 'field': field, + 'method': method, + 'weight': weight, + }; +} + +class CurationDiversitySchema { + final List similarityMetric; + + CurationDiversitySchema({required this.similarityMetric}); + + factory CurationDiversitySchema.fromJson(Map json) => + CurationDiversitySchema( + similarityMetric: (json['similarity_metric'] as List? ?? []) + .map((item) => CurationDiversitySimilarityMetricSchema.fromJson( + Map.from(item as Map))) + .toList(), + ); + + Map toJson() => { + 'similarity_metric': + similarityMetric.map((metric) => metric.toJson()).toList(), + }; +} + +class CurationObjectSchema { + final String id; + final CurationRuleSchema? rule; + final List? includes; + final List? excludes; + final String? filterBy; + final String? sortBy; + final String? replaceQuery; + final bool? removeMatchedTokens; + final bool? filterCuratedHits; + final int? effectiveFromTs; + final int? effectiveToTs; + final bool? stopProcessing; + final CurationDiversitySchema? diversity; + final Map? metadata; + + CurationObjectSchema({ + required this.id, + this.rule, + this.includes, + this.excludes, + this.filterBy, + this.sortBy, + this.replaceQuery, + this.removeMatchedTokens, + this.filterCuratedHits, + this.effectiveFromTs, + this.effectiveToTs, + this.stopProcessing, + this.diversity, + this.metadata, + }); + + factory CurationObjectSchema.fromJson(Map json) => + CurationObjectSchema( + id: json['id'] as String, + rule: json['rule'] == null + ? null + : CurationRuleSchema.fromJson( + Map.from(json['rule'] as Map), + ), + includes: (json['includes'] as List?) + ?.map((item) => CurationIncludeSchema.fromJson( + Map.from(item as Map))) + .toList(), + excludes: (json['excludes'] as List?) + ?.map((item) => CurationExcludeSchema.fromJson( + Map.from(item as Map))) + .toList(), + filterBy: json['filter_by'] as String?, + sortBy: json['sort_by'] as String?, + replaceQuery: json['replace_query'] as String?, + removeMatchedTokens: json['remove_matched_tokens'] as bool?, + filterCuratedHits: json['filter_curated_hits'] as bool?, + effectiveFromTs: json['effective_from_ts'] as int?, + effectiveToTs: json['effective_to_ts'] as int?, + stopProcessing: json['stop_processing'] as bool?, + diversity: json['diversity'] == null + ? null + : CurationDiversitySchema.fromJson( + Map.from(json['diversity'] as Map), + ), + metadata: (json['metadata'] as Map?)?.cast(), + ); + + 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 (filterBy != null) 'filter_by': filterBy, + if (sortBy != null) 'sort_by': sortBy, + if (replaceQuery != null) 'replace_query': replaceQuery, + if (removeMatchedTokens != null) + 'remove_matched_tokens': removeMatchedTokens, + if (filterCuratedHits != null) 'filter_curated_hits': filterCuratedHits, + if (effectiveFromTs != null) 'effective_from_ts': effectiveFromTs, + if (effectiveToTs != null) 'effective_to_ts': effectiveToTs, + if (stopProcessing != null) 'stop_processing': stopProcessing, + if (diversity != null) 'diversity': diversity!.toJson(), + if (metadata != null) 'metadata': metadata, + }; +} + +class CurationSetUpsertSchema { + final List items; + + CurationSetUpsertSchema({required this.items}); + + Map toJson() => { + 'items': items.map((e) => e.toJson()).toList(), + }; +} + +class CurationSetSchema { + final String? name; + final List items; + + CurationSetSchema({ + this.name, + required this.items, + }); + + factory CurationSetSchema.fromJson(Map json) => + CurationSetSchema( + name: json['name'] as String?, + items: (json['items'] as List? ?? []) + .map((item) => + CurationObjectSchema.fromJson(Map.from(item as Map))) + .toList(), + ); +} + +class CurationSetsListEntrySchema { + final String name; + final List items; + + CurationSetsListEntrySchema({ + required this.name, + required this.items, + }); + + factory CurationSetsListEntrySchema.fromJson(Map json) => + CurationSetsListEntrySchema( + name: json['name'] as String, + items: (json['items'] as List? ?? []) + .map((item) => + CurationObjectSchema.fromJson(Map.from(item as Map))) + .toList(), + ); +} + +class CurationSetDeleteResponseSchema { + final String name; + + CurationSetDeleteResponseSchema({required this.name}); + + factory CurationSetDeleteResponseSchema.fromJson(Map json) => + CurationSetDeleteResponseSchema( + name: json['name'] as String, + ); +} + +class CurationItemDeleteResponseSchema { + final String id; + + CurationItemDeleteResponseSchema({required this.id}); + + factory CurationItemDeleteResponseSchema.fromJson(Map json) => + CurationItemDeleteResponseSchema( + id: json['id'] as String, + ); +} diff --git a/lib/src/models/models.dart b/lib/src/models/models.dart index e747c76..e8a5a12 100644 --- a/lib/src/models/models.dart +++ b/lib/src/models/models.dart @@ -4,3 +4,11 @@ import '../exceptions/exceptions.dart'; part 'field.dart'; part 'schema.dart'; part 'node.dart'; +part 'stopwords.dart'; +part 'curation_sets.dart'; +part 'synonym_sets.dart'; +part 'stemming.dart'; +part 'conversations_model.dart'; +part 'nl_search_model.dart'; +part 'conversations.dart'; +part 'analytics.dart'; diff --git a/lib/src/models/nl_search_model.dart b/lib/src/models/nl_search_model.dart new file mode 100644 index 0000000..45e54a4 --- /dev/null +++ b/lib/src/models/nl_search_model.dart @@ -0,0 +1,246 @@ +part of 'models.dart'; + +class NLSearchModelBase { + final String modelName; + final String? apiKey; + final String? apiUrl; + final int? maxBytes; + final double? temperature; + final String? systemPrompt; + final double? topP; + final int? topK; + final List? stopSequences; + final String? apiVersion; + final String? projectId; + final String? accessToken; + final String? refreshToken; + final String? clientId; + final String? clientSecret; + final String? region; + final int? maxOutputTokens; + final String? accountId; + + NLSearchModelBase({ + required this.modelName, + this.apiKey, + this.apiUrl, + this.maxBytes, + this.temperature, + this.systemPrompt, + this.topP, + this.topK, + this.stopSequences, + this.apiVersion, + this.projectId, + this.accessToken, + this.refreshToken, + this.clientId, + this.clientSecret, + this.region, + this.maxOutputTokens, + this.accountId, + }); + + Map toJson() => { + 'model_name': modelName, + if (apiKey != null) 'api_key': apiKey, + if (apiUrl != null) 'api_url': apiUrl, + if (maxBytes != null) 'max_bytes': maxBytes, + if (temperature != null) 'temperature': temperature, + if (systemPrompt != null) 'system_prompt': systemPrompt, + if (topP != null) 'top_p': topP, + if (topK != null) 'top_k': topK, + if (stopSequences != null) 'stop_sequences': stopSequences, + if (apiVersion != null) 'api_version': apiVersion, + if (projectId != null) 'project_id': projectId, + if (accessToken != null) 'access_token': accessToken, + if (refreshToken != null) 'refresh_token': refreshToken, + if (clientId != null) 'client_id': clientId, + if (clientSecret != null) 'client_secret': clientSecret, + if (region != null) 'region': region, + if (maxOutputTokens != null) 'max_output_tokens': maxOutputTokens, + if (accountId != null) 'account_id': accountId, + }; + + static NLSearchModelBase fromJson(Map json) => + NLSearchModelBase( + modelName: json['model_name'] as String, + apiKey: json['api_key'] as String?, + apiUrl: json['api_url'] as String?, + maxBytes: json['max_bytes'] as int?, + temperature: (json['temperature'] as num?)?.toDouble(), + systemPrompt: json['system_prompt'] as String?, + topP: (json['top_p'] as num?)?.toDouble(), + topK: json['top_k'] as int?, + stopSequences: (json['stop_sequences'] as List?)?.cast(), + apiVersion: json['api_version'] as String?, + projectId: json['project_id'] as String?, + accessToken: json['access_token'] as String?, + refreshToken: json['refresh_token'] as String?, + clientId: json['client_id'] as String?, + clientSecret: json['client_secret'] as String?, + region: json['region'] as String?, + maxOutputTokens: json['max_output_tokens'] as int?, + accountId: json['account_id'] as String?, + ); +} + +class NLSearchModelCreateSchema extends NLSearchModelBase { + final String? id; + + NLSearchModelCreateSchema({ + this.id, + required super.modelName, + super.apiKey, + super.apiUrl, + super.maxBytes, + super.temperature, + super.systemPrompt, + super.topP, + super.topK, + super.stopSequences, + super.apiVersion, + super.projectId, + super.accessToken, + super.refreshToken, + super.clientId, + super.clientSecret, + super.region, + super.maxOutputTokens, + super.accountId, + }); + + factory NLSearchModelCreateSchema.fromJson(Map json) => + NLSearchModelCreateSchema( + id: json['id'] as String?, + modelName: json['model_name'] as String, + apiKey: json['api_key'] as String?, + apiUrl: json['api_url'] as String?, + maxBytes: json['max_bytes'] as int?, + temperature: (json['temperature'] as num?)?.toDouble(), + systemPrompt: json['system_prompt'] as String?, + topP: (json['top_p'] as num?)?.toDouble(), + topK: json['top_k'] as int?, + stopSequences: (json['stop_sequences'] as List?)?.cast(), + apiVersion: json['api_version'] as String?, + projectId: json['project_id'] as String?, + accessToken: json['access_token'] as String?, + refreshToken: json['refresh_token'] as String?, + clientId: json['client_id'] as String?, + clientSecret: json['client_secret'] as String?, + region: json['region'] as String?, + maxOutputTokens: json['max_output_tokens'] as int?, + accountId: json['account_id'] as String?, + ); + + @override + Map toJson() => { + if (id != null) 'id': id, + ...super.toJson(), + }; +} + +class NLSearchModelSchema extends NLSearchModelBase { + final String id; + + NLSearchModelSchema({ + required this.id, + required super.modelName, + super.apiKey, + super.apiUrl, + super.maxBytes, + super.temperature, + super.systemPrompt, + super.topP, + super.topK, + super.stopSequences, + super.apiVersion, + super.projectId, + super.accessToken, + super.refreshToken, + super.clientId, + super.clientSecret, + super.region, + super.maxOutputTokens, + super.accountId, + }); + + factory NLSearchModelSchema.fromJson(Map json) => + NLSearchModelSchema( + id: json['id'] as String, + modelName: json['model_name'] as String, + apiKey: json['api_key'] as String?, + apiUrl: json['api_url'] as String?, + maxBytes: json['max_bytes'] as int?, + temperature: (json['temperature'] as num?)?.toDouble(), + systemPrompt: json['system_prompt'] as String?, + topP: (json['top_p'] as num?)?.toDouble(), + topK: json['top_k'] as int?, + stopSequences: (json['stop_sequences'] as List?)?.cast(), + apiVersion: json['api_version'] as String?, + projectId: json['project_id'] as String?, + accessToken: json['access_token'] as String?, + refreshToken: json['refresh_token'] as String?, + clientId: json['client_id'] as String?, + clientSecret: json['client_secret'] as String?, + region: json['region'] as String?, + maxOutputTokens: json['max_output_tokens'] as int?, + accountId: json['account_id'] as String?, + ); +} + +class NLSearchModelUpdateSchema extends NLSearchModelBase { + NLSearchModelUpdateSchema({ + required super.modelName, + super.apiKey, + super.apiUrl, + super.maxBytes, + super.temperature, + super.systemPrompt, + super.topP, + super.topK, + super.stopSequences, + super.apiVersion, + super.projectId, + super.accessToken, + super.refreshToken, + super.clientId, + super.clientSecret, + super.region, + super.maxOutputTokens, + super.accountId, + }); + + factory NLSearchModelUpdateSchema.fromJson(Map json) => + NLSearchModelUpdateSchema( + modelName: json['model_name'] as String? ?? '', + apiKey: json['api_key'] as String?, + apiUrl: json['api_url'] as String?, + maxBytes: json['max_bytes'] as int?, + temperature: (json['temperature'] as num?)?.toDouble(), + systemPrompt: json['system_prompt'] as String?, + topP: (json['top_p'] as num?)?.toDouble(), + topK: json['top_k'] as int?, + stopSequences: (json['stop_sequences'] as List?)?.cast(), + apiVersion: json['api_version'] as String?, + projectId: json['project_id'] as String?, + accessToken: json['access_token'] as String?, + refreshToken: json['refresh_token'] as String?, + clientId: json['client_id'] as String?, + clientSecret: json['client_secret'] as String?, + region: json['region'] as String?, + maxOutputTokens: json['max_output_tokens'] as int?, + accountId: json['account_id'] as String?, + ); +} + +class NLSearchModelDeleteSchema { + final String id; + + NLSearchModelDeleteSchema({required this.id}); + + factory NLSearchModelDeleteSchema.fromJson(Map json) => + NLSearchModelDeleteSchema( + id: json['id'] as String, + ); +} diff --git a/lib/src/models/stemming.dart b/lib/src/models/stemming.dart new file mode 100644 index 0000000..796d322 --- /dev/null +++ b/lib/src/models/stemming.dart @@ -0,0 +1,53 @@ +part of 'models.dart'; + +class StemmingDictionaryCreateSchema { + final String word; + final String root; + + StemmingDictionaryCreateSchema({ + required this.word, + required this.root, + }); + + factory StemmingDictionaryCreateSchema.fromJson(Map json) => + StemmingDictionaryCreateSchema( + word: json['word'] as String, + root: json['root'] as String, + ); + + Map toJson() => { + 'word': word, + 'root': root, + }; +} + +class StemmingDictionarySchema { + final String id; + final List words; + + StemmingDictionarySchema({ + required this.id, + required this.words, + }); + + factory StemmingDictionarySchema.fromJson(Map json) => + StemmingDictionarySchema( + id: json['id'] as String, + words: (json['words'] as List? ?? []) + .map((item) => StemmingDictionaryCreateSchema.fromJson( + Map.from(item as Map))) + .toList(), + ); +} + +class StemmingDictionariesRetrieveSchema { + final List dictionaries; + + StemmingDictionariesRetrieveSchema({required this.dictionaries}); + + factory StemmingDictionariesRetrieveSchema.fromJson( + Map json) => + StemmingDictionariesRetrieveSchema( + dictionaries: (json['dictionaries'] as List? ?? []).cast(), + ); +} diff --git a/lib/src/models/stopwords.dart b/lib/src/models/stopwords.dart new file mode 100644 index 0000000..b80536b --- /dev/null +++ b/lib/src/models/stopwords.dart @@ -0,0 +1,67 @@ +part of 'models.dart'; + +class StopwordCreateSchema { + final List stopwords; + final String? locale; + + StopwordCreateSchema({ + required this.stopwords, + this.locale, + }); + + Map toJson() => { + 'stopwords': stopwords, + if (locale != null) 'locale': locale, + }; +} + +class StopwordSchema { + final String id; + final List stopwords; + final String? locale; + + StopwordSchema({ + required this.id, + required this.stopwords, + this.locale, + }); + + 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( + id: json['id'] as String, + stopwords: List.from(json['stopwords'] as List), + locale: json['locale'] as String?, + ); + } +} + +class StopwordsRetrieveSchema { + final List stopwords; + + StopwordsRetrieveSchema({ + required this.stopwords, + }); + + factory StopwordsRetrieveSchema.fromJson(Map json) { + final items = (json['stopwords'] as List? ?? []) + .map((item) => + StopwordSchema.fromJson(Map.from(item as Map))) + .toList(); + return StopwordsRetrieveSchema(stopwords: items); + } +} + +class StopwordDeleteSchema { + final String id; + + StopwordDeleteSchema({required this.id}); + + factory StopwordDeleteSchema.fromJson(Map json) => + StopwordDeleteSchema(id: json['id'] as String); +} diff --git a/lib/src/models/synonym_sets.dart b/lib/src/models/synonym_sets.dart new file mode 100644 index 0000000..f2f3371 --- /dev/null +++ b/lib/src/models/synonym_sets.dart @@ -0,0 +1,104 @@ +part of 'models.dart'; + +class SynonymItemSchema { + final String id; + final List synonyms; + final String? root; + final String? locale; + final List? symbolsToIndex; + + SynonymItemSchema({ + required this.id, + required this.synonyms, + this.root, + this.locale, + this.symbolsToIndex, + }); + + factory SynonymItemSchema.fromJson(Map json) => + SynonymItemSchema( + id: json['id'] as String, + synonyms: (json['synonyms'] as List).cast(), + root: json['root'] as String?, + locale: json['locale'] as String?, + symbolsToIndex: (json['symbols_to_index'] as List?)?.cast(), + ); + + Map toJson() => { + 'id': id, + 'synonyms': synonyms, + if (root != null) 'root': root, + if (locale != null) 'locale': locale, + if (symbolsToIndex != null) 'symbols_to_index': symbolsToIndex, + }; +} + +class SynonymItemDeleteSchema { + final String id; + + SynonymItemDeleteSchema({required this.id}); + + factory SynonymItemDeleteSchema.fromJson(Map json) => + SynonymItemDeleteSchema( + id: json['id'] as String, + ); +} + +class SynonymSetCreateSchema { + final List items; + + SynonymSetCreateSchema({required this.items}); + + Map toJson() => { + 'items': items.map((item) => item.toJson()).toList(), + }; + + factory SynonymSetCreateSchema.fromJson(Map json) => + SynonymSetCreateSchema( + items: (json['items'] as List? ?? []) + .map((item) => + SynonymItemSchema.fromJson(Map.from(item as Map))) + .toList(), + ); +} + +class SynonymSetSchema extends SynonymSetCreateSchema { + final String name; + + SynonymSetSchema({ + required this.name, + required super.items, + }); + + factory SynonymSetSchema.fromJson(Map json) => + SynonymSetSchema( + name: json['name'] as String, + items: (json['items'] as List? ?? []) + .map((item) => + SynonymItemSchema.fromJson(Map.from(item as Map))) + .toList(), + ); +} + +class SynonymSetRetrieveSchema extends SynonymSetCreateSchema { + SynonymSetRetrieveSchema({required super.items}); + + factory SynonymSetRetrieveSchema.fromJson(Map json) => + SynonymSetRetrieveSchema( + items: (json['items'] as List? ?? []) + .map((item) => + SynonymItemSchema.fromJson(Map.from(item as Map))) + .toList(), + ); +} + +class SynonymSetDeleteSchema { + final String name; + + SynonymSetDeleteSchema({required this.name}); + + factory SynonymSetDeleteSchema.fromJson(Map json) => + SynonymSetDeleteSchema( + name: json['name'] as String, + ); +} diff --git a/lib/src/nl_search_model.dart b/lib/src/nl_search_model.dart new file mode 100644 index 0000000..6ab0b0b --- /dev/null +++ b/lib/src/nl_search_model.dart @@ -0,0 +1,31 @@ +import 'models/models.dart'; +import 'services/api_call.dart'; +import 'nl_search_models.dart'; + +class NLSearchModel { + final String _id; + final ApiCall _apiCall; + + NLSearchModel(String id, ApiCall apiCall) + : _id = id, + _apiCall = apiCall; + + Future retrieve() async { + final response = await _apiCall.get(_endpointPath); + return NLSearchModelSchema.fromJson(response); + } + + Future update(NLSearchModelUpdateSchema schema) async { + final response = + await _apiCall.put(_endpointPath, bodyParameters: schema.toJson()); + return NLSearchModelSchema.fromJson(response); + } + + Future delete() async { + final response = await _apiCall.delete(_endpointPath); + return NLSearchModelDeleteSchema.fromJson(response); + } + + String get _endpointPath => + '${NLSearchModels.resourcepath}/${Uri.encodeComponent(_id)}'; +} diff --git a/lib/src/nl_search_models.dart b/lib/src/nl_search_models.dart new file mode 100644 index 0000000..4e549e0 --- /dev/null +++ b/lib/src/nl_search_models.dart @@ -0,0 +1,32 @@ +import 'models/models.dart'; +import 'services/api_call.dart'; +import 'nl_search_model.dart'; + +class NLSearchModels { + final ApiCall _apiCall; + static const String resourcepath = '/nl_search_models'; + final _individualModels = {}; + + NLSearchModels(ApiCall apiCall) : _apiCall = apiCall; + + Future create(NLSearchModelCreateSchema schema) async { + final response = + await _apiCall.post(resourcepath, bodyParameters: schema.toJson()); + return NLSearchModelSchema.fromJson(response); + } + + Future> retrieve() async { + final response = await _apiCall.getList(resourcepath); + return response + .map((item) => + NLSearchModelSchema.fromJson(Map.from(item))) + .toList(); + } + + NLSearchModel operator [](String modelId) { + if (!_individualModels.containsKey(modelId)) { + _individualModels[modelId] = NLSearchModel(modelId, _apiCall); + } + return _individualModels[modelId]!; + } +} diff --git a/lib/src/services/api_call.dart b/lib/src/services/api_call.dart index 91590dc..3acaa2f 100644 --- a/lib/src/services/api_call.dart +++ b/lib/src/services/api_call.dart @@ -53,6 +53,33 @@ class ApiCall extends BaseApiCall> { } } + /// Sends an HTTP GET request that expects a JSON array response. + /// Logic mirrors [get], only the response decoding differs. + Future> getList( + String endpoint, { + Map? queryParams, + bool shouldCacheResult = false, + }) { + if (shouldCacheResult && config.cachedSearchResultsTTL != Duration.zero) { + final queryParamsSplay = + queryParams == null ? null : SplayTreeMap.from(queryParams); + + return _requestCache.cacheList( + '$endpoint${queryParamsSplay ?? ''}', + sendList, + (node) => node.client!.get( + getRequestUri(node, endpoint, queryParams: queryParams), + headers: defaultHeaders, + ), + ); + } else { + return sendList((node) => node.client!.get( + getRequestUri(node, endpoint, queryParams: queryParams), + headers: defaultHeaders, + )); + } + } + /// Sends an HTTP DELETE request to the URL constructed using the [Node.uri], /// [endpoint] and [queryParams]. Future> delete( @@ -113,6 +140,22 @@ class ApiCall extends BaseApiCall> { } } + /// Sends an HTTP POST request with a raw string body and returns raw text. + Future postRaw( + String endpoint, { + Map? queryParams, + Map? additionalHeaders, + String? bodyParameters, + }) async { + final headers = {...defaultHeaders, ...?additionalHeaders}; + final response = await sendRaw((node) => node.client!.post( + getRequestUri(node, endpoint, queryParams: queryParams), + headers: headers, + body: bodyParameters, + )); + return response.body; + } + /// Sends an HTTP PUT request with the given [bodyParameters] to the URL /// constructed using the [Node.uri], [endpoint] and [queryParams]. /// @@ -156,6 +199,18 @@ class ApiCall extends BaseApiCall> { /// The [responseBody] is parsed as JSON and returned if no exceptions are /// raised. @override + @override Map decode(String responseBody) => - responseBody.isEmpty ? {} : json.decode(responseBody); + responseBody.isEmpty + ? {} + : json.decode(responseBody) as Map; + + List decodeList(String responseBody) => + responseBody.isEmpty ? [] : json.decode(responseBody) as List; + + Future> sendList( + Future Function(Node) request) async { + final response = await sendRaw(request); + return decodeList(response.body); + } } diff --git a/lib/src/services/base_api_call.dart b/lib/src/services/base_api_call.dart index 63389d4..25b788b 100644 --- a/lib/src/services/base_api_call.dart +++ b/lib/src/services/base_api_call.dart @@ -52,6 +52,12 @@ abstract class BaseApiCall { /// Also sets the health status of nodes after each request so it can be put /// in/out of [NodePool]'s circulation. Future send(Future Function(Node) request) async { + final response = await sendRaw(request); + return decode(response.body); + } + + Future sendRaw( + Future Function(Node) request) async { http.Response response; Node node; for (var triesLeft = config.numRetries;;) { @@ -71,7 +77,7 @@ abstract class BaseApiCall { if (response.statusCode >= 200 && response.statusCode < 300) { // If response is 2xx return a resolved promise. - return decode(response.body); + return response; } else if (response.statusCode < 500) { // Next, if response is anything but 5xx, don't retry, return a custom // error. diff --git a/lib/src/services/node_pool.dart b/lib/src/services/node_pool.dart index b5e1b09..d11aca2 100644 --- a/lib/src/services/node_pool.dart +++ b/lib/src/services/node_pool.dart @@ -52,7 +52,8 @@ class NodePool { } /// Sets [node]'s health as [isHealthy] along with it's last [accessTime]. - static setNodeHealthStatus(Node node, bool isHealthy, DateTime accessTime) { + static void setNodeHealthStatus( + Node node, bool isHealthy, DateTime accessTime) { node.isHealthy = isHealthy; node.lastAccessTimestamp = accessTime; } diff --git a/lib/src/services/request_cache.dart b/lib/src/services/request_cache.dart index 72ba472..75fef58 100644 --- a/lib/src/services/request_cache.dart +++ b/lib/src/services/request_cache.dart @@ -8,6 +8,7 @@ import '../models/models.dart'; class RequestCache { final Duration cacheTTL; final _cachedResponses = HashMap(); + final _cachedListResponses = HashMap(); RequestCache(this.cacheTTL); @@ -37,6 +38,29 @@ class RequestCache { bool _isCacheValid(_CachedResult cache) => cache.validTill.difference(DateTime.now()) > Duration.zero; + + Future> cacheList( + String key, + Future> Function(Future Function(Node)) send, + Future Function(Node) request, + ) async { + if (_cachedListResponses.containsKey(key)) { + if (_isCacheValidList(_cachedListResponses[key]!)) { + return Future.value(_cachedListResponses[key]!.data); + } else { + _cachedListResponses.remove(key); + } + } + + final response = await send(request); + _cachedListResponses[key] = + _CachedListResult(response, DateTime.now().add(cacheTTL)); + return response; + } + + bool _isCacheValidList(_CachedListResult cache) => + cache.validTill.difference(DateTime.now()) > Duration.zero; + } class _CachedResult { @@ -45,3 +69,10 @@ class _CachedResult { const _CachedResult(this.data, this.validTill); } + +class _CachedListResult { + final List data; + final DateTime validTill; + + const _CachedListResult(this.data, this.validTill); +} diff --git a/lib/src/stemming.dart b/lib/src/stemming.dart new file mode 100644 index 0000000..152e5e8 --- /dev/null +++ b/lib/src/stemming.dart @@ -0,0 +1,22 @@ +import 'services/api_call.dart'; +import 'stemming_dictionaries.dart'; +import 'stemming_dictionary.dart'; + +class Stemming { + final ApiCall _apiCall; + final StemmingDictionaries _dictionaries; + final _individualDictionaries = {}; + + Stemming(ApiCall apiCall) + : _apiCall = apiCall, + _dictionaries = StemmingDictionaries(apiCall); + + StemmingDictionaries get dictionaries => _dictionaries; + + StemmingDictionary dictionary(String id) { + if (!_individualDictionaries.containsKey(id)) { + _individualDictionaries[id] = StemmingDictionary(id, _apiCall); + } + return _individualDictionaries[id]!; + } +} diff --git a/lib/src/stemming_dictionaries.dart b/lib/src/stemming_dictionaries.dart new file mode 100644 index 0000000..c5273b0 --- /dev/null +++ b/lib/src/stemming_dictionaries.dart @@ -0,0 +1,55 @@ +import 'dart:convert'; + +import 'models/models.dart'; +import 'services/api_call.dart'; +import 'stemming_dictionary.dart'; + +class StemmingDictionaries { + final ApiCall _apiCall; + static const String resourcepath = '/stemming/dictionaries'; + final _individualDictionaries = {}; + + StemmingDictionaries(ApiCall apiCall) : _apiCall = apiCall; + + /// Retrieves the list of stemming dictionaries. + Future retrieve() async { + final response = await _apiCall.get(resourcepath); + return StemmingDictionariesRetrieveSchema.fromJson(response); + } + + /// Creates or updates a stemming dictionary from JSONL payload. + Future upsertRaw(String dictionaryId, String jsonl) async { + final response = await _apiCall.postRaw( + '$resourcepath/import', + queryParams: {'id': dictionaryId}, + bodyParameters: jsonl, + additionalHeaders: {contentType: 'text/plain'}, + ); + return response; + } + + /// Creates or updates a stemming dictionary from structured inputs. + Future> upsert( + String dictionaryId, + List wordRootCombinations, + ) async { + final jsonl = wordRootCombinations + .map((combo) => json.encode(combo.toJson())) + .join('\n'); + final response = await upsertRaw(dictionaryId, jsonl); + return response + .split('\n') + .where((line) => line.isNotEmpty) + .map((line) => StemmingDictionaryCreateSchema.fromJson( + Map.from(json.decode(line)))) + .toList(); + } + + StemmingDictionary operator [](String dictionaryId) { + if (!_individualDictionaries.containsKey(dictionaryId)) { + _individualDictionaries[dictionaryId] = + StemmingDictionary(dictionaryId, _apiCall); + } + return _individualDictionaries[dictionaryId]!; + } +} diff --git a/lib/src/stemming_dictionary.dart b/lib/src/stemming_dictionary.dart new file mode 100644 index 0000000..5cbeb25 --- /dev/null +++ b/lib/src/stemming_dictionary.dart @@ -0,0 +1,20 @@ +import 'models/models.dart'; +import 'services/api_call.dart'; +import 'stemming_dictionaries.dart'; + +class StemmingDictionary { + final String _id; + final ApiCall _apiCall; + + StemmingDictionary(String id, ApiCall apiCall) + : _id = id, + _apiCall = apiCall; + + Future retrieve() async { + final response = await _apiCall.get(_endpointPath); + return StemmingDictionarySchema.fromJson(response); + } + + String get _endpointPath => + '${StemmingDictionaries.resourcepath}/${Uri.encodeComponent(_id)}'; +} diff --git a/lib/src/stopword.dart b/lib/src/stopword.dart new file mode 100644 index 0000000..5cc58b8 --- /dev/null +++ b/lib/src/stopword.dart @@ -0,0 +1,26 @@ +import 'models/models.dart'; +import 'services/api_call.dart'; +import 'stopwords.dart'; + +class Stopword { + final String id; + final ApiCall _apiCall; + + const Stopword(this.id, ApiCall apiCall) : _apiCall = apiCall; + + /// Retrieves a stopwords set. + Future retrieve() async { + final response = await _apiCall.get( + '${Stopwords.resourcepath}/$id', + ); + return StopwordSchema.fromJson(response); + } + + /// Deletes a stopwords set. + Future delete() async { + final response = await _apiCall.delete( + '${Stopwords.resourcepath}/$id', + ); + return StopwordDeleteSchema.fromJson(response); + } +} diff --git a/lib/src/stopwords.dart b/lib/src/stopwords.dart new file mode 100644 index 0000000..e207342 --- /dev/null +++ b/lib/src/stopwords.dart @@ -0,0 +1,25 @@ +import 'models/models.dart'; +import 'services/api_call.dart'; + +class Stopwords { + final ApiCall _apiCall; + static const String resourcepath = '/stopwords'; + + Stopwords(ApiCall apiCall) : _apiCall = apiCall; + + /// Creates/updates a stopwords set corresponding to [stopwordId]. + Future upsert( + String stopwordId, StopwordCreateSchema params) async { + final response = await _apiCall.put( + '$resourcepath/$stopwordId', + bodyParameters: params.toJson(), + ); + return StopwordSchema.fromJson(response); + } + + /// Retrieves all stopwords sets. + Future retrieve() async { + final response = await _apiCall.get(resourcepath); + return StopwordsRetrieveSchema.fromJson(response); + } +} diff --git a/lib/src/synonym.dart b/lib/src/synonym.dart index ab0c940..1ef2f4f 100644 --- a/lib/src/synonym.dart +++ b/lib/src/synonym.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'collections.dart'; import 'synonyms.dart'; import 'services/api_call.dart'; @@ -5,20 +7,36 @@ import 'services/api_call.dart'; class Synonym { final String _collectionName, _synonymId; final ApiCall _apiCall; + static bool _warnedDeprecated = false; const Synonym(String collectionName, String synonymId, ApiCall apiCall) : _collectionName = collectionName, _synonymId = synonymId, _apiCall = apiCall; + static void _warnDeprecated() { + if (_warnedDeprecated) { + return; + } + _warnedDeprecated = true; + stderr.writeln( + "[typesense] 'Synonym' is deprecated on Typesense Server v30+. " + "Use client.synonymSets instead.", + ); + } + /// Retrieves a synonym. Future> retrieve() async { - return await _apiCall.get(_endpointPath); + _warnDeprecated(); + final response = await _apiCall.get(_endpointPath); + return Map.from(response); } /// Deletes a synonym. Future> delete() async { - return await _apiCall.delete(_endpointPath); + _warnDeprecated(); + final response = await _apiCall.delete(_endpointPath); + return Map.from(response); } String get _endpointPath => diff --git a/lib/src/synonym_set.dart b/lib/src/synonym_set.dart new file mode 100644 index 0000000..50f568a --- /dev/null +++ b/lib/src/synonym_set.dart @@ -0,0 +1,71 @@ +import 'models/models.dart'; +import 'services/api_call.dart'; +import 'synonym_sets.dart'; +import 'synonym_set_items.dart'; +import 'synonym_set_item.dart'; + +class SynonymSet { + final String _name; + final ApiCall _apiCall; + final SynonymSetItems _items; + final _individualItems = {}; + + SynonymSet(String name, ApiCall apiCall) + : _name = name, + _apiCall = apiCall, + _items = SynonymSetItems(name, apiCall); + + Future retrieve() async { + final response = await _apiCall.get(_endpointPath); + return SynonymSetRetrieveSchema.fromJson(response); + } + + Future upsert(SynonymSetCreateSchema params) async { + final response = await _apiCall.put( + _endpointPath, + bodyParameters: params.toJson(), + ); + return SynonymSetCreateSchema.fromJson(response); + } + + Future delete() async { + final response = await _apiCall.delete(_endpointPath); + return SynonymSetDeleteSchema.fromJson(response); + } + + SynonymSetItems items() => _items; + + SynonymSetItem item(String itemId) { + if (!_individualItems.containsKey(itemId)) { + _individualItems[itemId] = SynonymSetItem(_name, itemId, _apiCall); + } + return _individualItems[itemId]!; + } + + /// Retrieves items in the synonym set with optional pagination. + Future> listItems({ + int? limit, + int? offset, + }) async { + return _items.retrieve(limit: limit, offset: offset); + } + + /// Retrieves a single synonym item by [itemId]. + Future getItem(String itemId) async { + return item(itemId).retrieve(); + } + + /// Creates/updates a synonym item by [itemId]. + Future upsertItem( + String itemId, SynonymItemSchema params) async { + return item(itemId).upsert(params); + } + + /// Deletes a synonym item by [itemId]. + Future deleteItem(String itemId) async { + return item(itemId).delete(); + } + + String get _endpointPath => + '${SynonymSets.resourcepath}/${Uri.encodeComponent(_name)}'; +} diff --git a/lib/src/synonym_set_item.dart b/lib/src/synonym_set_item.dart new file mode 100644 index 0000000..3deb6ed --- /dev/null +++ b/lib/src/synonym_set_item.dart @@ -0,0 +1,35 @@ +import 'models/models.dart'; +import 'services/api_call.dart'; +import 'synonym_sets.dart'; + +class SynonymSetItem { + final String _synonymSetName; + final String _itemId; + final ApiCall _apiCall; + + SynonymSetItem(String synonymSetName, String itemId, ApiCall apiCall) + : _synonymSetName = synonymSetName, + _itemId = itemId, + _apiCall = apiCall; + + Future retrieve() async { + final response = await _apiCall.get(_endpointPath); + return SynonymItemSchema.fromJson(response); + } + + Future upsert(SynonymItemSchema params) async { + final response = await _apiCall.put( + _endpointPath, + bodyParameters: params.toJson(), + ); + return SynonymItemSchema.fromJson(response); + } + + Future delete() async { + final response = await _apiCall.delete(_endpointPath); + return SynonymItemDeleteSchema.fromJson(response); + } + + String get _endpointPath => + '${SynonymSets.resourcepath}/${Uri.encodeComponent(_synonymSetName)}/items/${Uri.encodeComponent(_itemId)}'; +} diff --git a/lib/src/synonym_set_items.dart b/lib/src/synonym_set_items.dart new file mode 100644 index 0000000..da5ecb2 --- /dev/null +++ b/lib/src/synonym_set_items.dart @@ -0,0 +1,36 @@ +import 'models/models.dart'; +import 'services/api_call.dart'; +import 'synonym_sets.dart'; + +class SynonymSetItems { + final String _synonymSetName; + final ApiCall _apiCall; + + SynonymSetItems(String synonymSetName, ApiCall apiCall) + : _synonymSetName = synonymSetName, + _apiCall = apiCall; + + Future> retrieve({ + int? limit, + int? offset, + }) async { + final query = {}; + if (limit != null) { + query['limit'] = limit.toString(); + } + if (offset != null) { + query['offset'] = offset.toString(); + } + final response = await _apiCall.getList( + _endpointPath, + queryParams: query.isEmpty ? null : query, + ); + return response + .map((item) => + SynonymItemSchema.fromJson(Map.from(item))) + .toList(); + } + + String get _endpointPath => + '${SynonymSets.resourcepath}/${Uri.encodeComponent(_synonymSetName)}/items'; +} diff --git a/lib/src/synonym_sets.dart b/lib/src/synonym_sets.dart new file mode 100644 index 0000000..124d6f5 --- /dev/null +++ b/lib/src/synonym_sets.dart @@ -0,0 +1,22 @@ +import 'models/models.dart'; +import 'services/api_call.dart'; +import 'synonym_set.dart'; + +class SynonymSets { + final ApiCall _apiCall; + static const String resourcepath = '/synonym_sets'; + + SynonymSets(ApiCall apiCall) : _apiCall = apiCall; + + /// Retrieves all synonym sets. + Future> retrieve() async { + final response = await _apiCall.getList(resourcepath); + return response + .map((item) => + SynonymSetSchema.fromJson(Map.from(item))) + .toList(); + } + + SynonymSet operator [](String synonymSetName) => + SynonymSet(synonymSetName, _apiCall); +} diff --git a/lib/src/synonyms.dart b/lib/src/synonyms.dart index ca6b656..91adb84 100644 --- a/lib/src/synonyms.dart +++ b/lib/src/synonyms.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'collections.dart'; import 'services/api_call.dart'; @@ -5,21 +7,37 @@ class Synonyms { final String _collectionName; final ApiCall _apiCall; static const resourcepath = '/synonyms'; + static bool _warnedDeprecated = false; const Synonyms(String collectionName, ApiCall apiCall) : _collectionName = collectionName, _apiCall = apiCall; + static void _warnDeprecated() { + if (_warnedDeprecated) { + return; + } + _warnedDeprecated = true; + stderr.writeln( + "[typesense] 'Synonyms' is deprecated on Typesense Server v30+. " + "Use client.synonymSets instead.", + ); + } + /// Creates/updates a synonym corresponding to [synonymId]. Future> upsert( String synonymId, Map params) async { - return await _apiCall.put('$_endpointPath/$synonymId', + _warnDeprecated(); + final response = await _apiCall.put('$_endpointPath/$synonymId', bodyParameters: params); + return Map.from(response); } /// Retrieves all synonyms. Future> retrieve() async { - return await _apiCall.get(_endpointPath); + _warnDeprecated(); + final response = await _apiCall.get(_endpointPath); + return Map.from(response); } String get _endpointPath => diff --git a/lib/typesense.dart b/lib/typesense.dart index bd74efc..98434ce 100644 --- a/lib/typesense.dart +++ b/lib/typesense.dart @@ -5,3 +5,20 @@ export 'src/models/models.dart'; export 'src/collection.dart'; export 'src/collections.dart'; export 'src/exceptions/exceptions.dart'; +export 'src/curation_sets.dart'; +export 'src/curation_set.dart'; +export 'src/synonym_sets.dart'; +export 'src/synonym_set.dart'; +export 'src/stemming.dart'; +export 'src/stemming_dictionaries.dart'; +export 'src/stemming_dictionary.dart'; +export 'src/conversations_models.dart'; +export 'src/conversation_model.dart'; +export 'src/nl_search_models.dart'; +export 'src/nl_search_model.dart'; +export 'src/conversations.dart'; +export 'src/conversation.dart'; +export 'src/analytics.dart'; +export 'src/analytics_rules.dart'; +export 'src/analytics_rule.dart'; +export 'src/analytics_events.dart'; diff --git a/pubspec.yaml b/pubspec.yaml index 377bd4f..9178cce 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,6 +13,6 @@ dependencies: dev_dependencies: test: ^1.25.14 mockito: ^5.4.5 - lints: ^5.1.1 + lints: ^6.0.0 build_runner: ^2.4.14 - analyzer: ^7.2.0 + analyzer: ^10.0.0 diff --git a/test/analytics_integration_test.dart b/test/analytics_integration_test.dart new file mode 100644 index 0000000..e6bfd08 --- /dev/null +++ b/test/analytics_integration_test.dart @@ -0,0 +1,184 @@ +import 'dart:io'; + +import 'package:test/test.dart'; +import 'package:typesense/typesense.dart'; + +void main() { + final env = Platform.environment; + final apiKey = env['TYPESENSE_API_KEY'] ?? 'xyz'; + 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; + + late Client client; + const companiesCollection = 'companies'; + const queriesCollection = 'companies_queries'; + const ruleName = 'company_analytics_rule'; + + setUpAll(() async { + final config = Configuration( + apiKey, + nodes: { + Node( + protocol, + host, + port: port, + ), + }, + ); + client = Client(config); + + await _recreateCollection(client, companiesCollection); + await _recreateCollection(client, queriesCollection); + }); + + tearDownAll(() async { + try { + await client.collection(companiesCollection).delete(); + } catch (_) {} + try { + await client.collection(queriesCollection).delete(); + } catch (_) {} + try { + await client.analytics.rule(ruleName).delete(); + } catch (_) {} + }); + + 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, + ), + ), + ); + expect(created, isA()); + + final updated = await client.analytics.rules().upsert( + ruleName, + AnalyticsRuleUpsertSchema( + params: AnalyticsRuleParams( + destinationCollection: queriesCollection, + limit: 500, + ), + ), + ); + expect(updated, isA()); + expect(updated.name, equals(ruleName)); + + final rules = await client.analytics.rules().retrieve(); + expect(rules, isA>()); + expect(rules.any((rule) => rule.name == ruleName), isTrue); + + final deleted = await client.analytics.rule(ruleName).delete(); + expect(deleted, isA()); + expect(deleted.name, equals(ruleName)); + }); + + test('analytics rules createMany', () async { + const ruleName1 = '${ruleName}_1'; + const ruleName2 = '${ruleName}_2'; + try { + await client.analytics.rule(ruleName1).delete(); + } catch (_) {} + try { + await client.analytics.rule(ruleName2).delete(); + } catch (_) {} + + final created = await client.analytics.rules().createMany([ + AnalyticsRuleCreateSchema( + name: ruleName1, + type: 'nohits_queries', + collection: companiesCollection, + eventType: 'search', + params: AnalyticsRuleParams( + destinationCollection: queriesCollection, + limit: 1000, + ), + ), + AnalyticsRuleCreateSchema( + name: ruleName2, + type: 'log', + collection: companiesCollection, + eventType: 'click', + params: AnalyticsRuleParams(), + ), + ]); + expect(created, isA>()); + expect(created.length, equals(2)); + expect(created[0], isA()); + expect(created[1], isA()); + expect(created[0].name, equals(ruleName1)); + expect(created[1].name, equals(ruleName2)); + + try { + await client.analytics.rule(ruleName1).delete(); + } catch (_) {} + try { + await client.analytics.rule(ruleName2).delete(); + } catch (_) {} + }); + + test('analytics events create, retrieve, status, flush', () async { + try { + await client.analytics.rule(ruleName).delete(); + } catch (_) {} + + await client.analytics.rules().create( + AnalyticsRuleCreateSchema( + name: ruleName, + type: 'log', + collection: companiesCollection, + eventType: 'click', + params: AnalyticsRuleParams(), + ), + ); + + final event = AnalyticsEventCreateSchema( + name: ruleName, + data: { + 'user_id': 'user-1', + 'doc_id': 'apple', + }, + ); + final created = await client.analytics.events().create(event); + expect(created, isA()); + expect(created.ok, isTrue); + + final retrieved = await client.analytics.events().retrieve( + userId: 'user-1', + name: ruleName, + n: 10, + ); + expect(retrieved, isA()); + + final status = await client.analytics.events().status(); + expect(status, isA()); + + final flushed = await client.analytics.events().flush(); + expect(flushed, isA()); + }); +} + +Future _recreateCollection(Client client, String name) async { + try { + await client.collection(name).delete(); + } catch (_) {} + try { + await client.collections.create( + Schema( + name, + { + Field('user_id', type: Type.string), + }, + ), + ); + } catch (_) {} +} diff --git a/test/conversations_models_integration_test.dart b/test/conversations_models_integration_test.dart new file mode 100644 index 0000000..f210f11 --- /dev/null +++ b/test/conversations_models_integration_test.dart @@ -0,0 +1,119 @@ +import 'dart:io'; + +import 'package:test/test.dart'; +import 'package:typesense/typesense.dart'; + +void main() { + final env = Platform.environment; + final apiKey = env['TYPESENSE_API_KEY'] ?? 'xyz'; + 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 openAiApiKey = env['OPENAI_API_KEY']; + final historyCollection = + env['CONVERSATION_HISTORY_COLLECTION'] ?? 'conversations_history'; + + final skipReason = (openAiApiKey == null || openAiApiKey.isEmpty) + ? 'Set OPENAI_API_KEY to run conversation model tests.' + : null; + + late Client client; + late String modelId; + + setUpAll(() async { + if (skipReason != null) { + return; + } + final config = Configuration( + apiKey, + nodes: { + Node( + protocol, + host, + port: port, + ), + }, + ); + client = Client(config); + modelId = + 'conversation_model_dart_test_${DateTime.now().millisecondsSinceEpoch}'; + await _ensureHistoryCollection(client, historyCollection); + }); + + tearDownAll(() async { + if (skipReason != null) { + return; + } + try { + await client.conversationModel(modelId).delete(); + } catch (_) { + // ignore cleanup errors + } + }); + + test('create, retrieve, update, delete conversation model', () async { + if (skipReason != null) { + return; + } + + final created = await client.conversationsModels.create( + ConversationModelCreateSchema( + id: modelId, + modelName: + env['CONVERSATION_MODEL_NAME'] ?? 'openai/gpt-3.5-turbo', + apiKey: openAiApiKey, + maxBytes: 16384, + historyCollection: historyCollection, + systemPrompt: 'This is meant for testing purposes', + ), + ); + expect(created, isA()); + + final retrieved = await client.conversationModel(modelId).retrieve(); + expect(retrieved, isA()); + 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, + ), + ); + expect(updated, isA()); + + final list = await client.conversationsModels.retrieve(); + expect(list, isA>()); + expect(list.any((model) => model.id == modelId), isTrue); + + final deleted = await client.conversationModel(modelId).delete(); + expect(deleted, isA()); + expect(deleted.id, equals(modelId)); + }, skip: skipReason); +} + +Future _ensureHistoryCollection(Client client, String name) async { + final schema = Schema( + name, + { + Field('conversation_id', type: Type.string), + Field('model_id', type: Type.string), + Field('timestamp', type: Type.int32), + Field('role', type: Type.string, shouldIndex: false), + Field('message', type: Type.string, shouldIndex: false), + }, + ); + + try { + await client.collection(name).delete(); + } catch (_) {} + try { + await client.collections.create(schema); + } catch (_) {} +} diff --git a/test/curation_sets_integration_test.dart b/test/curation_sets_integration_test.dart new file mode 100644 index 0000000..c31099c --- /dev/null +++ b/test/curation_sets_integration_test.dart @@ -0,0 +1,116 @@ +import 'dart:io'; + +import 'package:test/test.dart'; +import 'package:typesense/typesense.dart'; + +void main() { + final env = Platform.environment; + final apiKey = env['TYPESENSE_API_KEY'] ?? 'xyz'; + 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; + + late Client client; + late String setName; + late String itemId; + + setUpAll(() { + final config = Configuration( + apiKey, + nodes: { + Node( + protocol, + host, + port: port, + ), + }, + ); + client = Client(config); + final timestamp = DateTime.now().millisecondsSinceEpoch; + setName = 'curation_set_dart_test_$timestamp'; + itemId = 'curation_item_$timestamp'; + }); + + tearDownAll(() async { + try { + await client.curationSet(setName).delete(); + } catch (_) { + // ignore cleanup errors + } + }); + + 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, + ), + ], + metadata: { + 'source': 'dart-tests', + }, + ), + ], + ), + ); + + expect(upserted, isA()); + expect(upserted.items.any((item) => item.id == itemId), isTrue); + + final retrieved = await client.curationSet(setName).retrieve(); + expect(retrieved, isA()); + expect(retrieved.items.any((item) => item.id == itemId), isTrue); + }); + + 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', + ), + ], + ), + ], + ), + ); + + + 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, + ); + expect(items, isA>()); + expect(items.any((item) => item.id == itemId), isTrue); + + final retrievedItem = await client.curationSet(setName).item(itemId).retrieve(); + expect(retrievedItem, isA()); + expect(retrievedItem.id, equals(itemId)); + + final deleted = await client.curationSet(setName).item(itemId).delete(); + expect(deleted, isA()); + expect(deleted.id, equals(itemId)); + }); +} diff --git a/test/nl_search_models_integration_test.dart b/test/nl_search_models_integration_test.dart new file mode 100644 index 0000000..03e201d --- /dev/null +++ b/test/nl_search_models_integration_test.dart @@ -0,0 +1,95 @@ +import 'dart:io'; + +import 'package:test/test.dart'; +import 'package:typesense/typesense.dart'; + +void main() { + final env = Platform.environment; + final apiKey = env['TYPESENSE_API_KEY'] ?? 'xyz'; + 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 openAiApiKey = env['OPENAI_API_KEY']; + + final skipReason = (openAiApiKey == null || openAiApiKey.isEmpty) + ? 'Set OPENAI_API_KEY to run NL search model tests.' + : null; + + late Client client; + late String modelId; + + setUpAll(() { + if (skipReason != null) { + return; + } + final config = Configuration( + apiKey, + nodes: { + Node( + protocol, + host, + port: port, + ), + }, + ); + client = Client(config); + modelId = 'nl_search_model_dart_test_${DateTime.now().millisecondsSinceEpoch}'; + }); + + tearDownAll(() async { + if (skipReason != null) { + return; + } + try { + await client.nlSearchModel(modelId).delete(); + } catch (_) { + // ignore cleanup errors + } + }); + + test('create, retrieve, update, delete NL search model', () async { + if (skipReason != null) { + return; + } + + final created = await client.nlSearchModels.create( + NLSearchModelCreateSchema( + id: modelId, + modelName: + env['NL_SEARCH_MODEL_NAME'] ?? 'openai/gpt-3.5-turbo', + apiKey: openAiApiKey, + maxBytes: 16384, + systemPrompt: 'This is meant for testing purposes', + ), + ); + expect(created, isA()); + expect(created.id, equals(modelId)); + + final retrieved = await client.nlSearchModel(modelId).retrieve(); + expect(retrieved, isA()); + 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', + ), + ); + expect(updated, isA()); + expect(updated.systemPrompt, + equals('This is a new system prompt for NL search')); + + final list = await client.nlSearchModels.retrieve(); + expect(list, isA>()); + expect(list.any((model) => model.id == modelId), isTrue); + + final deleted = await client.nlSearchModel(modelId).delete(); + expect(deleted, isA()); + expect(deleted.id, equals(modelId)); + }, skip: skipReason); +} diff --git a/test/stemming_integration_test.dart b/test/stemming_integration_test.dart new file mode 100644 index 0000000..4422872 --- /dev/null +++ b/test/stemming_integration_test.dart @@ -0,0 +1,53 @@ +import 'dart:io'; + +import 'package:test/test.dart'; +import 'package:typesense/typesense.dart'; + +void main() { + final env = Platform.environment; + final apiKey = env['TYPESENSE_API_KEY'] ?? 'xyz'; + 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; + + late Client client; + late String dictionaryId; + + setUpAll(() { + final config = Configuration( + apiKey, + nodes: { + Node( + protocol, + host, + port: port, + ), + }, + ); + client = Client(config); + dictionaryId = + 'stemming_dict_dart_test_${DateTime.now().millisecondsSinceEpoch}'; + }); + + test('upsert and retrieve stemming dictionaries', () async { + final upserted = await client.stemming.dictionaries.upsert( + dictionaryId, + [ + StemmingDictionaryCreateSchema(word: 'shoes', root: 'shoe'), + StemmingDictionaryCreateSchema(word: 'running', root: 'run'), + ], + ); + expect(upserted, isA>()); + expect(upserted.isNotEmpty, isTrue); + + final list = await client.stemming.dictionaries.retrieve(); + expect(list, isA()); + expect(list.dictionaries.contains(dictionaryId), isTrue); + + 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 new file mode 100644 index 0000000..94eff6a --- /dev/null +++ b/test/stopwords_integration_test.dart @@ -0,0 +1,78 @@ +import 'dart:io'; + +import 'package:test/test.dart'; +import 'package:typesense/typesense.dart'; + +void main() { + final env = Platform.environment; + final apiKey = env['TYPESENSE_API_KEY'] ?? 'xyz'; + 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; + + + late Client client; + late String stopwordId; + + setUpAll(() { + final config = Configuration( + apiKey, + nodes: { + Node( + protocol, + host, + port: port, + ), + }, + ); + client = Client(config); + stopwordId = + 'stopwords_dart_test_${DateTime.now().millisecondsSinceEpoch}'; + }); + + tearDownAll(() async { + try { + await client.stopword(stopwordId).delete(); + } catch (_) { + // ignore cleanup errors + } + }); + + test('upsert and retrieve stopwords', () async { + final upserted = await client.stopwords.upsert( + stopwordId, + StopwordCreateSchema( + stopwords: ['a', 'the', 'of'], + ), + ); + expect(upserted, isA()); + expect(upserted.id, equals(stopwordId)); + + final allStopwords = await client.stopwords.retrieve(); + expect(allStopwords, isA()); + expect(allStopwords.stopwords, isNotEmpty); + expect( + allStopwords.stopwords.any((item) => item.id == stopwordId), + isTrue, + ); + }); + + test('retrieve and delete stopword', () async { + await client.stopwords.upsert( + stopwordId, + StopwordCreateSchema( + stopwords: ['and', 'or'], + ), + ); + + final retrieved = await client.stopword(stopwordId).retrieve(); + expect(retrieved, isA()); + expect(retrieved.id, equals(stopwordId)); + + final deleted = await client.stopword(stopwordId).delete(); + expect(deleted, isA()); + expect(deleted.id, equals(stopwordId)); + }); +} diff --git a/test/synonym_sets_integration_test.dart b/test/synonym_sets_integration_test.dart new file mode 100644 index 0000000..697367a --- /dev/null +++ b/test/synonym_sets_integration_test.dart @@ -0,0 +1,97 @@ +import 'dart:io'; + +import 'package:test/test.dart'; +import 'package:typesense/typesense.dart'; + +void main() { + final env = Platform.environment; + final apiKey = env['TYPESENSE_API_KEY'] ?? 'xyz'; + 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; + + late Client client; + late String setName; + late String itemId; + + setUpAll(() { + final config = Configuration( + apiKey, + nodes: { + Node( + protocol, + host, + port: port, + ), + }, + ); + client = Client(config); + final timestamp = DateTime.now().millisecondsSinceEpoch; + setName = 'synonym_set_dart_test_$timestamp'; + itemId = 'synonym_item_$timestamp'; + }); + + tearDownAll(() async { + try { + await client.synonymSet(setName).delete(); + } catch (_) { + // ignore cleanup errors + } + }); + + test('upsert and retrieve synonym set', () async { + final upserted = await client.synonymSet(setName).upsert( + SynonymSetCreateSchema( + items: [ + SynonymItemSchema( + id: itemId, + synonyms: ['sneakers', 'shoes'], + ), + ], + ), + ); + + expect(upserted, isA()); + expect(upserted.items.any((item) => item.id == itemId), isTrue); + + final retrieved = await client.synonymSet(setName).retrieve(); + expect(retrieved, isA()); + expect(retrieved.items.any((item) => item.id == itemId), isTrue); + }); + + test('list, items, and item CRUD', () async { + await client.synonymSet(setName).upsert( + 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, + ); + expect(items, isA>()); + expect(items.any((item) => item.id == itemId), isTrue); + + final retrievedItem = + await client.synonymSet(setName).item(itemId).retrieve(); + expect(retrievedItem, isA()); + expect(retrievedItem.id, equals(itemId)); + + final deleted = + await client.synonymSet(setName).item(itemId).delete(); + expect(deleted, isA()); + expect(deleted.id, equals(itemId)); + }); +}