From 8275a316153d34f0c1ca06eef735d8896c9e2790 Mon Sep 17 00:00:00 2001 From: "mario.danmarioalexandrudan" Date: Thu, 30 Apr 2026 17:34:24 +0300 Subject: [PATCH 01/15] feat(dart): add transformationOptions and *WithTransformation helpers - Add TransformationOptions to client_core - Add transformationOptions field to ClientOptions - Generate algolia_client_ingestion Dart package - Add ingestion transporter wiring to SearchClient template - Implement chunkedPush, saveObjectsWithTransformation, partialUpdateObjectsWithTransformation, and replaceAllObjectsWithTransformation extensions - Update pubspec_overrides across docs/tests/playground --- .../client_core/lib/algolia_client_core.dart | 1 + .../lib/src/config/client_options.dart | 13 +- .../src/config/transformation_options.dart | 18 ++ .../client_search/lib/src/extension.dart | 1 + .../lib/src/extension/transformation.dart | 259 ++++++++++++++++++ config/clients.config.json | 4 + playground/dart/pubspec_overrides.yaml | 2 + templates/dart/api.mustache | 53 +++- templates/dart/pubspec.mustache | 3 + .../dart/pubspec_overrides.tests.mustache | 2 + tests/output/dart/pubspec_overrides.yaml | 2 + 11 files changed, 353 insertions(+), 5 deletions(-) create mode 100644 clients/algoliasearch-client-dart/packages/client_core/lib/src/config/transformation_options.dart create mode 100644 clients/algoliasearch-client-dart/packages/client_search/lib/src/extension/transformation.dart diff --git a/clients/algoliasearch-client-dart/packages/client_core/lib/algolia_client_core.dart b/clients/algoliasearch-client-dart/packages/client_core/lib/algolia_client_core.dart index 9cc1adc0a02..c27b72414e2 100644 --- a/clients/algoliasearch-client-dart/packages/client_core/lib/algolia_client_core.dart +++ b/clients/algoliasearch-client-dart/packages/client_core/lib/algolia_client_core.dart @@ -10,6 +10,7 @@ export 'src/api_client.dart'; export 'src/config/agent_segment.dart'; export 'src/config/client_options.dart'; export 'src/config/host.dart'; +export 'src/config/transformation_options.dart'; export 'src/transport/api_request.dart'; export 'src/transport/request_options.dart'; export 'src/transport/requester.dart'; diff --git a/clients/algoliasearch-client-dart/packages/client_core/lib/src/config/client_options.dart b/clients/algoliasearch-client-dart/packages/client_core/lib/src/config/client_options.dart index 93489f61927..8b11d899262 100644 --- a/clients/algoliasearch-client-dart/packages/client_core/lib/src/config/client_options.dart +++ b/clients/algoliasearch-client-dart/packages/client_core/lib/src/config/client_options.dart @@ -2,6 +2,7 @@ import 'dart:core'; import 'package:algolia_client_core/src/config/agent_segment.dart'; import 'package:algolia_client_core/src/config/host.dart'; +import 'package:algolia_client_core/src/config/transformation_options.dart'; import 'package:algolia_client_core/src/transport/requester.dart'; import 'package:dio/dio.dart'; @@ -42,6 +43,9 @@ final class ClientOptions { /// Set to 'gzip' to enable gzip compression for POST/PUT requests. final String? compression; + /// Options for the ingestion transporter used by `*WithTransformation` helpers on [SearchClient]. + final TransformationOptions? transformationOptions; + /// Constructs a [ClientOptions] instance with the provided parameters. const ClientOptions({ this.connectTimeout = const Duration(seconds: 2), @@ -55,6 +59,7 @@ final class ClientOptions { this.interceptors, this.httpClientAdapter, this.compression, + this.transformationOptions, }); @override @@ -72,7 +77,8 @@ final class ClientOptions { requester == other.requester && interceptors == other.interceptors && httpClientAdapter == other.httpClientAdapter && - compression == other.compression; + compression == other.compression && + transformationOptions == other.transformationOptions; @override int get hashCode => @@ -86,10 +92,11 @@ final class ClientOptions { requester.hashCode ^ interceptors.hashCode ^ httpClientAdapter.hashCode ^ - compression.hashCode; + compression.hashCode ^ + transformationOptions.hashCode; @override String toString() { - return 'ClientOptions{hosts: $hosts, connectTimeout: $connectTimeout, writeTimeout: $writeTimeout, readTimeout: $readTimeout, headers: $headers, agentSegments: $agentSegments, logger: $logger, requester: $requester, interceptors: $interceptors, httpClientAdapter: $httpClientAdapter, compression: $compression}'; + return 'ClientOptions{hosts: $hosts, connectTimeout: $connectTimeout, writeTimeout: $writeTimeout, readTimeout: $readTimeout, headers: $headers, agentSegments: $agentSegments, logger: $logger, requester: $requester, interceptors: $interceptors, httpClientAdapter: $httpClientAdapter, compression: $compression, transformationOptions: $transformationOptions}'; } } diff --git a/clients/algoliasearch-client-dart/packages/client_core/lib/src/config/transformation_options.dart b/clients/algoliasearch-client-dart/packages/client_core/lib/src/config/transformation_options.dart new file mode 100644 index 00000000000..bc1d46a50db --- /dev/null +++ b/clients/algoliasearch-client-dart/packages/client_core/lib/src/config/transformation_options.dart @@ -0,0 +1,18 @@ +import 'package:algolia_client_core/src/config/client_options.dart'; + +final class TransformationOptions { + final String region; + final ClientOptions? ingestionClientOptions; + + TransformationOptions({ + required this.region, + this.ingestionClientOptions, + }) { + if (region.isEmpty) { + throw ArgumentError( + 'region is required in transformationOptions.' + ' See https://www.algolia.com/doc/libraries/sdk/methods/ingestion', + ); + } + } +} diff --git a/clients/algoliasearch-client-dart/packages/client_search/lib/src/extension.dart b/clients/algoliasearch-client-dart/packages/client_search/lib/src/extension.dart index 8858090b4cc..3733ea7b478 100644 --- a/clients/algoliasearch-client-dart/packages/client_search/lib/src/extension.dart +++ b/clients/algoliasearch-client-dart/packages/client_search/lib/src/extension.dart @@ -1,2 +1,3 @@ export 'extension/search.dart'; +export 'extension/transformation.dart'; export 'extension/wait_task.dart'; diff --git a/clients/algoliasearch-client-dart/packages/client_search/lib/src/extension/transformation.dart b/clients/algoliasearch-client-dart/packages/client_search/lib/src/extension/transformation.dart new file mode 100644 index 00000000000..f05ff45fd14 --- /dev/null +++ b/clients/algoliasearch-client-dart/packages/client_search/lib/src/extension/transformation.dart @@ -0,0 +1,259 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:algolia_client_core/algolia_client_core.dart'; +import 'package:algolia_client_ingestion/algolia_client_ingestion.dart' as ingestion; +import 'package:algolia_client_search/src/api/search_client.dart'; +import 'package:algolia_client_search/src/extension/wait_task.dart'; +import 'package:algolia_client_search/src/model/event.dart'; +import 'package:algolia_client_search/src/model/event_status.dart'; +import 'package:algolia_client_search/src/model/event_type.dart'; +import 'package:algolia_client_search/src/model/operation_index_params.dart'; +import 'package:algolia_client_search/src/model/operation_type.dart'; +import 'package:algolia_client_search/src/model/replace_all_objects_with_transformation_response.dart'; +import 'package:algolia_client_search/src/model/scope_type.dart'; +import 'package:algolia_client_search/src/model/watch_response.dart'; + +extension Transformation on SearchClient { + static const _notSetError = + 'transformationOptions must be set in the client config before calling this method.' + ' It defaults to the Ingestion API defaults.' + ' See https://www.algolia.com/doc/libraries/sdk/methods/ingestion'; + + /// Chunks [objects] and pushes them through the Ingestion pipeline with [action]. + Future> chunkedPush({ + required String indexName, + required Iterable> objects, + required ingestion.Action action, + bool waitForTasks = false, + int batchSize = 1000, + String? referenceIndexName, + RequestOptions? requestOptions, + }) async { + if (batchSize < 1) throw ArgumentError('`batchSize` must be greater than 0'); + final transporter = ingestionTransporter; + if (transporter == null) throw StateError(_notSetError); + + final responses = []; + final batch = []; + final pollInterval = (batchSize ~/ 10).clamp(1, batchSize); + int polledUpTo = 0; + + final iter = objects.iterator; + if (!iter.moveNext()) return responses; + + while (true) { + batch.add(_toRecord(iter.current)); + final isLast = !iter.moveNext(); + + if (batch.length == batchSize || isLast) { + final raw = await transporter.push( + indexName: indexName, + pushTaskPayload: ingestion.PushTaskPayload(action: action, records: List.of(batch)), + referenceIndexName: referenceIndexName, + requestOptions: requestOptions, + ); + responses.add(_convertWatchResponse(raw)); + batch.clear(); + + if (waitForTasks && + (responses.length % pollInterval == 0 || isLast)) { + await _pollBatch( + transporter: transporter, + responses: responses, + from: polledUpTo, + to: responses.length, + requestOptions: requestOptions, + ); + polledUpTo = responses.length; + } + } + + if (isLast) break; + } + + return responses; + } + + /// Saves objects through the Ingestion pipeline. Requires [TransformationOptions] to be set. + Future> saveObjectsWithTransformation({ + required String indexName, + required Iterable> objects, + bool waitForTasks = false, + int batchSize = 1000, + RequestOptions? requestOptions, + }) { + return chunkedPush( + indexName: indexName, + objects: objects, + action: ingestion.Action.addObject, + waitForTasks: waitForTasks, + batchSize: batchSize, + requestOptions: requestOptions, + ); + } + + /// Partially updates objects through the Ingestion pipeline. Requires [TransformationOptions] to be set. + Future> partialUpdateObjectsWithTransformation({ + required String indexName, + required Iterable> objects, + bool createIfNotExists = true, + bool waitForTasks = false, + int batchSize = 1000, + RequestOptions? requestOptions, + }) { + return chunkedPush( + indexName: indexName, + objects: objects, + action: createIfNotExists + ? ingestion.Action.partialUpdateObject + : ingestion.Action.partialUpdateObjectNoCreate, + waitForTasks: waitForTasks, + batchSize: batchSize, + requestOptions: requestOptions, + ); + } + + /// Replaces all objects in [indexName] via the Ingestion pipeline without downtime. + /// Requires [TransformationOptions] to be set. + Future replaceAllObjectsWithTransformation({ + required String indexName, + required Iterable> objects, + int batchSize = 1000, + List? scopes, + RequestOptions? requestOptions, + }) async { + if (ingestionTransporter == null) throw StateError(_notSetError); + + final effectiveScopes = scopes ?? [ScopeType.settings, ScopeType.rules, ScopeType.synonyms]; + final tmpIndex = '${indexName}_tmp_${Random().nextInt(900000) + 100000}'; + + try { + var copyResponse = await operationIndex( + indexName: indexName, + operationIndexParams: OperationIndexParams( + operation: OperationType.copy, + destination: tmpIndex, + scope: effectiveScopes, + ), + requestOptions: requestOptions, + ); + + final watchResponses = await chunkedPush( + indexName: tmpIndex, + objects: objects, + action: ingestion.Action.addObject, + waitForTasks: true, + batchSize: batchSize, + referenceIndexName: indexName, + requestOptions: requestOptions, + ); + + await waitTask(indexName: tmpIndex, taskID: copyResponse.taskID, requestOptions: requestOptions); + + copyResponse = await operationIndex( + indexName: indexName, + operationIndexParams: OperationIndexParams( + operation: OperationType.copy, + destination: tmpIndex, + scope: effectiveScopes, + ), + requestOptions: requestOptions, + ); + await waitTask(indexName: tmpIndex, taskID: copyResponse.taskID, requestOptions: requestOptions); + + final moveResponse = await operationIndex( + indexName: tmpIndex, + operationIndexParams: OperationIndexParams( + operation: OperationType.move, + destination: indexName, + ), + requestOptions: requestOptions, + ); + await waitTask(indexName: tmpIndex, taskID: moveResponse.taskID, requestOptions: requestOptions); + + return ReplaceAllObjectsWithTransformationResponse( + copyOperationResponse: copyResponse, + watchResponses: watchResponses, + moveOperationResponse: moveResponse, + ); + } catch (_) { + try { + await deleteIndex(indexName: tmpIndex); + } catch (_) {} + rethrow; + } + } +} + +Future _pollBatch({ + required ingestion.IngestionClient transporter, + required List responses, + required int from, + required int to, + RequestOptions? requestOptions, +}) async { + for (final resp in responses.sublist(from, to)) { + final eventID = resp.eventID; + if (eventID == null) continue; + await _waitForEvent( + transporter: transporter, + runID: resp.runID, + eventID: eventID, + requestOptions: requestOptions, + ); + } +} + +Future _waitForEvent({ + required ingestion.IngestionClient transporter, + required String runID, + required String eventID, + RequestOptions? requestOptions, +}) async { + for (var retries = 0; retries < 50; retries++) { + try { + await transporter.getEvent( + runID: runID, + eventID: eventID, + requestOptions: requestOptions, + ); + return; + } on AlgoliaApiException catch (e) { + if (e.statusCode != 404) rethrow; + } + await Future.delayed( + Duration(milliseconds: (retries * 1500).clamp(0, 5000)), + ); + } +} + +ingestion.PushTaskRecords _toRecord(Map obj) { + final objectID = obj['objectID']; + if (objectID == null || objectID is! String) { + throw ArgumentError('each object must have an `objectID` key in order to be indexed'); + } + final rest = Map.from(obj)..remove('objectID'); + return ingestion.PushTaskRecords(objectID: objectID, additionalProperties: rest); +} + +WatchResponse _convertWatchResponse(ingestion.WatchResponse r) { + return WatchResponse( + runID: r.runID, + eventID: r.eventID, + data: r.data, + events: r.events + ?.map((e) => Event( + eventID: e.eventID, + runID: e.runID, + status: e.status != null ? EventStatus.fromJson(e.status!.toJson()) : null, + type: EventType.fromJson(e.type.toJson()), + batchSize: e.batchSize, + data: e.data, + publishedAt: e.publishedAt, + )) + .toList(), + message: r.message, + createdAt: r.createdAt, + ); +} diff --git a/config/clients.config.json b/config/clients.config.json index 024770a5304..63828d3e54d 100644 --- a/config/clients.config.json +++ b/config/clients.config.json @@ -48,6 +48,10 @@ "name": "composition", "output": "clients/algoliasearch-client-dart/packages/client_composition" }, + { + "name": "ingestion", + "output": "clients/algoliasearch-client-dart/packages/client_ingestion" + }, { "name": "insights", "output": "clients/algoliasearch-client-dart/packages/client_insights" diff --git a/playground/dart/pubspec_overrides.yaml b/playground/dart/pubspec_overrides.yaml index ec9972620fd..dddb30ec12e 100644 --- a/playground/dart/pubspec_overrides.yaml +++ b/playground/dart/pubspec_overrides.yaml @@ -3,6 +3,8 @@ dependency_overrides: path: ../../clients/algoliasearch-client-dart/packages/algoliasearch algolia_client_core: path: ../../clients/algoliasearch-client-dart/packages/client_core + algolia_client_ingestion: + path: ../../clients/algoliasearch-client-dart/packages/client_ingestion algolia_client_search: path: ../../clients/algoliasearch-client-dart/packages/client_search algolia_client_insights: diff --git a/templates/dart/api.mustache b/templates/dart/api.mustache index 9c0ce01f494..8ef8f826bb4 100644 --- a/templates/dart/api.mustache +++ b/templates/dart/api.mustache @@ -7,6 +7,9 @@ import 'package:{{pubName}}/src/version.dart'; {{#operations}} {{#imports}}import '{{{.}}}'; {{/imports}} +{{#isSearchClient}} +import 'package:algolia_client_ingestion/algolia_client_ingestion.dart' as ingestion; +{{/isSearchClient}} {{#description}} /// {{{description}}} @@ -22,6 +25,12 @@ final class {{classname}} implements ApiClient { final RetryStrategy _retryStrategy; + {{#isSearchClient}} + final String _appId; + final String _apiKey; + ingestion.IngestionClient? _ingestionTransporter; + {{/isSearchClient}} + {{classname}}({ required String appId, required String apiKey, @@ -29,7 +38,7 @@ final class {{classname}} implements ApiClient { {{#hasRegionalHost}} {{^fallbackToAliasHost}}required{{/fallbackToAliasHost}} this.region, {{/hasRegionalHost}} - }) : _retryStrategy = RetryStrategy.create( + }) : {{#isSearchClient}}_appId = appId, _apiKey = apiKey, {{/isSearchClient}}_retryStrategy = RetryStrategy.create( segment: AgentSegment(value: "{{{baseName}}}", version: packageVersion), appId: appId, apiKey: apiKey, @@ -72,6 +81,11 @@ final class {{classname}} implements ApiClient { ) { assert(appId.isNotEmpty, '`appId` is missing.'); assert(apiKey.isNotEmpty, '`apiKey` is missing.'); + {{#isSearchClient}} + if (options.transformationOptions != null) { + _ingestionTransporter = _buildIngestionTransporter(options.transformationOptions!); + } + {{/isSearchClient}} } /// Allows to switch the API key used to authenticate requests. @@ -79,6 +93,38 @@ final class {{classname}} implements ApiClient { void setClientApiKey({required String apiKey}) { this._retryStrategy.requester.setClientApiKey(apiKey); } + + {{#isSearchClient}} + void setTransformationOptions(TransformationOptions transformationOptions) { + final previous = _ingestionTransporter; + _ingestionTransporter = _buildIngestionTransporter(transformationOptions); + previous?.dispose(); + } + + ingestion.IngestionClient? get ingestionTransporter => _ingestionTransporter; + + ingestion.IngestionClient _buildIngestionTransporter(TransformationOptions opts) { + final ingestionOpts = opts.ingestionClientOptions; + return ingestion.IngestionClient( + appId: _appId, + apiKey: _apiKey, + region: opts.region, + options: ClientOptions( + connectTimeout: ingestionOpts?.connectTimeout ?? const Duration(seconds: 25), + readTimeout: ingestionOpts?.readTimeout ?? const Duration(seconds: 25), + writeTimeout: ingestionOpts?.writeTimeout ?? const Duration(seconds: 25), + hosts: ingestionOpts?.hosts, + headers: ingestionOpts?.headers, + requester: ingestionOpts?.requester, + logger: ingestionOpts?.logger, + interceptors: ingestionOpts?.interceptors, + httpClientAdapter: ingestionOpts?.httpClientAdapter, + compression: ingestionOpts?.compression, + agentSegments: ingestionOpts?.agentSegments, + ), + ); + } + {{/isSearchClient}} {{#operation}} /// {{{notes}}}{{#vendorExtensions}}{{#x-acl.0}} @@ -218,6 +264,9 @@ final class {{classname}} implements ApiClient { {{/isCompositionClient}} @override - void dispose() => _retryStrategy.dispose(); + void dispose() { + _retryStrategy.dispose(); + {{#isSearchClient}}_ingestionTransporter?.dispose();{{/isSearchClient}} + } } {{/operations}} diff --git a/templates/dart/pubspec.mustache b/templates/dart/pubspec.mustache index c5401541167..53f80464d34 100644 --- a/templates/dart/pubspec.mustache +++ b/templates/dart/pubspec.mustache @@ -21,6 +21,9 @@ dependencies: algolia_client_search: ^{{{searchVersion}}} algolia_client_insights: ^{{{insightsVersion}}} {{/isAlgoliasearchClient}} + {{#isSearchClient}} + algolia_client_ingestion: ^{{{coreVersion}}} + {{/isSearchClient}} json_annotation: ^4.8.1 collection: ^1.17.1 diff --git a/templates/dart/pubspec_overrides.tests.mustache b/templates/dart/pubspec_overrides.tests.mustache index 5b2a15fb576..839034f23d6 100644 --- a/templates/dart/pubspec_overrides.tests.mustache +++ b/templates/dart/pubspec_overrides.tests.mustache @@ -3,6 +3,8 @@ dependency_overrides: path: ../../../clients/algoliasearch-client-dart/packages/algoliasearch algolia_client_core: path: ../../../clients/algoliasearch-client-dart/packages/client_core + algolia_client_ingestion: + path: ../../../clients/algoliasearch-client-dart/packages/client_ingestion algolia_client_search: path: ../../../clients/algoliasearch-client-dart/packages/client_search algolia_client_insights: diff --git a/tests/output/dart/pubspec_overrides.yaml b/tests/output/dart/pubspec_overrides.yaml index 5b2a15fb576..839034f23d6 100644 --- a/tests/output/dart/pubspec_overrides.yaml +++ b/tests/output/dart/pubspec_overrides.yaml @@ -3,6 +3,8 @@ dependency_overrides: path: ../../../clients/algoliasearch-client-dart/packages/algoliasearch algolia_client_core: path: ../../../clients/algoliasearch-client-dart/packages/client_core + algolia_client_ingestion: + path: ../../../clients/algoliasearch-client-dart/packages/client_ingestion algolia_client_search: path: ../../../clients/algoliasearch-client-dart/packages/client_search algolia_client_insights: From 2bebb0b5bafaad644890f97fa713d5e524aa47e2 Mon Sep 17 00:00:00 2001 From: "mario.danmarioalexandrudan" Date: Mon, 11 May 2026 17:38:01 +0300 Subject: [PATCH 02/15] feat(dart): enhance TransformationOptions and update ingestion transporter handling --- .../src/config/transformation_options.dart | 23 +++++++++++++++++++ ...partialUpdateObjectsWithTransformation.yml | 1 + .../replaceAllObjectsWithTransformation.yml | 1 + .../helpers/saveObjectsWithTransformation.yml | 1 + templates/dart/api.mustache | 3 +++ templates/dart/tests/client/client.mustache | 4 +--- .../dart/tests/client/createClient.mustache | 5 +++- 7 files changed, 34 insertions(+), 4 deletions(-) diff --git a/clients/algoliasearch-client-dart/packages/client_core/lib/src/config/transformation_options.dart b/clients/algoliasearch-client-dart/packages/client_core/lib/src/config/transformation_options.dart index bc1d46a50db..8025949444c 100644 --- a/clients/algoliasearch-client-dart/packages/client_core/lib/src/config/transformation_options.dart +++ b/clients/algoliasearch-client-dart/packages/client_core/lib/src/config/transformation_options.dart @@ -1,9 +1,16 @@ import 'package:algolia_client_core/src/config/client_options.dart'; +/// Options for the ingestion transporter used by `*WithTransformation` helpers. final class TransformationOptions { + /// The Algolia region for the Ingestion API (e.g. `'us'` or `'eu'`). Required. final String region; + + /// Optional overrides for the ingestion transporter's [ClientOptions]. + /// Only the fields you set here replace the Ingestion API defaults (25 s timeouts). + /// Do not set [ClientOptions.transformationOptions] here — it is ignored. final ClientOptions? ingestionClientOptions; + /// Constructs a [TransformationOptions] instance. TransformationOptions({ required this.region, this.ingestionClientOptions, @@ -15,4 +22,20 @@ final class TransformationOptions { ); } } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is TransformationOptions && + runtimeType == other.runtimeType && + region == other.region && + ingestionClientOptions == other.ingestionClientOptions; + + @override + int get hashCode => region.hashCode ^ ingestionClientOptions.hashCode; + + @override + String toString() { + return 'TransformationOptions{region: $region, ingestionClientOptions: $ingestionClientOptions}'; + } } diff --git a/specs/search/helpers/partialUpdateObjectsWithTransformation.yml b/specs/search/helpers/partialUpdateObjectsWithTransformation.yml index ae256787dfa..96eb16621c5 100644 --- a/specs/search/helpers/partialUpdateObjectsWithTransformation.yml +++ b/specs/search/helpers/partialUpdateObjectsWithTransformation.yml @@ -3,6 +3,7 @@ method: x-helper: true x-available-languages: - csharp + - dart - go - java - javascript diff --git a/specs/search/helpers/replaceAllObjectsWithTransformation.yml b/specs/search/helpers/replaceAllObjectsWithTransformation.yml index c4554b4c850..c4b3bad0cc1 100644 --- a/specs/search/helpers/replaceAllObjectsWithTransformation.yml +++ b/specs/search/helpers/replaceAllObjectsWithTransformation.yml @@ -5,6 +5,7 @@ method: - Records x-available-languages: - csharp + - dart - go - java - javascript diff --git a/specs/search/helpers/saveObjectsWithTransformation.yml b/specs/search/helpers/saveObjectsWithTransformation.yml index 090492bf3f3..a0088d4262e 100644 --- a/specs/search/helpers/saveObjectsWithTransformation.yml +++ b/specs/search/helpers/saveObjectsWithTransformation.yml @@ -5,6 +5,7 @@ method: - Records x-available-languages: - csharp + - dart - go - java - javascript diff --git a/templates/dart/api.mustache b/templates/dart/api.mustache index 41c50766c6a..7aebbaea382 100644 --- a/templates/dart/api.mustache +++ b/templates/dart/api.mustache @@ -95,12 +95,15 @@ final class {{classname}} implements ApiClient { } {{#isSearchClient}} + /// Sets (or replaces) the ingestion transporter used by `*WithTransformation` helpers. + /// The new transporter is built from [transformationOptions]; any previously created transporter is disposed. void setTransformationOptions(TransformationOptions transformationOptions) { final previous = _ingestionTransporter; _ingestionTransporter = _buildIngestionTransporter(transformationOptions); previous?.dispose(); } + /// The ingestion transporter used by `*WithTransformation` helpers, or `null` if not configured. ingestion.IngestionClient? get ingestionTransporter => _ingestionTransporter; ingestion.IngestionClient _buildIngestionTransporter(TransformationOptions opts) { diff --git a/templates/dart/tests/client/client.mustache b/templates/dart/tests/client/client.mustache index d7fe1bd07f5..310dae65d10 100644 --- a/templates/dart/tests/client/client.mustache +++ b/templates/dart/tests/client/client.mustache @@ -8,8 +8,7 @@ import 'dart:io' show Platform; void main() { {{#blocksClient}} {{#tests}} - {{^isHelper}} {{! Helper tests are not supported yet}} - test('{{{testName}}}', () async { + test('{{{testName}}}', () async { final requester = RequestInterceptor(); {{#autoCreateClient}} final client = {{client}}( @@ -56,7 +55,6 @@ void main() { } ); - {{/isHelper}} {{/tests}} {{/blocksClient}} } \ No newline at end of file diff --git a/templates/dart/tests/client/createClient.mustache b/templates/dart/tests/client/createClient.mustache index 62b0c9d566f..c99e5cc79dd 100644 --- a/templates/dart/tests/client/createClient.mustache +++ b/templates/dart/tests/client/createClient.mustache @@ -1 +1,4 @@ -{{^autoCreateClient}}final client = {{/autoCreateClient}}{{client}}(appId : "{{parametersWithDataTypeMap.appId.value}}", apiKey : "{{parametersWithDataTypeMap.apiKey.value}}",{{#hasRegionalHost}}{{#parametersWithDataTypeMap.region}}region: '{{parametersWithDataTypeMap.region.value}}',{{/parametersWithDataTypeMap.region}}{{/hasRegionalHost}}options: ClientOptions({{#gzipEncoding}}compression: 'gzip',{{/gzipEncoding}}{{#useEchoRequester}}requester: requester{{/useEchoRequester}}{{#hasCustomHosts}}hosts:[{{#customHosts}}Host.create(url: (Platform.environment['CI'] == 'true' ? 'localhost' : 'host.docker.internal') + ':{{port}}', scheme: 'http'),{{/customHosts}}]{{/hasCustomHosts}})); \ No newline at end of file +{{^autoCreateClient}}final client = {{/autoCreateClient}}{{client}}(appId : "{{parametersWithDataTypeMap.appId.value}}", apiKey : "{{parametersWithDataTypeMap.apiKey.value}}",{{#hasRegionalHost}}{{#parametersWithDataTypeMap.region}}region: '{{parametersWithDataTypeMap.region.value}}',{{/parametersWithDataTypeMap.region}}{{/hasRegionalHost}}options: ClientOptions({{#gzipEncoding}}compression: 'gzip',{{/gzipEncoding}}{{#useEchoRequester}}requester: requester{{/useEchoRequester}}{{#hasCustomHosts}}hosts:[{{#customHosts}}Host.create(url: (Platform.environment['CI'] == 'true' ? 'localhost' : 'host.docker.internal') + ':{{port}}', scheme: 'http'),{{/customHosts}}]{{/hasCustomHosts}})); +{{#hasTransformationOptions}} +client.setTransformationOptions(TransformationOptions(region: "{{{transformationRegion}}}"{{#hasTransformationCustomHosts}}, ingestionClientOptions: ClientOptions(hosts: [{{#transformationCustomHosts}}Host.create(url: (Platform.environment['CI'] == 'true' ? 'localhost' : 'host.docker.internal') + ':{{port}}', scheme: 'http'),{{/transformationCustomHosts}}]){{/hasTransformationCustomHosts}})); +{{/hasTransformationOptions}} \ No newline at end of file From 8fd0d02af347df9e2881951f067582d7600331f9 Mon Sep 17 00:00:00 2001 From: "mario.danmarioalexandrudan" Date: Tue, 12 May 2026 11:09:04 +0300 Subject: [PATCH 03/15] feat(dart): add gradle wrapper and update transformation options --- .../packages/client_search/lib/src/extension/transformation.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/clients/algoliasearch-client-dart/packages/client_search/lib/src/extension/transformation.dart b/clients/algoliasearch-client-dart/packages/client_search/lib/src/extension/transformation.dart index f05ff45fd14..6fddc32b428 100644 --- a/clients/algoliasearch-client-dart/packages/client_search/lib/src/extension/transformation.dart +++ b/clients/algoliasearch-client-dart/packages/client_search/lib/src/extension/transformation.dart @@ -1,4 +1,3 @@ -import 'dart:async'; import 'dart:math'; import 'package:algolia_client_core/algolia_client_core.dart'; From 44910b33ea21473567090d882d1e9204234e9fbe Mon Sep 17 00:00:00 2001 From: "mario.danmarioalexandrudan" Date: Tue, 12 May 2026 16:04:00 +0300 Subject: [PATCH 04/15] fix(dart): add algolia_client_ingestion dependency to client_search pubspec --- .../packages/client_search/pubspec.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/clients/algoliasearch-client-dart/packages/client_search/pubspec.yaml b/clients/algoliasearch-client-dart/packages/client_search/pubspec.yaml index d45b78449b4..ad09e0a0702 100644 --- a/clients/algoliasearch-client-dart/packages/client_search/pubspec.yaml +++ b/clients/algoliasearch-client-dart/packages/client_search/pubspec.yaml @@ -12,6 +12,7 @@ environment: dependencies: algolia_client_core: ^1.49.1 + algolia_client_ingestion: ^1.49.1 json_annotation: ^4.8.1 collection: ^1.17.1 From ee6ec15154e920da52ace216181885bd326b9985 Mon Sep 17 00:00:00 2001 From: "mario.danmarioalexandrudan" Date: Tue, 12 May 2026 16:51:18 +0300 Subject: [PATCH 05/15] fix(dart): add missing extension.dart to client_ingestion package --- .../packages/client_ingestion/lib/src/extension.dart | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 clients/algoliasearch-client-dart/packages/client_ingestion/lib/src/extension.dart diff --git a/clients/algoliasearch-client-dart/packages/client_ingestion/lib/src/extension.dart b/clients/algoliasearch-client-dart/packages/client_ingestion/lib/src/extension.dart new file mode 100644 index 00000000000..e69de29bb2d From 026e618a51ad89ef05164299cd9f67cf3f140ec8 Mon Sep 17 00:00:00 2001 From: "mario.danmarioalexandrudan" Date: Tue, 12 May 2026 17:14:55 +0300 Subject: [PATCH 06/15] fix(dart): add algolia_client_ingestion to test pubspec and fix trailing newline --- templates/dart/pubspec.tests.mustache | 3 ++- tests/output/dart/pubspec.yaml | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/templates/dart/pubspec.tests.mustache b/templates/dart/pubspec.tests.mustache index 419fb3820e5..89697695be3 100644 --- a/templates/dart/pubspec.tests.mustache +++ b/templates/dart/pubspec.tests.mustache @@ -18,6 +18,7 @@ dependencies: test_api: ^0.7.3 algolia_client_core: any + algolia_client_ingestion: any dev_dependencies: - lints: ^6.0.0 \ No newline at end of file + lints: ^6.0.0 diff --git a/tests/output/dart/pubspec.yaml b/tests/output/dart/pubspec.yaml index 910e241de52..91af6e62b50 100644 --- a/tests/output/dart/pubspec.yaml +++ b/tests/output/dart/pubspec.yaml @@ -18,6 +18,7 @@ dependencies: test_api: ^0.7.3 algolia_client_core: any + algolia_client_ingestion: any dev_dependencies: - lints: ^6.0.0 \ No newline at end of file + lints: ^6.0.0 From 3555bd99ec7bbdbd1a335946a55f802b04293b76 Mon Sep 17 00:00:00 2001 From: "mario.danmarioalexandrudan" Date: Tue, 12 May 2026 17:25:49 +0300 Subject: [PATCH 07/15] fix(dart): use Uri.parse instead of Uri.dataFromString for query parameter URLs --- .../client_core/lib/src/transport/dio/dio_requester.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clients/algoliasearch-client-dart/packages/client_core/lib/src/transport/dio/dio_requester.dart b/clients/algoliasearch-client-dart/packages/client_core/lib/src/transport/dio/dio_requester.dart index 36f69ba03e8..65882970a63 100644 --- a/clients/algoliasearch-client-dart/packages/client_core/lib/src/transport/dio/dio_requester.dart +++ b/clients/algoliasearch-client-dart/packages/client_core/lib/src/transport/dio/dio_requester.dart @@ -117,7 +117,7 @@ class DioRequester implements Requester { path: request.path, ); if (request.queryParameters.isNotEmpty) { - return Uri.dataFromString( + return Uri.parse( "${uri.toString()}?${request.queryParameters.entries.map((e) => "${e.key}=${e.value}").join("&")}"); } From 00436defcbec9440671294f6c326afa9bab99ee1 Mon Sep 17 00:00:00 2001 From: "mario.danmarioalexandrudan" Date: Tue, 12 May 2026 17:45:34 +0300 Subject: [PATCH 08/15] fix(dart): fix CTS test generation for ingestion - camelCase params, deduplicate additionalProperties, Platform alias --- .../codegen/cts/manager/DartCTSManager.java | 19 ++++++++++- templates/dart/tests/client/client.mustache | 2 +- .../dart/tests/client/createClient.mustache | 4 +-- templates/dart/tests/param_object.mustache | 3 ++ templates/dart/tests/request_param.mustache | 3 -- .../output/dart/test/client/search_test.dart | 32 +++++++++---------- 6 files changed, 40 insertions(+), 23 deletions(-) diff --git a/generators/src/main/java/com/algolia/codegen/cts/manager/DartCTSManager.java b/generators/src/main/java/com/algolia/codegen/cts/manager/DartCTSManager.java index 4a8f3e8a670..c2b8a974b6f 100644 --- a/generators/src/main/java/com/algolia/codegen/cts/manager/DartCTSManager.java +++ b/generators/src/main/java/com/algolia/codegen/cts/manager/DartCTSManager.java @@ -59,8 +59,25 @@ public void addMustacheLambdas(Map lambdas) { if (text.equals("external")) { writer.write("external_"); } else { - writer.write(text); + writer.write(snakeToCamel(text)); } }); } + + private static String snakeToCamel(String s) { + if (!s.contains("_")) return s; + StringBuilder sb = new StringBuilder(); + boolean nextUpper = false; + for (char c : s.toCharArray()) { + if (c == '_') { + nextUpper = true; + } else if (nextUpper) { + sb.append(Character.toUpperCase(c)); + nextUpper = false; + } else { + sb.append(c); + } + } + return sb.toString(); + } } diff --git a/templates/dart/tests/client/client.mustache b/templates/dart/tests/client/client.mustache index 310dae65d10..dccf2fb4ce0 100644 --- a/templates/dart/tests/client/client.mustache +++ b/templates/dart/tests/client/client.mustache @@ -3,7 +3,7 @@ import '{{{import}}}'; import 'package:algolia_test/algolia_test.dart'; import 'package:test/test.dart'; import 'package:test_api/hooks.dart'; -import 'dart:io' show Platform; +import 'dart:io' as io; void main() { {{#blocksClient}} diff --git a/templates/dart/tests/client/createClient.mustache b/templates/dart/tests/client/createClient.mustache index c99e5cc79dd..1e5f0a242df 100644 --- a/templates/dart/tests/client/createClient.mustache +++ b/templates/dart/tests/client/createClient.mustache @@ -1,4 +1,4 @@ -{{^autoCreateClient}}final client = {{/autoCreateClient}}{{client}}(appId : "{{parametersWithDataTypeMap.appId.value}}", apiKey : "{{parametersWithDataTypeMap.apiKey.value}}",{{#hasRegionalHost}}{{#parametersWithDataTypeMap.region}}region: '{{parametersWithDataTypeMap.region.value}}',{{/parametersWithDataTypeMap.region}}{{/hasRegionalHost}}options: ClientOptions({{#gzipEncoding}}compression: 'gzip',{{/gzipEncoding}}{{#useEchoRequester}}requester: requester{{/useEchoRequester}}{{#hasCustomHosts}}hosts:[{{#customHosts}}Host.create(url: (Platform.environment['CI'] == 'true' ? 'localhost' : 'host.docker.internal') + ':{{port}}', scheme: 'http'),{{/customHosts}}]{{/hasCustomHosts}})); +{{^autoCreateClient}}final client = {{/autoCreateClient}}{{client}}(appId : "{{parametersWithDataTypeMap.appId.value}}", apiKey : "{{parametersWithDataTypeMap.apiKey.value}}",{{#hasRegionalHost}}{{#parametersWithDataTypeMap.region}}region: '{{parametersWithDataTypeMap.region.value}}',{{/parametersWithDataTypeMap.region}}{{/hasRegionalHost}}options: ClientOptions({{#gzipEncoding}}compression: 'gzip',{{/gzipEncoding}}{{#useEchoRequester}}requester: requester{{/useEchoRequester}}{{#hasCustomHosts}}hosts:[{{#customHosts}}Host.create(url: (io.Platform.environment['CI'] == 'true' ? 'localhost' : 'host.docker.internal') + ':{{port}}', scheme: 'http'),{{/customHosts}}]{{/hasCustomHosts}})); {{#hasTransformationOptions}} -client.setTransformationOptions(TransformationOptions(region: "{{{transformationRegion}}}"{{#hasTransformationCustomHosts}}, ingestionClientOptions: ClientOptions(hosts: [{{#transformationCustomHosts}}Host.create(url: (Platform.environment['CI'] == 'true' ? 'localhost' : 'host.docker.internal') + ':{{port}}', scheme: 'http'),{{/transformationCustomHosts}}]){{/hasTransformationCustomHosts}})); +client.setTransformationOptions(TransformationOptions(region: "{{{transformationRegion}}}"{{#hasTransformationCustomHosts}}, ingestionClientOptions: ClientOptions(hosts: [{{#transformationCustomHosts}}Host.create(url: (io.Platform.environment['CI'] == 'true' ? 'localhost' : 'host.docker.internal') + ':{{port}}', scheme: 'http'),{{/transformationCustomHosts}}]){{/hasTransformationCustomHosts}})); {{/hasTransformationOptions}} \ No newline at end of file diff --git a/templates/dart/tests/param_object.mustache b/templates/dart/tests/param_object.mustache index b01ef3bc9b2..d6699f6c0fc 100644 --- a/templates/dart/tests/param_object.mustache +++ b/templates/dart/tests/param_object.mustache @@ -2,4 +2,7 @@ {{#value}} {{> tests/request_param}} {{/value}} +{{#hasAdditionalProperties}} +additionalProperties: { {{#value}}{{#isAdditionalProperty}}'{{{key}}}': {{> tests/param_value}}, {{/isAdditionalProperty}}{{/value}} }, +{{/hasAdditionalProperties}} ) \ No newline at end of file diff --git a/templates/dart/tests/request_param.mustache b/templates/dart/tests/request_param.mustache index b27bf4cfb15..83118450721 100644 --- a/templates/dart/tests/request_param.mustache +++ b/templates/dart/tests/request_param.mustache @@ -1,6 +1,3 @@ {{^isAdditionalProperty}} {{#lambda.identifier}}{{{key}}}{{/lambda.identifier}} : {{> tests/param_value}}, -{{/isAdditionalProperty}} -{{#isAdditionalProperty}} -additionalProperties : { '{{{key}}}' : '{{{value}}}' }, {{/isAdditionalProperty}} \ No newline at end of file diff --git a/tests/output/dart/test/client/search_test.dart b/tests/output/dart/test/client/search_test.dart index 10849f23921..dab72d612d7 100644 --- a/tests/output/dart/test/client/search_test.dart +++ b/tests/output/dart/test/client/search_test.dart @@ -3,7 +3,7 @@ import 'package:algolia_client_search/algolia_client_search.dart'; import 'package:algolia_test/algolia_test.dart'; import 'package:test/test.dart'; import 'package:test_api/hooks.dart'; -import 'dart:io' show Platform; +import 'dart:io' as io; void main() { test('calls api with correct read host', () async { @@ -68,15 +68,15 @@ void main() { options: ClientOptions(hosts: [ Host.create( url: - '${Platform.environment['CI'] == 'true' ? 'localhost' : 'host.docker.internal'}:6676', + '${io.Platform.environment['CI'] == 'true' ? 'localhost' : 'host.docker.internal'}:6676', scheme: 'http'), Host.create( url: - '${Platform.environment['CI'] == 'true' ? 'localhost' : 'host.docker.internal'}:6677', + '${io.Platform.environment['CI'] == 'true' ? 'localhost' : 'host.docker.internal'}:6677', scheme: 'http'), Host.create( url: - '${Platform.environment['CI'] == 'true' ? 'localhost' : 'host.docker.internal'}:6678', + '${io.Platform.environment['CI'] == 'true' ? 'localhost' : 'host.docker.internal'}:6678', scheme: 'http'), ])); requester.setOnRequest((request) {}); @@ -98,7 +98,7 @@ void main() { options: ClientOptions(hosts: [ Host.create( url: - '${Platform.environment['CI'] == 'true' ? 'localhost' : 'host.docker.internal'}:6676', + '${io.Platform.environment['CI'] == 'true' ? 'localhost' : 'host.docker.internal'}:6676', scheme: 'http'), ])); await expectError( @@ -123,15 +123,15 @@ void main() { options: ClientOptions(hosts: [ Host.create( url: - '${Platform.environment['CI'] == 'true' ? 'localhost' : 'host.docker.internal'}:6671', + '${io.Platform.environment['CI'] == 'true' ? 'localhost' : 'host.docker.internal'}:6671', scheme: 'http'), Host.create( url: - '${Platform.environment['CI'] == 'true' ? 'localhost' : 'host.docker.internal'}:6672', + '${io.Platform.environment['CI'] == 'true' ? 'localhost' : 'host.docker.internal'}:6672', scheme: 'http'), Host.create( url: - '${Platform.environment['CI'] == 'true' ? 'localhost' : 'host.docker.internal'}:6673', + '${io.Platform.environment['CI'] == 'true' ? 'localhost' : 'host.docker.internal'}:6673', scheme: 'http'), ])); requester.setOnRequest((request) {}); @@ -153,7 +153,7 @@ void main() { options: ClientOptions(compression: 'gzip', hosts: [ Host.create( url: - '${Platform.environment['CI'] == 'true' ? 'localhost' : 'host.docker.internal'}:6678', + '${io.Platform.environment['CI'] == 'true' ? 'localhost' : 'host.docker.internal'}:6678', scheme: 'http'), ])); requester.setOnRequest((request) {}); @@ -181,7 +181,7 @@ void main() { options: ClientOptions(hosts: [ Host.create( url: - '${Platform.environment['CI'] == 'true' ? 'localhost' : 'host.docker.internal'}:6691', + '${io.Platform.environment['CI'] == 'true' ? 'localhost' : 'host.docker.internal'}:6691', scheme: 'http'), ])); requester.setOnRequest((request) {}); @@ -242,7 +242,7 @@ void main() { options: ClientOptions(hosts: [ Host.create( url: - '${Platform.environment['CI'] == 'true' ? 'localhost' : 'host.docker.internal'}:6686', + '${io.Platform.environment['CI'] == 'true' ? 'localhost' : 'host.docker.internal'}:6686', scheme: 'http'), ])); requester.setOnRequest((request) {}); @@ -265,7 +265,7 @@ void main() { options: ClientOptions(hosts: [ Host.create( url: - '${Platform.environment['CI'] == 'true' ? 'localhost' : 'host.docker.internal'}:6686', + '${io.Platform.environment['CI'] == 'true' ? 'localhost' : 'host.docker.internal'}:6686', scheme: 'http'), ])); requester.setOnRequest((request) {}); @@ -289,11 +289,11 @@ void main() { options: ClientOptions(hosts: [ Host.create( url: - '${Platform.environment['CI'] == 'true' ? 'localhost' : 'host.docker.internal'}:6675', + '${io.Platform.environment['CI'] == 'true' ? 'localhost' : 'host.docker.internal'}:6675', scheme: 'http'), Host.create( url: - '${Platform.environment['CI'] == 'true' ? 'localhost' : 'host.docker.internal'}:6674', + '${io.Platform.environment['CI'] == 'true' ? 'localhost' : 'host.docker.internal'}:6674', scheme: 'http'), ])); { @@ -469,7 +469,7 @@ void main() { options: ClientOptions(hosts: [ Host.create( url: - '${Platform.environment['CI'] == 'true' ? 'localhost' : 'host.docker.internal'}:6686', + '${io.Platform.environment['CI'] == 'true' ? 'localhost' : 'host.docker.internal'}:6686', scheme: 'http'), ])); try { @@ -492,7 +492,7 @@ void main() { options: ClientOptions(hosts: [ Host.create( url: - '${Platform.environment['CI'] == 'true' ? 'localhost' : 'host.docker.internal'}:6683', + '${io.Platform.environment['CI'] == 'true' ? 'localhost' : 'host.docker.internal'}:6683', scheme: 'http'), ])); { From 051d01289c0fc6e08d2d1cf929c63482e261cc98 Mon Sep 17 00:00:00 2001 From: "mario.danmarioalexandrudan" Date: Tue, 12 May 2026 18:08:23 +0300 Subject: [PATCH 09/15] fix(dart): fix ingestion client default timeouts, AlgoliaApiException handling, and requestOptions timeouts in tests --- templates/dart/api.mustache | 6 +++++- templates/dart/tests/client/method.mustache | 1 + tests/CTS/client/ingestion/api.json | 3 ++- tests/output/dart/lib/src/expect.dart | 3 +++ 4 files changed, 11 insertions(+), 2 deletions(-) diff --git a/templates/dart/api.mustache b/templates/dart/api.mustache index 7aebbaea382..b7fe76ffec8 100644 --- a/templates/dart/api.mustache +++ b/templates/dart/api.mustache @@ -34,7 +34,11 @@ final class {{classname}} implements ApiClient { {{classname}}({ required String appId, required String apiKey, - this.options = const ClientOptions(), + this.options = const ClientOptions( + connectTimeout: Duration(milliseconds: {{#x-timeouts}}{{#server}}{{connect}}{{/server}}{{/x-timeouts}}), + readTimeout: Duration(milliseconds: {{#x-timeouts}}{{#server}}{{read}}{{/server}}{{/x-timeouts}}), + writeTimeout: Duration(milliseconds: {{#x-timeouts}}{{#server}}{{write}}{{/server}}{{/x-timeouts}}), + ), {{#hasRegionalHost}} {{^fallbackToAliasHost}}required{{/fallbackToAliasHost}} this.region, {{/hasRegionalHost}} diff --git a/templates/dart/tests/client/method.mustache b/templates/dart/tests/client/method.mustache index 23e87357c92..6cbc696f7fe 100644 --- a/templates/dart/tests/client/method.mustache +++ b/templates/dart/tests/client/method.mustache @@ -3,6 +3,7 @@ try { {{#parametersWithDataType}} {{> tests/request_param}} {{/parametersWithDataType}} + {{#hasRequestOptions}}requestOptions: RequestOptions({{#requestOptions.timeouts}}{{#write}}writeTimeout: Duration(milliseconds: {{{.}}}),{{/write}}{{#read}}readTimeout: Duration(milliseconds: {{{.}}}),{{/read}}{{#connect}}connectTimeout: Duration(milliseconds: {{{.}}}),{{/connect}}{{/requestOptions.timeouts}}),{{/hasRequestOptions}} ); {{#testResponse}} {{^match.isPrimitive}} diff --git a/tests/CTS/client/ingestion/api.json b/tests/CTS/client/ingestion/api.json index b5c9f063cf7..1d756400fd7 100644 --- a/tests/CTS/client/ingestion/api.json +++ b/tests/CTS/client/ingestion/api.json @@ -33,7 +33,8 @@ "python": "Too Many Requests", "ruby": "429: Too Many Requests", "scala": "429 Too Many Requests", - "swift": "HTTP error: Status code: 429 Message: No message" + "swift": "HTTP error: Status code: 429 Message: No message", + "dart": "429" } } } diff --git a/tests/output/dart/lib/src/expect.dart b/tests/output/dart/lib/src/expect.dart index ca0a7016ba6..f73c0b89932 100644 --- a/tests/output/dart/lib/src/expect.dart +++ b/tests/output/dart/lib/src/expect.dart @@ -72,6 +72,9 @@ Future expectError(String message, Function block) async { } on UnreachableHostsException catch (e) { expect(e.toString(), message); return; + } on AlgoliaApiException catch (e) { + expect(e.toString(), contains(message)); + return; } assert(false); From dfa8bc0757236abfd5e1b931a8ef22bc6e1a8676 Mon Sep 17 00:00:00 2001 From: "mario.danmarioalexandrudan" Date: Wed, 13 May 2026 11:03:27 +0300 Subject: [PATCH 10/15] fix(dart): use flat serverTimeouts map for default ClientOptions to fix Mustache traversal --- .../src/main/java/com/algolia/codegen/utils/Timeouts.java | 7 +++++++ templates/dart/api.mustache | 6 +++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/generators/src/main/java/com/algolia/codegen/utils/Timeouts.java b/generators/src/main/java/com/algolia/codegen/utils/Timeouts.java index 2b09bd2fb1a..613b9bd268c 100644 --- a/generators/src/main/java/com/algolia/codegen/utils/Timeouts.java +++ b/generators/src/main/java/com/algolia/codegen/utils/Timeouts.java @@ -46,5 +46,12 @@ public static void enrichBundle(OpenAPI spec, Map bundle) throws } bundle.put("x-timeouts", specTimeouts); + + // Flat server-timeout values for templates that can't traverse nested Java objects. + Map serverTimeouts = new HashMap<>(); + serverTimeouts.put("connect", specTimeouts.server.connect); + serverTimeouts.put("read", specTimeouts.server.read); + serverTimeouts.put("write", specTimeouts.server.write); + bundle.put("serverTimeouts", serverTimeouts); } } diff --git a/templates/dart/api.mustache b/templates/dart/api.mustache index b7fe76ffec8..552c2728fc8 100644 --- a/templates/dart/api.mustache +++ b/templates/dart/api.mustache @@ -35,9 +35,9 @@ final class {{classname}} implements ApiClient { required String appId, required String apiKey, this.options = const ClientOptions( - connectTimeout: Duration(milliseconds: {{#x-timeouts}}{{#server}}{{connect}}{{/server}}{{/x-timeouts}}), - readTimeout: Duration(milliseconds: {{#x-timeouts}}{{#server}}{{read}}{{/server}}{{/x-timeouts}}), - writeTimeout: Duration(milliseconds: {{#x-timeouts}}{{#server}}{{write}}{{/server}}{{/x-timeouts}}), + connectTimeout: Duration(milliseconds: {{#serverTimeouts}}{{connect}}{{/serverTimeouts}}), + readTimeout: Duration(milliseconds: {{#serverTimeouts}}{{read}}{{/serverTimeouts}}), + writeTimeout: Duration(milliseconds: {{#serverTimeouts}}{{write}}{{/serverTimeouts}}), ), {{#hasRegionalHost}} {{^fallbackToAliasHost}}required{{/fallbackToAliasHost}} this.region, From 6756b21238e1a8d429f6e0ed64e73423eea5f7ac Mon Sep 17 00:00:00 2001 From: "mario.danmarioalexandrudan" Date: Wed, 13 May 2026 12:19:46 +0300 Subject: [PATCH 11/15] fix(dart): add scalar server timeout values to bundle for reliable Mustache rendering --- .../java/com/algolia/codegen/AlgoliaDartGenerator.java | 7 ++++++- .../main/java/com/algolia/codegen/utils/Timeouts.java | 10 ++++------ templates/dart/api.mustache | 6 +++--- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/generators/src/main/java/com/algolia/codegen/AlgoliaDartGenerator.java b/generators/src/main/java/com/algolia/codegen/AlgoliaDartGenerator.java index 737ad73f16f..171f9dca585 100644 --- a/generators/src/main/java/com/algolia/codegen/AlgoliaDartGenerator.java +++ b/generators/src/main/java/com/algolia/codegen/AlgoliaDartGenerator.java @@ -111,9 +111,14 @@ public void processOpts() { } @Override - public void processOpenAPI(OpenAPI openAPI) { + public void processOpenAPI(OpenAPI openAPI) throws RuntimeException { super.processOpenAPI(openAPI); Helpers.generateServers(super.fromServers(openAPI.getServers()), additionalProperties); + try { + Timeouts.enrichBundle(openAPI, additionalProperties); + } catch (Exception e) { + throw new RuntimeException(e); + } } @Override diff --git a/generators/src/main/java/com/algolia/codegen/utils/Timeouts.java b/generators/src/main/java/com/algolia/codegen/utils/Timeouts.java index 613b9bd268c..7e11638668a 100644 --- a/generators/src/main/java/com/algolia/codegen/utils/Timeouts.java +++ b/generators/src/main/java/com/algolia/codegen/utils/Timeouts.java @@ -47,11 +47,9 @@ public static void enrichBundle(OpenAPI spec, Map bundle) throws bundle.put("x-timeouts", specTimeouts); - // Flat server-timeout values for templates that can't traverse nested Java objects. - Map serverTimeouts = new HashMap<>(); - serverTimeouts.put("connect", specTimeouts.server.connect); - serverTimeouts.put("read", specTimeouts.server.read); - serverTimeouts.put("write", specTimeouts.server.write); - bundle.put("serverTimeouts", serverTimeouts); + // Flat scalar values for templates that cannot traverse nested Java objects via Mustache sections. + bundle.put("serverConnectTimeout", specTimeouts.server.connect); + bundle.put("serverReadTimeout", specTimeouts.server.read); + bundle.put("serverWriteTimeout", specTimeouts.server.write); } } diff --git a/templates/dart/api.mustache b/templates/dart/api.mustache index 552c2728fc8..3a0437ccf2e 100644 --- a/templates/dart/api.mustache +++ b/templates/dart/api.mustache @@ -35,9 +35,9 @@ final class {{classname}} implements ApiClient { required String appId, required String apiKey, this.options = const ClientOptions( - connectTimeout: Duration(milliseconds: {{#serverTimeouts}}{{connect}}{{/serverTimeouts}}), - readTimeout: Duration(milliseconds: {{#serverTimeouts}}{{read}}{{/serverTimeouts}}), - writeTimeout: Duration(milliseconds: {{#serverTimeouts}}{{write}}{{/serverTimeouts}}), + connectTimeout: Duration(milliseconds: {{{serverConnectTimeout}}}), + readTimeout: Duration(milliseconds: {{{serverReadTimeout}}}), + writeTimeout: Duration(milliseconds: {{{serverWriteTimeout}}}), ), {{#hasRegionalHost}} {{^fallbackToAliasHost}}required{{/fallbackToAliasHost}} this.region, From daae4907db034eb50d49a4e9ca78b3f2ca755283 Mon Sep 17 00:00:00 2001 From: "mario.danmarioalexandrudan" Date: Wed, 13 May 2026 13:04:10 +0300 Subject: [PATCH 12/15] fix(dart): simplify processOpenAPI - no try/catch needed since ConfigException is RuntimeException --- .../java/com/algolia/codegen/AlgoliaDartGenerator.java | 8 ++------ .../src/main/java/com/algolia/codegen/utils/Timeouts.java | 2 +- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/generators/src/main/java/com/algolia/codegen/AlgoliaDartGenerator.java b/generators/src/main/java/com/algolia/codegen/AlgoliaDartGenerator.java index 171f9dca585..9bc12a4fc4a 100644 --- a/generators/src/main/java/com/algolia/codegen/AlgoliaDartGenerator.java +++ b/generators/src/main/java/com/algolia/codegen/AlgoliaDartGenerator.java @@ -111,14 +111,10 @@ public void processOpts() { } @Override - public void processOpenAPI(OpenAPI openAPI) throws RuntimeException { + public void processOpenAPI(OpenAPI openAPI) { super.processOpenAPI(openAPI); Helpers.generateServers(super.fromServers(openAPI.getServers()), additionalProperties); - try { - Timeouts.enrichBundle(openAPI, additionalProperties); - } catch (Exception e) { - throw new RuntimeException(e); - } + Timeouts.enrichBundle(openAPI, additionalProperties); } @Override diff --git a/generators/src/main/java/com/algolia/codegen/utils/Timeouts.java b/generators/src/main/java/com/algolia/codegen/utils/Timeouts.java index 7e11638668a..11cebaae1d2 100644 --- a/generators/src/main/java/com/algolia/codegen/utils/Timeouts.java +++ b/generators/src/main/java/com/algolia/codegen/utils/Timeouts.java @@ -47,7 +47,7 @@ public static void enrichBundle(OpenAPI spec, Map bundle) throws bundle.put("x-timeouts", specTimeouts); - // Flat scalar values for templates that cannot traverse nested Java objects via Mustache sections. + // Flat scalars for templates that cannot traverse nested Java objects via Mustache. bundle.put("serverConnectTimeout", specTimeouts.server.connect); bundle.put("serverReadTimeout", specTimeouts.server.read); bundle.put("serverWriteTimeout", specTimeouts.server.write); From a05cf6f6e6c1d716827a109ad387fe892dd45edd Mon Sep 17 00:00:00 2001 From: "mario.danmarioalexandrudan" Date: Wed, 13 May 2026 13:48:50 +0300 Subject: [PATCH 13/15] fix(dart): apply spec server timeouts at request level for methods without endpoint-level timeouts --- templates/dart/api.mustache | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/templates/dart/api.mustache b/templates/dart/api.mustache index 3a0437ccf2e..7a280014af6 100644 --- a/templates/dart/api.mustache +++ b/templates/dart/api.mustache @@ -34,11 +34,7 @@ final class {{classname}} implements ApiClient { {{classname}}({ required String appId, required String apiKey, - this.options = const ClientOptions( - connectTimeout: Duration(milliseconds: {{{serverConnectTimeout}}}), - readTimeout: Duration(milliseconds: {{{serverReadTimeout}}}), - writeTimeout: Duration(milliseconds: {{{serverWriteTimeout}}}), - ), + this.options = const ClientOptions(), {{#hasRegionalHost}} {{^fallbackToAliasHost}}required{{/fallbackToAliasHost}} this.region, {{/hasRegionalHost}} @@ -225,6 +221,10 @@ final class {{classname}} implements ApiClient { writeTimeout: Duration(milliseconds: {{write}}), readTimeout: Duration(milliseconds: {{read}}), connectTimeout: Duration(milliseconds: {{connect}}), + ) + {{/vendorExtensions.x-timeouts}}{{^vendorExtensions.x-timeouts}}RequestOptions( + writeTimeout: Duration(milliseconds: {{{serverWriteTimeout}}}), + readTimeout: Duration(milliseconds: {{{serverReadTimeout}}}), + connectTimeout: Duration(milliseconds: {{{serverConnectTimeout}}}), ) + {{/vendorExtensions.x-timeouts}}requestOptions, ); {{#returnType}} From d2fa25b1d7f0f42cfaec329a40b275c987c6ab6c Mon Sep 17 00:00:00 2001 From: "mario.danmarioalexandrudan" Date: Wed, 13 May 2026 14:16:52 +0300 Subject: [PATCH 14/15] fix(dart): add dart to assertValidReplaceAllObjectsWithTransformation count in runCts --- scripts/cts/runCts.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/cts/runCts.ts b/scripts/cts/runCts.ts index 14dd786b6bb..7c41ed9d11d 100644 --- a/scripts/cts/runCts.ts +++ b/scripts/cts/runCts.ts @@ -184,7 +184,7 @@ export async function runCts( assertChunkWrapperValid(languages.length - skip('dart')); assertValidReplaceAllObjects(languages.length - skip('dart')); assertValidReplaceAllObjectsWithTransformation( - only('javascript') + only('go') + only('python') + only('java') + only('php') + only('csharp') + only('scala'), + only('javascript') + only('go') + only('python') + only('java') + only('php') + only('csharp') + only('scala') + only('dart'), ); assertValidAccountCopyIndex(only('javascript')); assertValidReplaceAllObjectsFailed(languages.length - skip('dart')); From 97ac91922f5be44c4df54be7e734765fc2f70eac Mon Sep 17 00:00:00 2001 From: "mario.danmarioalexandrudan" Date: Wed, 13 May 2026 14:26:32 +0300 Subject: [PATCH 15/15] fix(dart): add dart to assertPushMockValid count in runCts --- scripts/cts/runCts.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/cts/runCts.ts b/scripts/cts/runCts.ts index 7c41ed9d11d..2828e4df5bf 100644 --- a/scripts/cts/runCts.ts +++ b/scripts/cts/runCts.ts @@ -191,7 +191,7 @@ export async function runCts( assertValidReplaceAllObjectsScopes(languages.length - skip('dart')); assertValidWaitForApiKey(languages.length - skip('dart')); assertPushMockValid( - only('javascript') + only('go') + only('python') + only('java') + only('php') + only('csharp') + only('scala'), + only('javascript') + only('go') + only('python') + only('java') + only('php') + only('csharp') + only('scala') + only('dart'), ); } if (withBenchmarkServer) {