@_nazmiforreal/flutter-ota 0.1.2 → 0.1.3
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/dart-src/packages/cli-tools/.dart_tool/package_config.json +496 -0
- package/dart-src/packages/cli-tools/.dart_tool/package_graph.json +743 -0
- package/dart-src/packages/cli-tools/lib/src/cli_base.dart +7 -6
- package/dart-src/packages/cli-tools/lib/src/commands/build.dart +46 -49
- package/dart-src/packages/cli-tools/lib/src/commands/bundle.dart +44 -23
- package/dart-src/packages/cli-tools/lib/src/commands/channel.dart +27 -17
- package/dart-src/packages/cli-tools/lib/src/commands/console.dart +11 -6
- package/dart-src/packages/cli-tools/lib/src/commands/deploy.dart +39 -22
- package/dart-src/packages/cli-tools/lib/src/commands/doctor.dart +22 -13
- package/dart-src/packages/cli-tools/lib/src/commands/init.dart +6 -5
- package/dart-src/packages/cli-tools/lib/src/commands/keys.dart +15 -9
- package/dart-src/packages/cli-tools/lib/src/commands/migrate.dart +14 -8
- package/dart-src/packages/cli-tools/lib/src/commands/rollback.dart +37 -12
- package/dart-src/packages/cli-tools/lib/src/operations.dart +45 -0
- package/dart-src/packages/cli-tools/lib/src/pack.dart +9 -0
- package/dart-src/packages/cli-tools/lib/src/ui/ui.dart +234 -0
- package/dart-src/packages/cli-tools/pubspec.lock +638 -0
- package/dart-src/packages/cli-tools/test/backends_test.dart +207 -0
- package/dart-src/packages/cli-tools/test/build_pack_test.dart +101 -0
- package/dart-src/packages/cli-tools/test/cli_test.dart +442 -0
- package/dart-src/packages/cli-tools/test/mocks/mock_aws_s3_client.dart +83 -0
- package/dart-src/packages/cli-tools/test/mocks/mock_d1_client.dart +256 -0
- package/dart-src/packages/cli-tools/test/mocks/mock_postgres_client.dart +347 -0
- package/dart-src/packages/cli-tools/test/standalone_backend_test.dart +315 -0
- package/dart-src/packages/core/test/rollout_test.dart +128 -0
- package/dart-src/packages/core/test/semver_test.dart +52 -0
- package/dart-src/packages/core/test/semver_vectors.json +1 -0
- package/dart-src/packages/core/test/types_test.dart +129 -0
- package/dart-src/packages/core/test/uuid_test.dart +38 -0
- package/dart-src/plugins/aws/test/aws_database_test.dart +167 -0
- package/dart-src/plugins/aws/test/aws_lambda_edge_storage_test.dart +190 -0
- package/dart-src/plugins/aws/test/aws_storage_test.dart +73 -0
- package/dart-src/plugins/aws/test/mock_aws_s3_client.dart +83 -0
- package/dart-src/plugins/cloudflare/test/cloudflare_worker_database_test.dart +122 -0
- package/dart-src/plugins/cloudflare/test/d1_bundle_mapper_test.dart +106 -0
- package/dart-src/plugins/cloudflare/test/d1_database_test.dart +190 -0
- package/dart-src/plugins/cloudflare/test/mock_d1_client.dart +256 -0
- package/dart-src/plugins/cloudflare/test/mock_r2_client.dart +83 -0
- package/dart-src/plugins/cloudflare/test/r2_storage_test.dart +77 -0
- package/dart-src/plugins/plugin-core/test/asset_storage_layout_test.dart +166 -0
- package/dart-src/plugins/plugin-core/test/bundle_unit_of_work_store_test.dart +74 -0
- package/dart-src/plugins/plugin-core/test/bundle_unit_of_work_test.dart +332 -0
- package/dart-src/plugins/plugin-core/test/create_blob_database_plugin_test.dart +711 -0
- package/dart-src/plugins/plugin-core/test/create_database_plugin_test.dart +294 -0
- package/dart-src/plugins/plugin-core/test/create_storage_plugin_test.dart +192 -0
- package/dart-src/plugins/plugin-core/test/generate_min_bundle_id_test.dart +51 -0
- package/dart-src/plugins/plugin-core/test/get_update_info_test.dart +1600 -0
- package/dart-src/plugins/plugin-core/test/helpers_test.dart +259 -0
- package/dart-src/plugins/plugin-core/test/query_bundles_test.dart +139 -0
- package/dart-src/plugins/plugin-core/test/request_update_bundle_state_test.dart +86 -0
- package/dart-src/plugins/plugin-core/test/uuidv7_test.dart +77 -0
- package/dart-src/plugins/postgres/test/mock_postgres_client.dart +297 -0
- package/dart-src/plugins/postgres/test/postgres_bundle_mapper_test.dart +140 -0
- package/dart-src/plugins/postgres/test/postgres_database_test.dart +181 -0
- package/dart-src/plugins/standalone/test/standalone_test.dart +264 -0
- package/dart-src/plugins/supabase/test/mock_supabase_client.dart +654 -0
- package/dart-src/plugins/supabase/test/supabase_bundle_mapper_test.dart +112 -0
- package/dart-src/plugins/supabase/test/supabase_database_test.dart +207 -0
- package/dart-src/plugins/supabase/test/supabase_storage_test.dart +175 -0
- package/package.json +1 -1
- package/scripts/vendor.sh +29 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import 'dart:convert' show jsonDecode;
|
|
2
|
+
|
|
3
|
+
import 'package:flutter_patcher_cloudflare/flutter_patcher_cloudflare.dart'
|
|
4
|
+
show
|
|
5
|
+
CloudflareWorkerDatabaseConfig,
|
|
6
|
+
D1ClientLike,
|
|
7
|
+
D1DatabaseConfig,
|
|
8
|
+
cloudflareWorkerDatabase,
|
|
9
|
+
d1Database;
|
|
10
|
+
import 'package:flutter_patcher_plugin_core/flutter_patcher_plugin_core.dart'
|
|
11
|
+
show DatabasePlugin;
|
|
12
|
+
|
|
13
|
+
/// In-memory store backing [MockD1Client], mirroring the D1 tables.
|
|
14
|
+
class Store {
|
|
15
|
+
final List<Map<String, dynamic>> bundleRows = [];
|
|
16
|
+
final List<Map<String, dynamic>> patchRows = [];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/// Mock D1 client that emulates the SQL issued by `_D1DatabasePlugin`.
|
|
20
|
+
class MockD1Client implements D1ClientLike {
|
|
21
|
+
MockD1Client(this.store);
|
|
22
|
+
|
|
23
|
+
final Store store;
|
|
24
|
+
|
|
25
|
+
bool _asBool(dynamic v) => v is bool ? v : (v as num? ?? 0) != 0;
|
|
26
|
+
|
|
27
|
+
List<String> _asList(dynamic v) {
|
|
28
|
+
final decoded = jsonDecode(v as String) as List;
|
|
29
|
+
return decoded.cast<String>();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
bool _match(Map<String, dynamic> row, String where, List<dynamic> params) {
|
|
33
|
+
var p = 0;
|
|
34
|
+
if (where.contains('channel = ?')) {
|
|
35
|
+
if (row['channel'] != params[p++]) return false;
|
|
36
|
+
}
|
|
37
|
+
if (where.contains('platform = ?')) {
|
|
38
|
+
if (row['platform'] != params[p++]) return false;
|
|
39
|
+
}
|
|
40
|
+
if (where.contains('enabled = ?')) {
|
|
41
|
+
if ((_asBool(row['enabled']) ? 1 : 0) != params[p++]) return false;
|
|
42
|
+
}
|
|
43
|
+
if (where.contains('id IN (SELECT value FROM json_each(?))')) {
|
|
44
|
+
if (!_asList(params[p++]).contains(row['id'])) return false;
|
|
45
|
+
}
|
|
46
|
+
if (where.contains('id = ?')) {
|
|
47
|
+
if (row['id'] != params[p++]) return false;
|
|
48
|
+
}
|
|
49
|
+
if (where.contains('id > ?')) {
|
|
50
|
+
if ((row['id'] as String).compareTo(params[p++] as String) <= 0) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (where.contains('id >= ?')) {
|
|
55
|
+
if ((row['id'] as String).compareTo(params[p++] as String) < 0) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (where.contains('id < ?')) {
|
|
60
|
+
if ((row['id'] as String).compareTo(params[p++] as String) >= 0) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (where.contains('id <= ?')) {
|
|
65
|
+
if ((row['id'] as String).compareTo(params[p++] as String) > 0) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (where.contains('target_app_version IS NOT NULL') &&
|
|
70
|
+
!where.contains('target_app_version =')) {
|
|
71
|
+
if (row['target_app_version'] == null) return false;
|
|
72
|
+
}
|
|
73
|
+
if (where.contains('target_app_version IS NULL')) {
|
|
74
|
+
if (row['target_app_version'] != null) return false;
|
|
75
|
+
}
|
|
76
|
+
if (where.contains('target_app_version = ?')) {
|
|
77
|
+
if (row['target_app_version'] != params[p++]) return false;
|
|
78
|
+
}
|
|
79
|
+
if (where.contains('target_app_version IN (SELECT value FROM json_each(?))')) {
|
|
80
|
+
if (!_asList(params[p++]).contains(row['target_app_version'])) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (where.contains('fingerprint_hash IS NULL')) {
|
|
85
|
+
if (row['fingerprint_hash'] != null) return false;
|
|
86
|
+
}
|
|
87
|
+
if (where.contains('fingerprint_hash = ?')) {
|
|
88
|
+
if (row['fingerprint_hash'] != params[p++]) return false;
|
|
89
|
+
}
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
List<Map<String, dynamic>> _selectBundles(String sql, List<dynamic> params) {
|
|
94
|
+
final fromIdx = sql.indexOf('FROM bundles') + 'FROM bundles'.length;
|
|
95
|
+
var endIdx = sql.length;
|
|
96
|
+
final orderIdx = sql.indexOf('ORDER BY id');
|
|
97
|
+
if (orderIdx != -1) endIdx = orderIdx;
|
|
98
|
+
final whereClause = sql.substring(fromIdx, endIdx);
|
|
99
|
+
final whereParamCount = '?'.allMatches(whereClause).length;
|
|
100
|
+
final whereParams = params.sublist(0, whereParamCount);
|
|
101
|
+
|
|
102
|
+
final filtered = store.bundleRows
|
|
103
|
+
.where((row) => _match(row, whereClause, whereParams))
|
|
104
|
+
.toList();
|
|
105
|
+
|
|
106
|
+
final descending = !sql.contains('ORDER BY id ASC');
|
|
107
|
+
filtered.sort(
|
|
108
|
+
(a, b) => descending
|
|
109
|
+
? (b['id'] as String).compareTo(a['id'] as String)
|
|
110
|
+
: (a['id'] as String).compareTo(b['id'] as String),
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
if (sql.contains('LIMIT ?')) {
|
|
114
|
+
final limit = params[whereParamCount] as int;
|
|
115
|
+
final offset = params[whereParamCount + 1] as int;
|
|
116
|
+
final start = offset.clamp(0, filtered.length);
|
|
117
|
+
final end = (offset + limit).clamp(0, filtered.length);
|
|
118
|
+
return filtered.sublist(start, end);
|
|
119
|
+
}
|
|
120
|
+
return filtered;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
@override
|
|
124
|
+
Future<List<Map<String, dynamic>>> query(
|
|
125
|
+
String sql, [
|
|
126
|
+
List<dynamic> params = const [],
|
|
127
|
+
]) async {
|
|
128
|
+
if (sql.contains('SELECT COUNT(*)')) {
|
|
129
|
+
final fromIdx = sql.indexOf('FROM bundles') + 'FROM bundles'.length;
|
|
130
|
+
final whereClause = sql.substring(fromIdx);
|
|
131
|
+
final total = store.bundleRows
|
|
132
|
+
.where((row) => _match(row, whereClause, params))
|
|
133
|
+
.length;
|
|
134
|
+
return [
|
|
135
|
+
{'total': total},
|
|
136
|
+
];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (sql.contains('SELECT channel FROM bundles')) {
|
|
140
|
+
final channels = {
|
|
141
|
+
for (final row in store.bundleRows) row['channel'] as String,
|
|
142
|
+
};
|
|
143
|
+
return [for (final c in channels) {'channel': c}];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (sql.contains('SELECT target_app_version FROM bundles')) {
|
|
147
|
+
final channel = params[0] as String;
|
|
148
|
+
final platform = params[1] as String;
|
|
149
|
+
final minId = params[2] as String;
|
|
150
|
+
return [
|
|
151
|
+
for (final row in store.bundleRows)
|
|
152
|
+
if (row['channel'] == channel &&
|
|
153
|
+
row['platform'] == platform &&
|
|
154
|
+
_asBool(row['enabled']) &&
|
|
155
|
+
(row['id'] as String).compareTo(minId) >= 0 &&
|
|
156
|
+
row['target_app_version'] != null)
|
|
157
|
+
{'target_app_version': row['target_app_version'] as String},
|
|
158
|
+
];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (sql.contains('SELECT * FROM bundle_patches')) {
|
|
162
|
+
final ids = _asList(params[0]);
|
|
163
|
+
final rows = store.patchRows
|
|
164
|
+
.where((row) => ids.contains(row['bundle_id']))
|
|
165
|
+
.toList();
|
|
166
|
+
rows.sort(
|
|
167
|
+
(a, b) => (a['order_index'] as num).compareTo(b['order_index'] as num) +
|
|
168
|
+
(a['base_bundle_id'] as String)
|
|
169
|
+
.compareTo(b['base_bundle_id'] as String),
|
|
170
|
+
);
|
|
171
|
+
return rows;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (sql.contains('SELECT * FROM bundles')) {
|
|
175
|
+
return _selectBundles(sql, params);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (sql.contains('INSERT OR REPLACE INTO bundles')) {
|
|
179
|
+
final row = <String, dynamic>{
|
|
180
|
+
'id': params[0],
|
|
181
|
+
'channel': params[1],
|
|
182
|
+
'enabled': params[2],
|
|
183
|
+
'should_force_update': params[3],
|
|
184
|
+
'file_hash': params[4],
|
|
185
|
+
'git_commit_hash': params[5],
|
|
186
|
+
'message': params[6],
|
|
187
|
+
'platform': params[7],
|
|
188
|
+
'target_app_version': params[8],
|
|
189
|
+
'storage_uri': params[9],
|
|
190
|
+
'fingerprint_hash': params[10],
|
|
191
|
+
'metadata': params[11],
|
|
192
|
+
'manifest_storage_uri': params[12],
|
|
193
|
+
'manifest_file_hash': params[13],
|
|
194
|
+
'asset_base_storage_uri': params[14],
|
|
195
|
+
'rollout_cohort_count': params[15],
|
|
196
|
+
'target_cohorts': params[16],
|
|
197
|
+
};
|
|
198
|
+
store.bundleRows
|
|
199
|
+
.removeWhere((r) => r['id'] == row['id']);
|
|
200
|
+
store.bundleRows.add(row);
|
|
201
|
+
return const [];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (sql.contains('INSERT OR REPLACE INTO bundle_patches')) {
|
|
205
|
+
final row = <String, dynamic>{
|
|
206
|
+
'id': params[0],
|
|
207
|
+
'bundle_id': params[1],
|
|
208
|
+
'base_bundle_id': params[2],
|
|
209
|
+
'base_file_hash': params[3],
|
|
210
|
+
'patch_file_hash': params[4],
|
|
211
|
+
'patch_storage_uri': params[5],
|
|
212
|
+
'order_index': params[6],
|
|
213
|
+
};
|
|
214
|
+
store.patchRows.removeWhere((r) => r['id'] == row['id']);
|
|
215
|
+
store.patchRows.add(row);
|
|
216
|
+
return const [];
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (sql.contains('DELETE FROM bundles')) {
|
|
220
|
+
final id = params[0];
|
|
221
|
+
store.bundleRows.removeWhere((r) => r['id'] == id);
|
|
222
|
+
store.patchRows
|
|
223
|
+
.removeWhere((r) => r['bundle_id'] == id || r['base_bundle_id'] == id);
|
|
224
|
+
return const [];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (sql.contains('DELETE FROM bundle_patches')) {
|
|
228
|
+
final id = params[0];
|
|
229
|
+
if (sql.contains('base_bundle_id')) {
|
|
230
|
+
store.patchRows.removeWhere((r) => r['base_bundle_id'] == id);
|
|
231
|
+
} else {
|
|
232
|
+
store.patchRows.removeWhere((r) => r['bundle_id'] == id);
|
|
233
|
+
}
|
|
234
|
+
return const [];
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return const [];
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/// Build a [D1DatabaseConfig] wired to an in-memory [Store].
|
|
242
|
+
D1DatabaseConfig mockConfig(Store store) => D1DatabaseConfig(
|
|
243
|
+
databaseId: 'test-db',
|
|
244
|
+
accountId: 'test-account',
|
|
245
|
+
cloudflareApiToken: 'test-token',
|
|
246
|
+
clientFactory: (_) => MockD1Client(store),
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
/// Construct the plugin with an in-memory store.
|
|
250
|
+
DatabasePlugin newPlugin(Store store) => d1Database(mockConfig(store))();
|
|
251
|
+
|
|
252
|
+
/// Construct the Workers D1 variant with an in-memory store.
|
|
253
|
+
DatabasePlugin newWorkerPlugin(Store store) =>
|
|
254
|
+
cloudflareWorkerDatabase(
|
|
255
|
+
CloudflareWorkerDatabaseConfig(getDb: () => MockD1Client(store)),
|
|
256
|
+
)();
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import 'dart:typed_data';
|
|
2
|
+
|
|
3
|
+
import 'package:flutter_patcher_plugin_core/flutter_patcher_plugin_core.dart';
|
|
4
|
+
|
|
5
|
+
import 'package:flutter_patcher_postgres/flutter_patcher_postgres.dart';
|
|
6
|
+
|
|
7
|
+
/// In-memory store backing [MockPostgresClient]. Mirrors the `bundles` and
|
|
8
|
+
/// `bundle_patches` tables (snake_case row maps).
|
|
9
|
+
class Store {
|
|
10
|
+
Store({List<Bundle>? seed}) {
|
|
11
|
+
if (seed != null) {
|
|
12
|
+
for (final b in seed) {
|
|
13
|
+
bundleRows.add(bundleToRowValues(b));
|
|
14
|
+
bundlePatchRows.addAll(bundleToPatchRows(b));
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
final List<Map<String, dynamic>> bundleRows = [];
|
|
20
|
+
final List<Map<String, dynamic>> bundlePatchRows = [];
|
|
21
|
+
final Map<String, List<int>> storageObjects = {};
|
|
22
|
+
|
|
23
|
+
Map<String, dynamic>? bundleRowById(String id) => bundleRows
|
|
24
|
+
.where((row) => row['id'] == id)
|
|
25
|
+
.cast<Map<String, dynamic>?>()
|
|
26
|
+
.firstOrNull;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/// Mock [PostgresClientLike] that emulates the queries the plugin issues
|
|
30
|
+
/// against an in-memory [Store]. Faithful enough to exercise the plugin's
|
|
31
|
+
/// SQL building, row mapping, transactions, and RPC calls without a live DB.
|
|
32
|
+
class MockPostgresClient implements PostgresClientLike {
|
|
33
|
+
MockPostgresClient(this.store);
|
|
34
|
+
|
|
35
|
+
final Store store;
|
|
36
|
+
final List<String> executedSqls = [];
|
|
37
|
+
final List<Map<String, dynamic>> executedParams = [];
|
|
38
|
+
|
|
39
|
+
@override
|
|
40
|
+
Future<List<Map<String, dynamic>>> execute(
|
|
41
|
+
String sql,
|
|
42
|
+
Map<String, dynamic> parameters,
|
|
43
|
+
) async {
|
|
44
|
+
executedSqls.add(sql);
|
|
45
|
+
executedParams.add(parameters);
|
|
46
|
+
return _handle(sql, parameters);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@override
|
|
50
|
+
Future<T> runTx<T>(Future<T> Function(PostgresClientLike tx) fn) async {
|
|
51
|
+
return fn(_TxClient(this));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@override
|
|
55
|
+
Future<void> close() async {}
|
|
56
|
+
|
|
57
|
+
List<Map<String, dynamic>> _handle(
|
|
58
|
+
String sql,
|
|
59
|
+
Map<String, dynamic> params,
|
|
60
|
+
) {
|
|
61
|
+
if (sql.contains('get_target_app_version_list')) {
|
|
62
|
+
return _targetAppVersionList(params);
|
|
63
|
+
}
|
|
64
|
+
if (sql.contains('get_update_info_by_app_version')) {
|
|
65
|
+
return _updateInfoByAppVersion(params);
|
|
66
|
+
}
|
|
67
|
+
if (sql.contains('get_update_info_by_fingerprint_hash')) {
|
|
68
|
+
return _updateInfoByFingerprint(params);
|
|
69
|
+
}
|
|
70
|
+
if (sql.contains('GROUP BY channel')) return _channels();
|
|
71
|
+
if (sql.contains('COUNT(*)')) return _count(sql, params);
|
|
72
|
+
if (sql.contains('FROM bundle_patches') && sql.contains('ANY')) {
|
|
73
|
+
return _patchesForIds(params);
|
|
74
|
+
}
|
|
75
|
+
if (sql.contains('INSERT INTO bundle_patches')) {
|
|
76
|
+
_upsertPatch(params);
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
if (sql.contains('INSERT INTO bundles')) {
|
|
80
|
+
_upsertBundle(params);
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
if (sql.contains('DELETE FROM bundle_patches')) {
|
|
84
|
+
_deletePatches(sql, params);
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
if (sql.contains('DELETE FROM bundles')) {
|
|
88
|
+
return _deleteBundle(sql, params);
|
|
89
|
+
}
|
|
90
|
+
if (sql.contains('SELECT * FROM bundles')) {
|
|
91
|
+
return _selectBundles(sql, params);
|
|
92
|
+
}
|
|
93
|
+
if (sql.contains('flutter_patcher_storage')) {
|
|
94
|
+
return _handleStorage(sql, params);
|
|
95
|
+
}
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
List<Map<String, dynamic>> _handleStorage(
|
|
100
|
+
String sql,
|
|
101
|
+
Map<String, dynamic> params,
|
|
102
|
+
) {
|
|
103
|
+
if (sql.contains('INSERT INTO flutter_patcher_storage')) {
|
|
104
|
+
final key = params['@key'] as String;
|
|
105
|
+
final data = params['@data'];
|
|
106
|
+
store.storageObjects[key] = data is List<int>
|
|
107
|
+
? data
|
|
108
|
+
: data is Uint8List
|
|
109
|
+
? data
|
|
110
|
+
: <int>[];
|
|
111
|
+
return [];
|
|
112
|
+
}
|
|
113
|
+
if (sql.contains('SELECT 1 FROM flutter_patcher_storage')) {
|
|
114
|
+
final key = params['@key'] as String;
|
|
115
|
+
return store.storageObjects.containsKey(key) ? [{'1': 1}] : [];
|
|
116
|
+
}
|
|
117
|
+
if (sql.contains('DELETE FROM flutter_patcher_storage')) {
|
|
118
|
+
final key = params['@key'] as String;
|
|
119
|
+
store.storageObjects.remove(key);
|
|
120
|
+
return [];
|
|
121
|
+
}
|
|
122
|
+
if (sql.contains('SELECT data FROM flutter_patcher_storage')) {
|
|
123
|
+
final key = params['@key'] as String;
|
|
124
|
+
final data = store.storageObjects[key];
|
|
125
|
+
if (data == null) return [];
|
|
126
|
+
return [
|
|
127
|
+
{'data': Uint8List.fromList(data)},
|
|
128
|
+
];
|
|
129
|
+
}
|
|
130
|
+
if (sql.contains('SELECT key, octet_length(data)')) {
|
|
131
|
+
final prefix = (params['@prefix'] as String?) ?? '';
|
|
132
|
+
return store.storageObjects.keys
|
|
133
|
+
.where((k) => k.startsWith(prefix))
|
|
134
|
+
.map((k) => {
|
|
135
|
+
'key': k,
|
|
136
|
+
'size': store.storageObjects[k]!.length,
|
|
137
|
+
})
|
|
138
|
+
.toList();
|
|
139
|
+
}
|
|
140
|
+
return [];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
dynamic _paramAfter(String sql, String token, Map<String, dynamic> params) {
|
|
144
|
+
final idx = sql.indexOf(token);
|
|
145
|
+
if (idx < 0) return null;
|
|
146
|
+
final rest = sql.substring(idx + token.length).trim();
|
|
147
|
+
final match = RegExp(r'@(\w+)').firstMatch(rest);
|
|
148
|
+
if (match == null) return null;
|
|
149
|
+
final name = match.group(1)!;
|
|
150
|
+
return params['@$name'] ?? params[name];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
List<Map<String, dynamic>> _filterBundles(
|
|
154
|
+
String sql,
|
|
155
|
+
Map<String, dynamic> params,
|
|
156
|
+
) {
|
|
157
|
+
return store.bundleRows.where((row) {
|
|
158
|
+
final channel = _paramAfter(sql, 'channel =', params);
|
|
159
|
+
if (channel != null && row['channel'] != channel) return false;
|
|
160
|
+
final platform = _paramAfter(sql, 'platform =', params);
|
|
161
|
+
if (platform != null && row['platform'] != platform) return false;
|
|
162
|
+
final enabled = _paramAfter(sql, 'enabled =', params);
|
|
163
|
+
if (enabled != null && row['enabled'] != enabled) return false;
|
|
164
|
+
final target = _paramAfter(sql, 'target_app_version =', params);
|
|
165
|
+
if (target != null && row['target_app_version'] != target) return false;
|
|
166
|
+
final idEq = _paramAfter(sql, 'id =', params);
|
|
167
|
+
if (idEq != null && row['id'] != idEq) return false;
|
|
168
|
+
return true;
|
|
169
|
+
}).toList();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
List<Map<String, dynamic>> _selectBundles(
|
|
173
|
+
String sql,
|
|
174
|
+
Map<String, dynamic> params,
|
|
175
|
+
) {
|
|
176
|
+
var rows = _filterBundles(sql, params);
|
|
177
|
+
|
|
178
|
+
if (sql.contains('ORDER BY id asc')) {
|
|
179
|
+
rows = rows.toList()..sort((a, b) => (a['id'] as String).compareTo(b['id'] as String));
|
|
180
|
+
} else {
|
|
181
|
+
rows = rows.toList()
|
|
182
|
+
..sort((a, b) => (b['id'] as String).compareTo(a['id'] as String));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
final limit = params['@limit'] as int?;
|
|
186
|
+
final offset = params['@offset'] as int? ?? 0;
|
|
187
|
+
if (limit != null && limit > 0) {
|
|
188
|
+
final end = (offset + limit).clamp(0, rows.length);
|
|
189
|
+
rows = rows.sublist(offset.clamp(0, rows.length), end);
|
|
190
|
+
} else if (offset > 0) {
|
|
191
|
+
rows = rows.sublist(offset.clamp(0, rows.length));
|
|
192
|
+
}
|
|
193
|
+
return rows;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
List<Map<String, dynamic>> _count(String sql, Map<String, dynamic> params) {
|
|
197
|
+
return [
|
|
198
|
+
{'total': _filterBundles(sql, params).length},
|
|
199
|
+
];
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
List<Map<String, dynamic>> _channels() {
|
|
203
|
+
final channels = store.bundleRows.map((r) => r['channel'] as String).toSet().toList();
|
|
204
|
+
return channels.map((c) => {'channel': c}).toList();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
List<Map<String, dynamic>> _patchesForIds(Map<String, dynamic> params) {
|
|
208
|
+
final ids = (params['bundle_ids'] as List).cast<String>();
|
|
209
|
+
return store.bundlePatchRows.where((p) => ids.contains(p['bundle_id'])).toList();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
void _upsertBundle(Map<String, dynamic> params) {
|
|
213
|
+
final row = <String, dynamic>{};
|
|
214
|
+
params.forEach((k, v) => row[k] = v);
|
|
215
|
+
final existing = store.bundleRows.indexWhere((r) => r['id'] == row['id']);
|
|
216
|
+
if (existing >= 0) {
|
|
217
|
+
store.bundleRows[existing] = row;
|
|
218
|
+
} else {
|
|
219
|
+
store.bundleRows.add(row);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
void _upsertPatch(Map<String, dynamic> params) {
|
|
224
|
+
final row = <String, dynamic>{};
|
|
225
|
+
params.forEach((k, v) => row[k] = v);
|
|
226
|
+
final existing = store.bundlePatchRows.indexWhere((r) => r['id'] == row['id']);
|
|
227
|
+
if (existing >= 0) {
|
|
228
|
+
store.bundlePatchRows[existing] = row;
|
|
229
|
+
} else {
|
|
230
|
+
store.bundlePatchRows.add(row);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
void _deletePatches(String sql, Map<String, dynamic> params) {
|
|
235
|
+
final id = params['@p0'] ?? params['p0'];
|
|
236
|
+
if (sql.contains('base_bundle_id')) {
|
|
237
|
+
store.bundlePatchRows.removeWhere((p) => p['base_bundle_id'] == id);
|
|
238
|
+
} else {
|
|
239
|
+
store.bundlePatchRows.removeWhere((p) => p['bundle_id'] == id);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
List<Map<String, dynamic>> _deleteBundle(
|
|
244
|
+
String sql,
|
|
245
|
+
Map<String, dynamic> params,
|
|
246
|
+
) {
|
|
247
|
+
final id = params['@p0'] ?? params['p0'];
|
|
248
|
+
final existed = store.bundleRows.any((r) => r['id'] == id);
|
|
249
|
+
store.bundleRows.removeWhere((r) => r['id'] == id);
|
|
250
|
+
store.bundlePatchRows.removeWhere((p) => p['bundle_id'] == id);
|
|
251
|
+
return existed ? [{'id': id}] : [];
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
List<Map<String, dynamic>> _targetAppVersionList(Map<String, dynamic> params) {
|
|
255
|
+
final platform = params['app_platform'];
|
|
256
|
+
final min = params['min_bundle_id'] as String? ?? '';
|
|
257
|
+
final versions = store.bundleRows
|
|
258
|
+
.where((r) => r['platform'] == platform && (r['id'] as String).compareTo(min) >= 0)
|
|
259
|
+
.map((r) => r['target_app_version'])
|
|
260
|
+
.where((v) => v != null)
|
|
261
|
+
.toSet()
|
|
262
|
+
.cast<String>()
|
|
263
|
+
.toList();
|
|
264
|
+
return versions.map((v) => {'target_app_version': v}).toList();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
Map<String, dynamic>? _newestMatch(
|
|
268
|
+
bool Function(Map<String, dynamic> row) predicate,
|
|
269
|
+
) {
|
|
270
|
+
final matches = store.bundleRows.where(predicate).toList()
|
|
271
|
+
..sort((a, b) => (b['id'] as String).compareTo(a['id'] as String));
|
|
272
|
+
return matches.isEmpty ? null : matches.first;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
List<Map<String, dynamic>> _updateInfoByAppVersion(Map<String, dynamic> params) {
|
|
276
|
+
final platform = params['app_platform'];
|
|
277
|
+
final channel = params['target_channel'];
|
|
278
|
+
final min = params['min_bundle_id'] as String? ?? '';
|
|
279
|
+
final list = (params['target_app_version_list'] as List?)?.cast<String>() ?? [];
|
|
280
|
+
final row = _newestMatch((r) =>
|
|
281
|
+
r['platform'] == platform &&
|
|
282
|
+
r['enabled'] == true &&
|
|
283
|
+
r['channel'] == channel &&
|
|
284
|
+
(r['id'] as String).compareTo(min) >= 0 &&
|
|
285
|
+
list.contains(r['target_app_version']));
|
|
286
|
+
if (row == null) return [];
|
|
287
|
+
return [_infoRow(row)];
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
List<Map<String, dynamic>> _updateInfoByFingerprint(Map<String, dynamic> params) {
|
|
291
|
+
final platform = params['app_platform'];
|
|
292
|
+
final channel = params['target_channel'];
|
|
293
|
+
final min = params['min_bundle_id'] as String? ?? '';
|
|
294
|
+
final fp = params['target_fingerprint_hash'];
|
|
295
|
+
final row = _newestMatch((r) =>
|
|
296
|
+
r['platform'] == platform &&
|
|
297
|
+
r['enabled'] == true &&
|
|
298
|
+
r['channel'] == channel &&
|
|
299
|
+
(r['id'] as String).compareTo(min) >= 0 &&
|
|
300
|
+
r['fingerprint_hash'] == fp);
|
|
301
|
+
if (row == null) return [];
|
|
302
|
+
return [_infoRow(row)];
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
Map<String, dynamic> _infoRow(Map<String, dynamic> row) => {
|
|
306
|
+
'id': row['id'],
|
|
307
|
+
'should_force_update': row['should_force_update'],
|
|
308
|
+
'message': row['message'],
|
|
309
|
+
'status': 'UPDATE',
|
|
310
|
+
'storage_uri': row['storage_uri'],
|
|
311
|
+
'file_hash': row['file_hash'],
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
class _TxClient implements PostgresClientLike {
|
|
316
|
+
_TxClient(this._parent);
|
|
317
|
+
|
|
318
|
+
final MockPostgresClient _parent;
|
|
319
|
+
|
|
320
|
+
@override
|
|
321
|
+
Future<List<Map<String, dynamic>>> execute(
|
|
322
|
+
String sql,
|
|
323
|
+
Map<String, dynamic> parameters,
|
|
324
|
+
) =>
|
|
325
|
+
_parent.execute(sql, parameters);
|
|
326
|
+
|
|
327
|
+
@override
|
|
328
|
+
Future<T> runTx<T>(Future<T> Function(PostgresClientLike tx) fn) =>
|
|
329
|
+
throw UnsupportedError('Nested transactions are not supported');
|
|
330
|
+
|
|
331
|
+
@override
|
|
332
|
+
Future<void> close() async {}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/// Build a [PostgresConfig] wired to [MockPostgresClient].
|
|
336
|
+
PostgresConfig mockPostgresConfig(Store store) => PostgresConfig(
|
|
337
|
+
host: 'mock',
|
|
338
|
+
database: 'mock',
|
|
339
|
+
clientFactory: (_) => MockPostgresClient(store),
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
/// Create a [DatabasePlugin] backed by the mock store.
|
|
343
|
+
DatabasePlugin newPlugin({Store? store}) {
|
|
344
|
+
final s = store ?? Store();
|
|
345
|
+
final plugin = postgresDatabase(mockPostgresConfig(s))();
|
|
346
|
+
return plugin;
|
|
347
|
+
}
|