@_nazmiforreal/flutter-ota 0.1.11 → 0.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/bin/flutter-ota-linux-x64 +0 -0
- package/dart-src/packages/cli-tools/.dart_tool/package_config.json +46 -76
- package/dart-src/packages/cli-tools/.dart_tool/package_graph.json +140 -166
- package/dart-src/packages/cli-tools/README.md +1 -1
- package/dart-src/packages/cli-tools/lib/flutter_ota_kit_cli.dart +30 -17
- package/dart-src/packages/cli-tools/lib/src/backend.dart +30 -31
- package/dart-src/packages/cli-tools/lib/src/cli_base.dart +3 -1
- package/dart-src/packages/cli-tools/lib/src/commands/build.dart +44 -43
- package/dart-src/packages/cli-tools/lib/src/commands/bundle.dart +265 -239
- package/dart-src/packages/cli-tools/lib/src/commands/channel.dart +75 -66
- package/dart-src/packages/cli-tools/lib/src/commands/config_command.dart +39 -35
- package/dart-src/packages/cli-tools/lib/src/commands/console.dart +30 -27
- package/dart-src/packages/cli-tools/lib/src/commands/deploy.dart +88 -72
- package/dart-src/packages/cli-tools/lib/src/commands/doctor.dart +66 -29
- package/dart-src/packages/cli-tools/lib/src/commands/fingerprint.dart +10 -6
- package/dart-src/packages/cli-tools/lib/src/commands/init.dart +299 -190
- package/dart-src/packages/cli-tools/lib/src/commands/keys.dart +24 -23
- package/dart-src/packages/cli-tools/lib/src/commands/migrate.dart +111 -97
- package/dart-src/packages/cli-tools/lib/src/commands/pocketbase.dart +237 -0
- package/dart-src/packages/cli-tools/lib/src/commands/rollback.dart +43 -34
- package/dart-src/packages/cli-tools/lib/src/commands/storage.dart +57 -53
- package/dart-src/packages/cli-tools/lib/src/config.dart +227 -93
- package/dart-src/packages/cli-tools/lib/src/operations.dart +9 -8
- package/dart-src/packages/cli-tools/lib/src/pack.dart +26 -25
- package/dart-src/packages/cli-tools/lib/src/pocketbase/data_bootstrap.dart +39 -0
- package/dart-src/packages/cli-tools/lib/src/pocketbase/hooks/bundles.pb.js +56 -0
- package/dart-src/packages/cli-tools/lib/src/pocketbase/installer.dart +189 -0
- package/dart-src/packages/cli-tools/lib/src/pocketbase/process_manager.dart +123 -0
- package/dart-src/packages/cli-tools/lib/src/pocketbase/schema_installer.dart +237 -0
- package/dart-src/packages/cli-tools/lib/src/runner.dart +36 -15
- package/dart-src/packages/cli-tools/lib/src/sign.dart +13 -5
- package/dart-src/packages/cli-tools/lib/src/ui/ui.dart +10 -13
- package/dart-src/packages/cli-tools/lib/src/util.dart +3 -1
- package/dart-src/packages/cli-tools/pubspec.lock +69 -116
- package/dart-src/packages/cli-tools/pubspec.yaml +25 -7
- package/dart-src/packages/cli-tools/test/backends_test.dart +62 -32
- package/dart-src/packages/cli-tools/test/build_pack_test.dart +4 -4
- package/dart-src/packages/cli-tools/test/cli_test.dart +89 -42
- package/dart-src/packages/cli-tools/test/mocks/mock_aws_s3_client.dart +8 -13
- package/dart-src/packages/cli-tools/test/mocks/mock_d1_client.dart +23 -18
- package/dart-src/packages/cli-tools/test/mocks/mock_pocketbase_client.dart +197 -0
- package/dart-src/packages/cli-tools/test/mocks/mock_postgres_client.dart +70 -46
- package/dart-src/packages/cli-tools/test/pocketbase_installer_test.dart +72 -0
- package/dart-src/packages/cli-tools/test/pocketbase_schema_test.dart +107 -0
- package/dart-src/packages/cli-tools/test/pocketbase_smoke_test.dart +59 -0
- package/dart-src/packages/core/README.md +2 -2
- package/dart-src/packages/core/example/lib/main.dart +69 -0
- package/dart-src/packages/core/lib/flutter_ota_kit_core.dart +1 -1
- package/dart-src/packages/core/lib/src/app_update_info.dart +17 -15
- package/dart-src/packages/core/lib/src/bundle.dart +102 -94
- package/dart-src/packages/core/lib/src/bundle_artifacts.dart +1 -2
- package/dart-src/packages/core/lib/src/bundle_patch_artifact.dart +15 -16
- package/dart-src/packages/core/lib/src/changed_asset.dart +24 -22
- package/dart-src/packages/core/lib/src/get_bundles_args.dart +13 -13
- package/dart-src/packages/core/lib/src/metadata.dart +9 -7
- package/dart-src/packages/core/lib/src/patch_info.dart +8 -7
- package/dart-src/packages/core/lib/src/platform.dart +4 -3
- package/dart-src/packages/core/lib/src/rollout.dart +14 -11
- package/dart-src/packages/core/lib/src/semver_range.dart +49 -39
- package/dart-src/packages/core/lib/src/semver_version.dart +8 -7
- package/dart-src/packages/core/lib/src/status.dart +4 -3
- package/dart-src/packages/core/lib/src/update_bundle_params.dart +7 -7
- package/dart-src/packages/core/lib/src/uuid.dart +1 -2
- package/dart-src/packages/core/pubspec.yaml +6 -7
- package/dart-src/packages/core/test/rollout_test.dart +15 -8
- package/dart-src/packages/core/test/semver_test.dart +5 -2
- package/dart-src/packages/core/test/types_test.dart +26 -24
- package/dart-src/packages/core/test/uuid_test.dart +3 -1
- package/dart-src/packages/core/tool/dbg.dart +1 -0
- package/dart-src/plugins/aws/CHANGELOG.md +6 -0
- package/dart-src/plugins/aws/example/lib/main.dart +60 -0
- package/dart-src/plugins/aws/lib/flutter_ota_kit_aws.dart +5 -1
- package/dart-src/plugins/aws/lib/src/aws_cloudfront_client.dart +11 -16
- package/dart-src/plugins/aws/lib/src/aws_cloudfront_signer.dart +7 -2
- package/dart-src/plugins/aws/lib/src/aws_database.dart +14 -18
- package/dart-src/plugins/aws/lib/src/aws_s3_client.dart +23 -18
- package/dart-src/plugins/aws/lib/src/aws_ssm_client.dart +5 -4
- package/dart-src/plugins/aws/lib/src/aws_storage.dart +2 -3
- package/dart-src/plugins/aws/lib/src/aws_storage_profile.dart +10 -16
- package/dart-src/plugins/aws/lib/src/with_cloudfront_signed_url.dart +6 -9
- package/dart-src/plugins/aws/pubspec.yaml +15 -10
- package/dart-src/plugins/aws/test/aws_database_test.dart +51 -57
- package/dart-src/plugins/aws/test/mock_aws_s3_client.dart +8 -13
- package/dart-src/plugins/cloudflare/CHANGELOG.md +7 -0
- package/dart-src/plugins/cloudflare/example/lib/main.dart +62 -0
- package/dart-src/plugins/cloudflare/lib/flutter_ota_kit_cloudflare.dart +4 -2
- package/dart-src/plugins/cloudflare/lib/src/cloudflare_worker_database.dart +3 -3
- package/dart-src/plugins/cloudflare/lib/src/d1_build_where.dart +3 -2
- package/dart-src/plugins/cloudflare/lib/src/d1_bundle_mapper.dart +24 -27
- package/dart-src/plugins/cloudflare/lib/src/d1_database_plugin.dart +22 -30
- package/dart-src/plugins/cloudflare/lib/src/r2_config.dart +1 -1
- package/dart-src/plugins/cloudflare/lib/src/r2_s3_client.dart +21 -29
- package/dart-src/plugins/cloudflare/lib/src/r2_storage.dart +7 -7
- package/dart-src/plugins/cloudflare/lib/src/r2_storage_profile.dart +7 -15
- package/dart-src/plugins/cloudflare/pubspec.yaml +14 -9
- package/dart-src/plugins/cloudflare/test/cloudflare_worker_database_test.dart +46 -42
- package/dart-src/plugins/cloudflare/test/d1_database_test.dart +100 -90
- package/dart-src/plugins/cloudflare/test/mock_d1_client.dart +23 -18
- package/dart-src/plugins/cloudflare/test/mock_r2_client.dart +8 -13
- package/dart-src/plugins/cloudflare/test/r2_storage_test.dart +1 -4
- package/dart-src/plugins/plugin-core/CHANGELOG.md +16 -0
- package/dart-src/plugins/plugin-core/example/lib/main.dart +61 -0
- package/dart-src/plugins/plugin-core/lib/flutter_ota_kit_plugin_core.dart +3 -7
- package/dart-src/plugins/plugin-core/lib/src/asset_storage_layout.dart +5 -5
- package/dart-src/plugins/plugin-core/lib/src/bundle_helpers.dart +23 -0
- package/dart-src/plugins/plugin-core/lib/src/bundle_storage_layout.dart +16 -10
- package/dart-src/plugins/plugin-core/lib/src/bundle_unit_of_work.dart +31 -26
- package/dart-src/plugins/plugin-core/lib/src/calculate_pagination.dart +5 -1
- package/dart-src/plugins/plugin-core/lib/src/content_addressed_assets.dart +1 -1
- package/dart-src/plugins/plugin-core/lib/src/create_blob_database_plugin.dart +78 -71
- package/dart-src/plugins/plugin-core/lib/src/create_database_plugin.dart +56 -35
- package/dart-src/plugins/plugin-core/lib/src/create_storage_plugin.dart +10 -13
- package/dart-src/plugins/plugin-core/lib/src/paginate_bundles.dart +30 -17
- package/dart-src/plugins/plugin-core/lib/src/parse_storage_uri.dart +3 -1
- package/dart-src/plugins/plugin-core/lib/src/query_bundles.dart +5 -3
- package/dart-src/plugins/plugin-core/lib/src/request_update_bundle_state.dart +2 -4
- package/dart-src/plugins/plugin-core/lib/src/storage_helpers.dart +18 -0
- package/dart-src/plugins/plugin-core/lib/src/types.dart +7 -6
- package/dart-src/plugins/plugin-core/pubspec.yaml +12 -9
- package/dart-src/plugins/plugin-core/test/asset_storage_layout_test.dart +13 -9
- package/dart-src/plugins/plugin-core/test/bundle_unit_of_work_test.dart +28 -15
- package/dart-src/plugins/plugin-core/test/create_blob_database_plugin_test.dart +254 -122
- package/dart-src/plugins/plugin-core/test/create_database_plugin_test.dart +36 -32
- package/dart-src/plugins/plugin-core/test/create_storage_plugin_test.dart +18 -15
- package/dart-src/plugins/plugin-core/test/generate_min_bundle_id_test.dart +8 -3
- package/dart-src/plugins/plugin-core/test/get_update_info_test.dart +682 -693
- package/dart-src/plugins/plugin-core/test/helpers_test.dart +45 -21
- package/dart-src/plugins/plugin-core/test/query_bundles_test.dart +76 -13
- package/dart-src/plugins/plugin-core/test/uuidv7_test.dart +20 -9
- package/dart-src/plugins/pocketbase/CHANGELOG.md +8 -0
- package/dart-src/plugins/pocketbase/LICENSE +21 -0
- package/dart-src/plugins/pocketbase/README.md +54 -0
- package/dart-src/plugins/pocketbase/example/lib/main.dart +62 -0
- package/dart-src/plugins/pocketbase/lib/flutter_ota_kit_pocketbase.dart +28 -0
- package/dart-src/plugins/pocketbase/lib/src/pocketbase_bundle_mapper.dart +121 -0
- package/dart-src/plugins/pocketbase/lib/src/pocketbase_client.dart +349 -0
- package/dart-src/plugins/pocketbase/lib/src/pocketbase_config.dart +61 -0
- package/dart-src/plugins/pocketbase/lib/src/pocketbase_database.dart +278 -0
- package/dart-src/plugins/pocketbase/lib/src/pocketbase_storage.dart +255 -0
- package/dart-src/plugins/pocketbase/pubspec.yaml +26 -0
- package/dart-src/plugins/pocketbase/test/pb_uri_test.dart +12 -0
- package/dart-src/plugins/postgres/CHANGELOG.md +6 -0
- package/dart-src/plugins/postgres/example/lib/main.dart +67 -0
- package/dart-src/plugins/postgres/lib/flutter_ota_kit_postgres.dart +3 -6
- package/dart-src/plugins/postgres/lib/src/postgres_bundle_mapper.dart +32 -52
- package/dart-src/plugins/postgres/lib/src/postgres_config.dart +3 -1
- package/dart-src/plugins/postgres/lib/src/postgres_database.dart +28 -38
- package/dart-src/plugins/postgres/lib/src/postgres_get_update_info.dart +16 -14
- package/dart-src/plugins/postgres/lib/src/postgres_storage.dart +26 -14
- package/dart-src/plugins/postgres/lib/src/postgres_types.dart +27 -28
- package/dart-src/plugins/postgres/pubspec.yaml +14 -9
- package/dart-src/plugins/postgres/test/mock_postgres_client.dart +62 -39
- package/dart-src/plugins/postgres/test/postgres_bundle_mapper_test.dart +3 -4
- package/dart-src/plugins/postgres/test/postgres_database_test.dart +102 -73
- package/dart-src/plugins/postgres/test/postgres_storage_runtime_test.dart +60 -0
- package/dart-src/plugins/supabase/CHANGELOG.md +17 -0
- package/dart-src/plugins/supabase/README.md +2 -2
- package/dart-src/plugins/supabase/example/lib/main.dart +63 -0
- package/dart-src/plugins/supabase/lib/flutter_ota_kit_supabase.dart +2 -0
- package/dart-src/plugins/supabase/lib/src/error_message.dart +11 -0
- package/dart-src/plugins/supabase/lib/src/supabase_bundle_mapper.dart +34 -58
- package/dart-src/plugins/supabase/lib/src/supabase_client_adapter.dart +3 -13
- package/dart-src/plugins/supabase/lib/src/supabase_client_http.dart +34 -52
- package/dart-src/plugins/supabase/lib/src/supabase_database.dart +38 -30
- package/dart-src/plugins/supabase/lib/src/supabase_edge_function_storage.dart +21 -29
- package/dart-src/plugins/supabase/lib/src/supabase_signed_url_batcher.dart +7 -19
- package/dart-src/plugins/supabase/lib/src/supabase_storage.dart +18 -29
- package/dart-src/plugins/supabase/lib/src/types.dart +26 -26
- package/dart-src/plugins/supabase/pubspec.yaml +14 -9
- package/dart-src/plugins/supabase/test/mock_supabase_client.dart +57 -57
- package/dart-src/plugins/supabase/test/supabase_bundle_mapper_test.dart +30 -27
- package/dart-src/plugins/supabase/test/supabase_database_test.dart +26 -35
- package/dart-src/plugins/supabase/test/supabase_storage_test.dart +27 -34
- package/package.json +2 -2
- package/scripts/vendor.sh +1 -1
|
@@ -84,11 +84,7 @@ class AwsS3Client implements AwsS3ClientLike {
|
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
@override
|
|
87
|
-
Future<void> putObject(
|
|
88
|
-
String key,
|
|
89
|
-
List<int> body,
|
|
90
|
-
String contentType,
|
|
91
|
-
) async {
|
|
87
|
+
Future<void> putObject(String key, List<int> body, String contentType) async {
|
|
92
88
|
final response = await _send(
|
|
93
89
|
'PUT',
|
|
94
90
|
_objectUri(key),
|
|
@@ -104,8 +100,8 @@ class AwsS3Client implements AwsS3ClientLike {
|
|
|
104
100
|
@override
|
|
105
101
|
Future<bool> headObject(String key) async {
|
|
106
102
|
final uri = _objectUri(key);
|
|
107
|
-
final request =
|
|
108
|
-
|
|
103
|
+
final request = Request('HEAD', uri)
|
|
104
|
+
..headers.addAll(_authHeaders('HEAD', uri, null));
|
|
109
105
|
final response = await _http.send(request);
|
|
110
106
|
if (response.statusCode == 404) return false;
|
|
111
107
|
if (response.statusCode < 200 || response.statusCode >= 300) {
|
|
@@ -148,10 +144,11 @@ class AwsS3Client implements AwsS3ClientLike {
|
|
|
148
144
|
|
|
149
145
|
...tokenParam,
|
|
150
146
|
};
|
|
151
|
-
final canonicalQuery =
|
|
152
|
-
.
|
|
153
|
-
|
|
154
|
-
|
|
147
|
+
final canonicalQuery =
|
|
148
|
+
params.entries
|
|
149
|
+
.map((e) => '${_uriEncode(e.key)}=${_uriEncode(e.value)}')
|
|
150
|
+
.toList()
|
|
151
|
+
..sort();
|
|
155
152
|
final queryString = canonicalQuery.join('&');
|
|
156
153
|
|
|
157
154
|
const payloadHash = 'UNSIGNED-PAYLOAD';
|
|
@@ -188,7 +185,8 @@ class AwsS3Client implements AwsS3ClientLike {
|
|
|
188
185
|
final scope = '$dateStamp/${config.region}/$_service/aws4_request';
|
|
189
186
|
final payloadHash = body == null ? _hashHex(const []) : _hashHex(body);
|
|
190
187
|
|
|
191
|
-
final canonicalHeaders =
|
|
188
|
+
final canonicalHeaders =
|
|
189
|
+
'host:${uri.host}\n'
|
|
192
190
|
'x-amz-content-sha256:$payloadHash\n'
|
|
193
191
|
'x-amz-date:$amzDate\n'
|
|
194
192
|
'${config.sessionToken != null ? 'x-amz-security-token:${config.sessionToken}\n' : ''}';
|
|
@@ -196,7 +194,8 @@ class AwsS3Client implements AwsS3ClientLike {
|
|
|
196
194
|
? 'host;x-amz-content-sha256;x-amz-date;x-amz-security-token'
|
|
197
195
|
: 'host;x-amz-content-sha256;x-amz-date';
|
|
198
196
|
|
|
199
|
-
final canonicalRequest =
|
|
197
|
+
final canonicalRequest =
|
|
198
|
+
'$method\n${uri.path}\n'
|
|
200
199
|
'\n$canonicalHeaders$signedHeaders\n$payloadHash';
|
|
201
200
|
final stringToSign = _stringToSign(amzDate, scope, canonicalRequest);
|
|
202
201
|
final signingKey = _signingKey(dateStamp, config.region);
|
|
@@ -231,8 +230,10 @@ class AwsS3Client implements AwsS3ClientLike {
|
|
|
231
230
|
|
|
232
231
|
List<StorageObject> _parseListObjects(String xml) {
|
|
233
232
|
final objects = <StorageObject>[];
|
|
234
|
-
final contents =
|
|
235
|
-
|
|
233
|
+
final contents = _allMatches(
|
|
234
|
+
xml,
|
|
235
|
+
RegExp(r'<Contents>([\s\S]*?)</Contents>'),
|
|
236
|
+
);
|
|
236
237
|
for (final block in contents) {
|
|
237
238
|
final key = _tag(block, 'Key');
|
|
238
239
|
final size = int.tryParse(_tag(block, 'Size') ?? '');
|
|
@@ -273,8 +274,11 @@ class AwsS3Client implements AwsS3ClientLike {
|
|
|
273
274
|
|
|
274
275
|
static String _uriEncode(String value) => Uri.encodeQueryComponent(value);
|
|
275
276
|
|
|
276
|
-
static String _xmlEscape(String value) =>
|
|
277
|
-
|
|
277
|
+
static String _xmlEscape(String value) => value
|
|
278
|
+
.replaceAll('&', '&')
|
|
279
|
+
.replaceAll('<', '<')
|
|
280
|
+
.replaceAll('>', '>')
|
|
281
|
+
.replaceAll('"', '"');
|
|
278
282
|
|
|
279
283
|
static Uint8List _hmac(List<int> key, String data) =>
|
|
280
284
|
Hmac(sha256, key).convert(utf8.encode(data)).bytes as Uint8List;
|
|
@@ -302,7 +306,8 @@ class AwsS3Client implements AwsS3ClientLike {
|
|
|
302
306
|
}
|
|
303
307
|
|
|
304
308
|
/// Read a local file's bytes (node profile helper).
|
|
305
|
-
Future<List<int>> readFileBytes(String filePath) =>
|
|
309
|
+
Future<List<int>> readFileBytes(String filePath) =>
|
|
310
|
+
File(filePath).readAsBytes();
|
|
306
311
|
|
|
307
312
|
/// Write bytes to a local file, creating parent directories.
|
|
308
313
|
Future<void> writeFileBytes(String filePath, List<int> bytes) async {
|
|
@@ -55,7 +55,8 @@ class AwsSsmClient {
|
|
|
55
55
|
final scope = '$dateStamp/$region/$_service/aws4_request';
|
|
56
56
|
final payloadHash = _hashHex(body);
|
|
57
57
|
|
|
58
|
-
final canonicalHeaders =
|
|
58
|
+
final canonicalHeaders =
|
|
59
|
+
'host:${uri.host}\n'
|
|
59
60
|
'x-amz-content-sha256:$payloadHash\n'
|
|
60
61
|
'x-amz-date:$amzDate\n'
|
|
61
62
|
'${sessionToken != null ? 'x-amz-security-token:$sessionToken\n' : ''}'
|
|
@@ -64,7 +65,8 @@ class AwsSsmClient {
|
|
|
64
65
|
? 'host;x-amz-content-sha256;x-amz-date;x-amz-security-token'
|
|
65
66
|
: 'host;x-amz-content-sha256;x-amz-date';
|
|
66
67
|
|
|
67
|
-
final canonicalRequest =
|
|
68
|
+
final canonicalRequest =
|
|
69
|
+
'$method\n${uri.path}\n'
|
|
68
70
|
'\n$canonicalHeaders$signedHeaders\n$payloadHash';
|
|
69
71
|
final stringToSign = _stringToSign(amzDate, scope, canonicalRequest);
|
|
70
72
|
final signingKey = _signingKey(dateStamp);
|
|
@@ -109,8 +111,7 @@ class AwsSsmClient {
|
|
|
109
111
|
static Uint8List _hmac(List<int> key, String data) =>
|
|
110
112
|
Hmac(sha256, key).convert(utf8.encode(data)).bytes as Uint8List;
|
|
111
113
|
|
|
112
|
-
static String _hashHex(List<int> data) =>
|
|
113
|
-
_toHex(sha256.convert(data).bytes);
|
|
114
|
+
static String _hashHex(List<int> data) => _toHex(sha256.convert(data).bytes);
|
|
114
115
|
|
|
115
116
|
static String _toHex(List<int> bytes) =>
|
|
116
117
|
bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
|
|
@@ -2,9 +2,8 @@ import 'package:flutter_ota_kit_plugin_core/flutter_ota_kit_plugin_core.dart'
|
|
|
2
2
|
show createUniversalStoragePlugin;
|
|
3
3
|
|
|
4
4
|
import 'aws_config.dart' show AwsS3StorageConfig;
|
|
5
|
-
import 'aws_storage_profile.dart'
|
|
6
|
-
createS3RuntimeStorageProfile,
|
|
7
|
-
createS3StorageProfile;
|
|
5
|
+
import 'aws_storage_profile.dart'
|
|
6
|
+
show createS3RuntimeStorageProfile, createS3StorageProfile;
|
|
8
7
|
|
|
9
8
|
/// AWS S3 storage plugin (faithful port of hot-updater `s3Storage.ts`).
|
|
10
9
|
///
|
|
@@ -6,20 +6,13 @@ import 'package:flutter_ota_kit_plugin_core/flutter_ota_kit_plugin_core.dart'
|
|
|
6
6
|
RuntimeStorageProfile,
|
|
7
7
|
StorageObject,
|
|
8
8
|
createStorageKeyBuilder,
|
|
9
|
+
ensureExpectedBucket,
|
|
9
10
|
getContentType,
|
|
10
11
|
parseStorageUri;
|
|
11
12
|
|
|
12
13
|
import 'aws_config.dart' show AwsS3StorageConfig, resolveAwsS3Client;
|
|
13
14
|
import 'aws_s3_client.dart' show AwsS3ClientLike, readFileBytes, writeFileBytes;
|
|
14
15
|
|
|
15
|
-
void _ensureExpectedBucket(String bucket, String bucketName) {
|
|
16
|
-
if (bucket != bucketName) {
|
|
17
|
-
throw ArgumentError(
|
|
18
|
-
'Bucket name mismatch: expected "$bucketName", but found "$bucket".',
|
|
19
|
-
);
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
16
|
/// Build the node (deploy/CLI) storage profile for AWS S3.
|
|
24
17
|
NodeStorageProfile createS3StorageProfile(AwsS3StorageConfig config) {
|
|
25
18
|
final AwsS3ClientLike client = resolveAwsS3Client(config);
|
|
@@ -43,9 +36,10 @@ class _S3NodeProfile implements NodeStorageProfile {
|
|
|
43
36
|
|
|
44
37
|
String _getListPrefix(String prefix) {
|
|
45
38
|
final normalizedPrefix = prefix.replaceAll(RegExp(r'^/+|/+$'), '');
|
|
46
|
-
final value = [
|
|
47
|
-
|
|
48
|
-
|
|
39
|
+
final value = [
|
|
40
|
+
_config.basePath ?? '',
|
|
41
|
+
normalizedPrefix,
|
|
42
|
+
].where((s) => s.isNotEmpty).join('/');
|
|
49
43
|
return value.isNotEmpty ? '$value/' : '';
|
|
50
44
|
}
|
|
51
45
|
|
|
@@ -63,14 +57,14 @@ class _S3NodeProfile implements NodeStorageProfile {
|
|
|
63
57
|
@override
|
|
64
58
|
Future<bool> exists(String storageUri) async {
|
|
65
59
|
final parsed = parseStorageUri(storageUri, 's3');
|
|
66
|
-
|
|
60
|
+
ensureExpectedBucket(parsed.bucket, _config.bucketName);
|
|
67
61
|
return _client.headObject(parsed.key);
|
|
68
62
|
}
|
|
69
63
|
|
|
70
64
|
@override
|
|
71
65
|
Future<void> delete(String storageUri) async {
|
|
72
66
|
final parsed = parseStorageUri(storageUri, 's3');
|
|
73
|
-
|
|
67
|
+
ensureExpectedBucket(parsed.bucket, _config.bucketName);
|
|
74
68
|
|
|
75
69
|
final list = await _client.listObjects(parsed.key);
|
|
76
70
|
if (list.isEmpty) {
|
|
@@ -82,7 +76,7 @@ class _S3NodeProfile implements NodeStorageProfile {
|
|
|
82
76
|
@override
|
|
83
77
|
Future<void> downloadFile(String storageUri, String filePath) async {
|
|
84
78
|
final parsed = parseStorageUri(storageUri, 's3');
|
|
85
|
-
|
|
79
|
+
ensureExpectedBucket(parsed.bucket, _config.bucketName);
|
|
86
80
|
final bytes = await _client.getObjectAsBytes(parsed.key);
|
|
87
81
|
await writeFileBytes(filePath, bytes);
|
|
88
82
|
}
|
|
@@ -127,7 +121,7 @@ class _S3RuntimeProfile implements RuntimeStorageProfile {
|
|
|
127
121
|
@override
|
|
128
122
|
Future<Map<String, String>> getDownloadUrl(String storageUri) async {
|
|
129
123
|
final parsed = parseStorageUri(storageUri, 's3');
|
|
130
|
-
|
|
124
|
+
ensureExpectedBucket(parsed.bucket, _config.bucketName);
|
|
131
125
|
final fileUrl = await _client.getPresignedUrl(parsed.key);
|
|
132
126
|
return {'fileUrl': fileUrl};
|
|
133
127
|
}
|
|
@@ -135,7 +129,7 @@ class _S3RuntimeProfile implements RuntimeStorageProfile {
|
|
|
135
129
|
@override
|
|
136
130
|
Future<String?> readText(String storageUri) async {
|
|
137
131
|
final parsed = parseStorageUri(storageUri, 's3');
|
|
138
|
-
|
|
132
|
+
ensureExpectedBucket(parsed.bucket, _config.bucketName);
|
|
139
133
|
try {
|
|
140
134
|
return await _client.getObjectAsString(parsed.key);
|
|
141
135
|
} catch (_) {
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import 'package:flutter_ota_kit_plugin_core/flutter_ota_kit_plugin_core.dart'
|
|
2
|
-
show
|
|
3
|
-
RuntimeStorageProfile,
|
|
4
|
-
StoragePlugin,
|
|
5
|
-
StoragePluginProfiles;
|
|
2
|
+
show RuntimeStorageProfile, StoragePlugin, StoragePluginProfiles;
|
|
6
3
|
|
|
7
4
|
import 'aws_cloudfront_signer.dart' show cloudfrontSignedUrl;
|
|
8
5
|
import 'aws_ssm_client.dart' show AwsSsmClient;
|
|
@@ -67,10 +64,8 @@ class _CloudFrontSignedStoragePlugin implements StoragePlugin {
|
|
|
67
64
|
String get supportedProtocol => _base.supportedProtocol;
|
|
68
65
|
|
|
69
66
|
@override
|
|
70
|
-
StoragePluginProfiles get profiles =>
|
|
71
|
-
|
|
72
|
-
runtime: _runtime,
|
|
73
|
-
);
|
|
67
|
+
StoragePluginProfiles get profiles =>
|
|
68
|
+
StoragePluginProfiles(node: _base.profiles.node, runtime: _runtime);
|
|
74
69
|
}
|
|
75
70
|
|
|
76
71
|
class _CloudFrontSignedRuntimeProfile implements RuntimeStorageProfile {
|
|
@@ -124,7 +119,9 @@ class _CloudFrontSignedRuntimeProfile implements RuntimeStorageProfile {
|
|
|
124
119
|
if (_config.publicBaseUrlResolver != null) {
|
|
125
120
|
final value = await _config.publicBaseUrlResolver!();
|
|
126
121
|
if (value.isEmpty) {
|
|
127
|
-
throw StateError(
|
|
122
|
+
throw StateError(
|
|
123
|
+
'CloudFront publicBaseUrl resolver returned empty URL',
|
|
124
|
+
);
|
|
128
125
|
}
|
|
129
126
|
return value;
|
|
130
127
|
}
|
|
@@ -1,25 +1,30 @@
|
|
|
1
1
|
name: flutter_ota_kit_aws
|
|
2
2
|
description: AWS S3 storage + S3/DynamoDB database plugins for flutter_ota_kit (faithful port of hot-updater plugins/aws).
|
|
3
|
-
version: 0.1.
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
documentation: https://github.com/HYPER12755/flutter_ota_kit#readme
|
|
3
|
+
version: 0.1.2
|
|
4
|
+
homepage: https://github.com/HYPER12755/flutter-ota-kit
|
|
5
|
+
repository: https://github.com/HYPER12755/flutter-ota-kit
|
|
6
|
+
issue_tracker: https://github.com/HYPER12755/flutter-ota-kit/issues
|
|
7
|
+
documentation: https://github.com/HYPER12755/flutter-ota-kit#readme
|
|
9
8
|
license: MIT
|
|
10
9
|
|
|
11
10
|
environment:
|
|
12
|
-
sdk: ">=3.
|
|
11
|
+
sdk: ">=3.13.2 <4.0.0"
|
|
13
12
|
|
|
14
13
|
dependencies:
|
|
15
|
-
flutter_ota_kit_core: ^0.1.
|
|
16
|
-
flutter_ota_kit_plugin_core: ^0.0.
|
|
14
|
+
flutter_ota_kit_core: ^0.1.1
|
|
15
|
+
flutter_ota_kit_plugin_core: ^0.0.3
|
|
17
16
|
crypto: ^3.0.0
|
|
18
17
|
http: ^1.2.0
|
|
19
18
|
path: ^1.9.0
|
|
20
|
-
pointycastle: ^
|
|
19
|
+
pointycastle: ^4.0.0
|
|
21
20
|
asn1lib: ^1.5.4
|
|
22
21
|
|
|
23
22
|
dev_dependencies:
|
|
24
23
|
test: ^1.25.0
|
|
25
24
|
lints: ^4.0.0
|
|
25
|
+
|
|
26
|
+
dependency_overrides:
|
|
27
|
+
flutter_ota_kit_core:
|
|
28
|
+
path: ../../packages/core
|
|
29
|
+
flutter_ota_kit_plugin_core:
|
|
30
|
+
path: ../plugin-core
|
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
import 'package:flutter_ota_kit_core/flutter_ota_kit_core.dart'
|
|
2
|
-
show
|
|
3
|
-
AppVersionGetBundlesArgs,
|
|
4
|
-
Bundle,
|
|
5
|
-
Platform,
|
|
6
|
-
nilUuid;
|
|
2
|
+
show AppVersionGetBundlesArgs, Bundle, Platform, nilUuid;
|
|
7
3
|
import 'package:flutter_ota_kit_plugin_core/flutter_ota_kit_plugin_core.dart'
|
|
8
4
|
show DatabaseBundleQueryOptions, DatabasePlugin;
|
|
9
5
|
import 'package:flutter_ota_kit_aws/flutter_ota_kit_aws.dart'
|
|
@@ -33,20 +29,19 @@ Bundle _makeBundle({
|
|
|
33
29
|
String channel = 'production',
|
|
34
30
|
String? targetAppVersion,
|
|
35
31
|
String? fingerprintHash,
|
|
36
|
-
}) =>
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
);
|
|
32
|
+
}) => Bundle(
|
|
33
|
+
id: id,
|
|
34
|
+
channel: channel,
|
|
35
|
+
enabled: true,
|
|
36
|
+
shouldForceUpdate: false,
|
|
37
|
+
fileHash: 'h-$id',
|
|
38
|
+
platform: Platform.android,
|
|
39
|
+
targetAppVersion: targetAppVersion,
|
|
40
|
+
storageUri: 's3://test-bucket/$id',
|
|
41
|
+
fingerprintHash: fingerprintHash,
|
|
42
|
+
message: 'msg-$id',
|
|
43
|
+
patches: null,
|
|
44
|
+
);
|
|
50
45
|
|
|
51
46
|
void main() {
|
|
52
47
|
group('aws s3Database', () {
|
|
@@ -57,46 +52,46 @@ void main() {
|
|
|
57
52
|
S3DatabaseConfig config({
|
|
58
53
|
required AwsS3ClientLike Function(S3DatabaseConfig) clientFactory,
|
|
59
54
|
String? cloudfrontDistributionId,
|
|
60
|
-
}) =>
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
);
|
|
55
|
+
}) => S3DatabaseConfig(
|
|
56
|
+
bucketName: 'test-bucket',
|
|
57
|
+
region: 'us-east-1',
|
|
58
|
+
accessKeyId: 'ak',
|
|
59
|
+
secretAccessKey: 'sk',
|
|
60
|
+
clientFactory: clientFactory,
|
|
61
|
+
cloudfrontDistributionId: cloudfrontDistributionId,
|
|
62
|
+
cloudfrontClientFactory: (_) => cf,
|
|
63
|
+
);
|
|
70
64
|
|
|
71
65
|
setUp(() {
|
|
72
66
|
store = Store();
|
|
73
67
|
cf = MockCloudFrontClient();
|
|
74
|
-
plugin = s3Database(
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
68
|
+
plugin = s3Database(
|
|
69
|
+
config(
|
|
70
|
+
clientFactory: (_) => MockAwsS3Client(store),
|
|
71
|
+
cloudfrontDistributionId: 'DIST123',
|
|
72
|
+
),
|
|
73
|
+
)();
|
|
78
74
|
});
|
|
79
75
|
|
|
80
76
|
test('plugin metadata', () {
|
|
81
77
|
expect(plugin.name, 's3Database');
|
|
82
78
|
});
|
|
83
79
|
|
|
84
|
-
test(
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
80
|
+
test(
|
|
81
|
+
'commitBundle writes update.json and invalidates CloudFront',
|
|
82
|
+
() async {
|
|
83
|
+
await plugin.appendBundle(
|
|
84
|
+
_makeBundle(id: 'b1', targetAppVersion: '1.0.0'),
|
|
85
|
+
);
|
|
86
|
+
await plugin.commitBundle();
|
|
90
87
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
keys,
|
|
94
|
-
contains('production/android/1.0.0/update.json'),
|
|
95
|
-
);
|
|
88
|
+
final keys = store.objects.keys.toList();
|
|
89
|
+
expect(keys, contains('production/android/1.0.0/update.json'));
|
|
96
90
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
91
|
+
expect(cf.invalidations, isNotEmpty);
|
|
92
|
+
expect(cf.invalidations.first.$1, 'DIST123');
|
|
93
|
+
},
|
|
94
|
+
);
|
|
100
95
|
|
|
101
96
|
test('getUpdateInfo (appVersion) resolves committed bundle', () async {
|
|
102
97
|
await plugin.appendBundle(
|
|
@@ -135,14 +130,15 @@ void main() {
|
|
|
135
130
|
_makeBundle(id: 'b1', targetAppVersion: '1.0.0'),
|
|
136
131
|
);
|
|
137
132
|
await plugin.commitBundle();
|
|
138
|
-
expect(
|
|
133
|
+
expect(
|
|
134
|
+
store.objects.keys,
|
|
135
|
+
contains('production/android/1.0.0/update.json'),
|
|
136
|
+
);
|
|
139
137
|
|
|
140
138
|
cf.invalidations.clear();
|
|
141
139
|
final bundle = (await plugin.getBundles(
|
|
142
140
|
const DatabaseBundleQueryOptions(limit: 100, offset: 0),
|
|
143
|
-
))
|
|
144
|
-
.data
|
|
145
|
-
.first;
|
|
141
|
+
)).data.first;
|
|
146
142
|
await plugin.deleteBundle(bundle);
|
|
147
143
|
await plugin.commitBundle();
|
|
148
144
|
|
|
@@ -154,12 +150,10 @@ void main() {
|
|
|
154
150
|
});
|
|
155
151
|
|
|
156
152
|
test('no CloudFront invalidation when distribution id omitted', () async {
|
|
157
|
-
final noCf = s3Database(
|
|
158
|
-
clientFactory: (_) => MockAwsS3Client(store),
|
|
159
|
-
)
|
|
160
|
-
await noCf.appendBundle(
|
|
161
|
-
_makeBundle(id: 'b1', targetAppVersion: '1.0.0'),
|
|
162
|
-
);
|
|
153
|
+
final noCf = s3Database(
|
|
154
|
+
config(clientFactory: (_) => MockAwsS3Client(store)),
|
|
155
|
+
)();
|
|
156
|
+
await noCf.appendBundle(_makeBundle(id: 'b1', targetAppVersion: '1.0.0'));
|
|
163
157
|
await noCf.commitBundle();
|
|
164
158
|
expect(cf.invalidations, isEmpty);
|
|
165
159
|
});
|
|
@@ -29,8 +29,7 @@ class MockAwsS3Client implements AwsS3ClientLike {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
@override
|
|
32
|
-
Future<bool> headObject(String key) async =>
|
|
33
|
-
store.objects.containsKey(key);
|
|
32
|
+
Future<bool> headObject(String key) async => store.objects.containsKey(key);
|
|
34
33
|
|
|
35
34
|
@override
|
|
36
35
|
Future<List<int>> getObjectAsBytes(String key) async {
|
|
@@ -64,20 +63,16 @@ class MockAwsS3Client implements AwsS3ClientLike {
|
|
|
64
63
|
}
|
|
65
64
|
|
|
66
65
|
@override
|
|
67
|
-
Future<void> putObject(
|
|
68
|
-
String key,
|
|
69
|
-
List<int> body,
|
|
70
|
-
String contentType,
|
|
71
|
-
) async {
|
|
66
|
+
Future<void> putObject(String key, List<int> body, String contentType) async {
|
|
72
67
|
store.objects[key] = body;
|
|
73
68
|
}
|
|
74
69
|
}
|
|
75
70
|
|
|
76
71
|
/// Build an [AwsS3StorageConfig] wired to an in-memory [Store].
|
|
77
72
|
AwsS3StorageConfig mockConfig(Store store) => AwsS3StorageConfig(
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
73
|
+
bucketName: 'test-bucket',
|
|
74
|
+
region: 'us-east-1',
|
|
75
|
+
accessKeyId: 'test-access-key',
|
|
76
|
+
secretAccessKey: 'test-secret',
|
|
77
|
+
clientFactory: (_) => MockAwsS3Client(store),
|
|
78
|
+
);
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
## 0.1.1
|
|
2
|
+
|
|
3
|
+
- Add `parseMetadata` / `parseTargetCohorts` exports from the D1 bundle
|
|
4
|
+
mapper for writing custom Cloudflare Worker logic.
|
|
5
|
+
- Add `defaultRolloutCohortCount` export.
|
|
6
|
+
- Repository moved to `https://github.com/HYPER12755/flutter-ota-kit`.
|
|
7
|
+
|
|
1
8
|
## 0.1.0
|
|
2
9
|
|
|
3
10
|
- Initial release of `flutter_ota_kit_cloudflare` as part of the flutter_ota_kit OTA toolkit.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// flutter_ota_kit_cloudflare example
|
|
2
|
+
//
|
|
3
|
+
// `flutter_ota_kit_cloudflare` provides Cloudflare D1 (database) + R2
|
|
4
|
+
// (storage) plugins for `flutter_ota_kit`. D1 is Cloudflare's SQLite-
|
|
5
|
+
// based serverless database; R2 is S3-compatible object storage. The
|
|
6
|
+
// combo is the cheapest self-hostable option for low-traffic apps.
|
|
7
|
+
//
|
|
8
|
+
// Most apps don't import this package directly; they wire it through
|
|
9
|
+
// `flutter_ota_kit.configureCloudflare(...)` in their app boot path.
|
|
10
|
+
// This file shows the low-level config types in case you're writing a
|
|
11
|
+
// custom server or a CLI extension.
|
|
12
|
+
//
|
|
13
|
+
// Run: `dart run example/main.dart`
|
|
14
|
+
|
|
15
|
+
import 'package:flutter_ota_kit_cloudflare/flutter_ota_kit_cloudflare.dart';
|
|
16
|
+
|
|
17
|
+
void main() {
|
|
18
|
+
// ── 1. D1 database config (Cloudflare's SQLite). ─────────────────
|
|
19
|
+
// `databaseId` is the UUID of the D1 database. `accountId` is your
|
|
20
|
+
// Cloudflare account ID. `cloudflareApiToken` needs D1 read/write.
|
|
21
|
+
const d1Config = D1DatabaseConfig(
|
|
22
|
+
databaseId: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
|
23
|
+
accountId: '0123456789abcdef0123456789abcdef',
|
|
24
|
+
cloudflareApiToken: 'cf-token-...',
|
|
25
|
+
);
|
|
26
|
+
print('D1 database: ${d1Config.databaseId}');
|
|
27
|
+
print('Account: ${d1Config.accountId}');
|
|
28
|
+
|
|
29
|
+
// ── 2. R2 storage config (S3-compatible object storage). ──────────
|
|
30
|
+
// R2 is S3-compatible: same `accessKeyId`/`secretAccessKey` flow,
|
|
31
|
+
// but the endpoint host is `{accountId}.r2.cloudflarestorage.com`.
|
|
32
|
+
const r2Config = R2S3StorageConfig(
|
|
33
|
+
accountId: '0123456789abcdef0123456789abcdef',
|
|
34
|
+
bucketName: 'flutter-ota-bundles',
|
|
35
|
+
accessKeyId: 'r2-access-key',
|
|
36
|
+
secretAccessKey: 'r2-secret-key',
|
|
37
|
+
basePath: 'bundles',
|
|
38
|
+
);
|
|
39
|
+
print('R2 bucket: ${r2Config.bucketName}');
|
|
40
|
+
print('Region: ${r2Config.region}');
|
|
41
|
+
|
|
42
|
+
// ── 3. Resolving a custom client (test seam). ────────────────────
|
|
43
|
+
// The `clientFactory` lets you inject a mock S3 client in tests or
|
|
44
|
+
// use a custom S3-compatible client in production.
|
|
45
|
+
// final client = resolveR2Client(r2Config);
|
|
46
|
+
|
|
47
|
+
// ── 4. Row-to-Bundle mapping (server-side helper). ────────────────
|
|
48
|
+
// Use this when reading bundles from a custom D1 query and passing
|
|
49
|
+
// them to the SDK's `PatchInfo.fromJson`. The mapper handles both
|
|
50
|
+
// camelCase and snake_case column names.
|
|
51
|
+
// const row = {
|
|
52
|
+
// 'id': '01a059a6-7d3c-4f3e-8a2c-9c2e3a4b5d6e',
|
|
53
|
+
// 'channel': 'production',
|
|
54
|
+
// 'enabled': true,
|
|
55
|
+
// 'file_hash': '7c4a8d09ca3762af61e59520943dc26494f8941b',
|
|
56
|
+
// 'storage_uri': 'r2://flutter-ota-bundles/01a059a6/patch.zip',
|
|
57
|
+
// 'target_app_version': '1.0.0',
|
|
58
|
+
// 'rollout_cohort_count': 100,
|
|
59
|
+
// };
|
|
60
|
+
// final bundle = transformRowToBundle(row);
|
|
61
|
+
// print('Mapped bundle: ${bundle.id} (${bundle.storageUri})');
|
|
62
|
+
}
|
|
@@ -9,10 +9,12 @@ export 'src/d1_bundle_mapper.dart'
|
|
|
9
9
|
parseTargetCohorts,
|
|
10
10
|
transformRowToBundle;
|
|
11
11
|
export 'src/d1_client.dart' show D1Client, D1ClientLike;
|
|
12
|
-
export 'src/d1_config.dart'
|
|
12
|
+
export 'src/d1_config.dart'
|
|
13
|
+
show D1ClientFactory, D1DatabaseConfig, resolveD1Client;
|
|
13
14
|
export 'src/d1_database.dart' show d1Database;
|
|
14
15
|
export 'src/cloudflare_worker_database.dart'
|
|
15
16
|
show CloudflareWorkerDatabaseConfig, cloudflareWorkerDatabase;
|
|
16
|
-
export 'src/r2_config.dart'
|
|
17
|
+
export 'src/r2_config.dart'
|
|
18
|
+
show R2S3ClientFactory, R2S3StorageConfig, resolveR2Client;
|
|
17
19
|
export 'src/r2_s3_client.dart' show R2S3Client, R2S3ClientLike;
|
|
18
20
|
export 'src/r2_storage.dart' show r2Storage;
|
|
@@ -24,6 +24,6 @@ class CloudflareWorkerDatabaseConfig {
|
|
|
24
24
|
/// client source differs (an in-process D1 binding instead of the REST API).
|
|
25
25
|
final cloudflareWorkerDatabase =
|
|
26
26
|
createDatabasePlugin<CloudflareWorkerDatabaseConfig>(
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
);
|
|
27
|
+
name: 'd1WorkerDatabase',
|
|
28
|
+
factory: (config) => D1DatabasePlugin(config.getDb()),
|
|
29
|
+
);
|
|
@@ -87,7 +87,8 @@ _BuildWhereResult buildWhereClause(DatabaseBundleQueryWhere conditions) {
|
|
|
87
87
|
params.add(conditions.fingerprintHash);
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
final whereClause =
|
|
91
|
-
|
|
90
|
+
final whereClause = clauses.isNotEmpty
|
|
91
|
+
? ' WHERE ${clauses.join(' AND ')}'
|
|
92
|
+
: '';
|
|
92
93
|
return _BuildWhereResult(whereClause, params);
|
|
93
94
|
}
|