@_nazmiforreal/flutter-ota 0.1.19 → 0.1.20

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.
Files changed (26) hide show
  1. package/bin/flutter-ota-linux-x64 +0 -0
  2. package/dart-src/packages/cli-tools/.dart_tool/package_config.json +2 -0
  3. package/dart-src/packages/cli-tools/.dart_tool/package_graph.json +8 -8
  4. package/dart-src/packages/cli-tools/lib/flutter_ota_kit_cli.dart +2 -2
  5. package/dart-src/packages/cli-tools/lib/src/commands/pocketbase.dart +1379 -11
  6. package/dart-src/packages/cli-tools/lib/src/pocketbase/installer.dart +1 -1
  7. package/dart-src/packages/cli-tools/lib/src/pocketbase/process_manager.dart +104 -9
  8. package/dart-src/packages/cli-tools/lib/src/pocketbase/schema_installer.dart +59 -5
  9. package/dart-src/packages/cli-tools/pubspec.lock +7 -7
  10. package/dart-src/packages/cli-tools/test/mocks/mock_pocketbase_client.dart +149 -0
  11. package/dart-src/packages/cli-tools/test/pocketbase_schema_test.dart +14 -9
  12. package/dart-src/packages/core/CHANGELOG.md +6 -1
  13. package/dart-src/packages/core/lib/src/app_update_info.dart +2 -0
  14. package/dart-src/plugins/aws/CHANGELOG.md +5 -1
  15. package/dart-src/plugins/cloudflare/CHANGELOG.md +5 -1
  16. package/dart-src/plugins/plugin-core/CHANGELOG.md +5 -1
  17. package/dart-src/plugins/pocketbase/CHANGELOG.md +5 -1
  18. package/dart-src/plugins/pocketbase/lib/flutter_ota_kit_pocketbase.dart +2 -1
  19. package/dart-src/plugins/pocketbase/lib/src/pocketbase_bundle_mapper.dart +10 -2
  20. package/dart-src/plugins/pocketbase/lib/src/pocketbase_client.dart +614 -4
  21. package/dart-src/plugins/pocketbase/lib/src/pocketbase_database.dart +262 -42
  22. package/dart-src/plugins/pocketbase/lib/src/pocketbase_storage.dart +9 -2
  23. package/dart-src/plugins/postgres/CHANGELOG.md +5 -1
  24. package/dart-src/plugins/supabase/CHANGELOG.md +6 -1
  25. package/dart-src/plugins/supabase/lib/src/supabase_database.dart +4 -0
  26. package/package.json +2 -2
@@ -17,7 +17,7 @@ import 'package:path/path.dart' as p;
17
17
 
18
18
  /// Default PocketBase version managed by the CLI. Bumped in lockstep with
19
19
  /// the rest of the stack.
20
- const String kDefaultPocketBaseVersion = '0.22.21';
20
+ const String kDefaultPocketBaseVersion = '0.40.3';
21
21
 
22
22
  /// Where the binary + extracted files live.
23
23
  class PocketBaseInstallPaths {
@@ -4,9 +4,13 @@
4
4
  /// PB reads its data directory from the `--dir` flag (default `./pb_data`).
5
5
  /// The CLI's installer pre-creates that directory and copies hooks into
6
6
  /// `pb_data/pb_hooks/` before the first start.
7
+ ///
8
+ /// A PID file (`pb.pid`) is written to the data directory on start so that
9
+ /// `stop` can find and terminate the process even after the CLI restarts.
7
10
  library;
8
11
 
9
12
  import 'dart:async';
13
+ import 'dart:convert';
10
14
  import 'dart:io';
11
15
 
12
16
  import 'package:path/path.dart' as p;
@@ -18,17 +22,15 @@ class PocketBaseProcess {
18
22
  final Directory dataDir;
19
23
  final File binaryPath;
20
24
 
21
- bool get isRunning => true; // Process instance is always present after start.
25
+ bool get isRunning => true;
22
26
 
23
27
  int get pid => process.pid;
24
28
 
25
29
  Future<int> get exitCode => process.exitCode;
26
30
 
27
- /// Returns the stdout/stderr streams merged.
28
31
  Stream<List<int>> get output => process.stdout;
29
32
  Stream<List<int>> get errors => process.stderr;
30
33
 
31
- /// Send SIGINT (Ctrl+C) for a graceful shutdown, then SIGKILL if needed.
32
34
  Future<void> stop({Duration timeout = const Duration(seconds: 5)}) async {
33
35
  if (Platform.isWindows) {
34
36
  process.kill();
@@ -59,6 +61,55 @@ class PocketBaseProcessManager {
59
61
  PocketBaseProcess? _process;
60
62
  PocketBaseProcess? get process => _process;
61
63
 
64
+ File get pidFile => File(p.join(dataDir.path, 'pb.pid'));
65
+ File get configFile => File(p.join(dataDir.path, 'pb.json'));
66
+
67
+ /// Read the stored PID from the PID file, or -1 if not present.
68
+ int readPid() {
69
+ if (!pidFile.existsSync()) return -1;
70
+ final content = pidFile.readAsStringSync().trim();
71
+ return int.tryParse(content) ?? -1;
72
+ }
73
+
74
+ /// Check if the process identified by the PID file is still alive.
75
+ Future<bool> isRunningByPidFile() async {
76
+ final pid = readPid();
77
+ if (pid <= 0) return false;
78
+ try {
79
+ final result = await Process.run('kill', ['-0', '$pid']);
80
+ return result.exitCode == 0;
81
+ } catch (_) {
82
+ return false;
83
+ }
84
+ }
85
+
86
+ void _writePidFile(int pid) {
87
+ pidFile.writeAsStringSync(pid.toString());
88
+ }
89
+
90
+ void _writeConfigFile(int pid) {
91
+ configFile.writeAsStringSync(jsonEncode({
92
+ 'pid': pid,
93
+ 'host': host,
94
+ 'port': port,
95
+ 'url': 'http://$host:$port',
96
+ }));
97
+ }
98
+
99
+ Map<String, dynamic>? readConfig() {
100
+ if (!configFile.existsSync()) return null;
101
+ try {
102
+ return jsonDecode(configFile.readAsStringSync()) as Map<String, dynamic>;
103
+ } catch (_) {
104
+ return null;
105
+ }
106
+ }
107
+
108
+ void deletePidFile() {
109
+ if (pidFile.existsSync()) pidFile.deleteSync();
110
+ if (configFile.existsSync()) configFile.deleteSync();
111
+ }
112
+
62
113
  /// Start PB in the background. Returns once PB is listening (or after a
63
114
  /// short timeout waiting for it to come up).
64
115
  Future<PocketBaseProcess> start({
@@ -82,11 +133,22 @@ class PocketBaseProcessManager {
82
133
 
83
134
  final mergedEnv = <String, String>{
84
135
  ...Platform.environment,
85
- if (adminEmail != null) 'PB_ADMIN_EMAIL': adminEmail,
86
- if (adminPassword != null) 'PB_ADMIN_PASSWORD': adminPassword,
87
136
  ...?env,
88
137
  };
89
138
 
139
+ // Create superuser if credentials provided (PB 0.40+ doesn't support
140
+ // PB_ADMIN_EMAIL env var, so we use the CLI directly).
141
+ if (adminEmail != null && adminPassword != null) {
142
+ final result = await Process.run(
143
+ binaryPath.path,
144
+ ['superuser', 'upsert', '--dir=${dataDir.path}', adminEmail, adminPassword],
145
+ environment: mergedEnv,
146
+ );
147
+ if (result.exitCode != 0) {
148
+ // Non-fatal; serve will continue but schema install may fail.
149
+ }
150
+ }
151
+
90
152
  final proc = await Process.start(
91
153
  binaryPath.path,
92
154
  ['serve', '--http=${host}:$port', '--dir=${dataDir.path}'],
@@ -94,8 +156,10 @@ class PocketBaseProcessManager {
94
156
  mode: ProcessStartMode.detached,
95
157
  );
96
158
 
97
- // Wait for the health endpoint to come up (best-effort; fall through
98
- // after readyTimeout either way).
159
+ _writePidFile(proc.pid);
160
+ _writeConfigFile(proc.pid);
161
+
162
+ // Wait for the health endpoint to come up.
99
163
  final deadline = DateTime.now().add(readyTimeout);
100
164
  while (DateTime.now().isBefore(deadline)) {
101
165
  try {
@@ -114,10 +178,41 @@ class PocketBaseProcessManager {
114
178
  return _process!;
115
179
  }
116
180
 
181
+ /// Stop the running process by sending SIGINT, then SIGKILL if needed.
182
+ /// Also cleans up the PID file.
117
183
  Future<void> stop() async {
184
+ final proc = _process;
185
+ if (proc != null) {
186
+ await proc.stop();
187
+ _process = null;
188
+ } else {
189
+ // Try stopping by PID file (for externally started processes).
190
+ final pid = readPid();
191
+ if (pid > 0) {
192
+ try {
193
+ Process.killPid(pid, ProcessSignal.sigint);
194
+ // Give it a moment to shut down gracefully.
195
+ await Future<void>.delayed(const Duration(seconds: 2));
196
+ // Force kill if still alive.
197
+ try {
198
+ final check = await Process.run('kill', ['-0', '$pid']);
199
+ if (check.exitCode == 0) {
200
+ Process.killPid(pid, ProcessSignal.sigkill);
201
+ }
202
+ } catch (_) {}
203
+ } catch (_) {}
204
+ }
205
+ }
206
+ deletePidFile();
207
+ }
208
+
209
+ /// Get combined stdout+stderr log lines from the running process.
210
+ Stream<String> get logStream async* {
118
211
  final proc = _process;
119
212
  if (proc == null) return;
120
- await proc.stop();
121
- _process = null;
213
+ yield* proc.output
214
+ .transform(utf8.decoder)
215
+ .transform(const LineSplitter())
216
+ .map((line) => '[stdout] $line');
122
217
  }
123
218
  }
@@ -1,19 +1,22 @@
1
1
  /// Installs the flutter_ota_kit PocketBase schema (collections, indexes,
2
- /// default admin) on a running PB instance.
2
+ /// default superuser) on a running PB instance.
3
3
  ///
4
4
  /// PocketBase is initialized with:
5
5
  /// - `bundles` collection (the OTA bundle metadata)
6
6
  /// - `channels` collection (release channels with a current_bundle pointer)
7
7
  /// - `audit_log` collection (immutable event log)
8
- /// - `admins` is left to PB's built-in bootstrapping
9
- /// (set PB_ADMIN_EMAIL + PB_ADMIN_PASSWORD env vars on first start)
8
+ /// - `app_meta` collection (key-value app metadata)
9
+ /// - `bundles_patches` collection (per-bundle patch metadata)
10
+ /// - `users` is PB's built-in auth collection
11
+ ///
12
+ /// The first superuser is created via `pocketbase superuser upsert`
13
+ /// before starting the server.
10
14
  ///
11
15
  /// Idempotent: re-running the installer is a no-op when collections exist.
12
16
  library;
13
17
 
14
18
  import 'dart:convert';
15
19
 
16
- import 'package:flutter_ota_kit_pocketbase/flutter_ota_kit_pocketbase.dart';
17
20
  import 'package:http/http.dart' as http;
18
21
 
19
22
  const _kBundlesCollection = r'''
@@ -95,6 +98,50 @@ const _kAuditLogCollection = r'''
95
98
  }
96
99
  ''';
97
100
 
101
+ const _kBundlePatchesCollection = r'''
102
+ {
103
+ "name": "bundles_patches",
104
+ "type": "base",
105
+ "listRule": null,
106
+ "viewRule": null,
107
+ "createRule": null,
108
+ "updateRule": null,
109
+ "deleteRule": null,
110
+ "fields": [
111
+ {"name": "bundle_id", "type": "text", "required": true, "options": {"max": 64}},
112
+ {"name": "base_bundle_id", "type": "text", "required": true, "options": {"max": 64}},
113
+ {"name": "base_file_hash", "type": "text", "required": true, "options": {"max": 128}},
114
+ {"name": "patch_file_hash", "type": "text", "required": true, "options": {"max": 128}},
115
+ {"name": "patch_storage_uri", "type": "text", "required": true, "options": {"max": 512}},
116
+ {"name": "order_index", "type": "number", "options": {"min": 0}},
117
+ {"name": "artifact", "type": "file", "options": {"maxSelect": 1, "maxSize": 0, "mimeTypes": []}}
118
+ ],
119
+ "indexes": [
120
+ "CREATE INDEX idx_patches_bundle ON bundles_patches (bundle_id)",
121
+ "CREATE INDEX idx_patches_base ON bundles_patches (base_bundle_id)"
122
+ ]
123
+ }
124
+ ''';
125
+
126
+ const _kMetaCollection = r'''
127
+ {
128
+ "name": "app_meta",
129
+ "type": "base",
130
+ "listRule": null,
131
+ "viewRule": null,
132
+ "createRule": null,
133
+ "updateRule": null,
134
+ "deleteRule": null,
135
+ "fields": [
136
+ {"name": "key", "type": "text", "required": true, "options": {"min": 1, "max": 128}},
137
+ {"name": "value", "type": "json"}
138
+ ],
139
+ "indexes": [
140
+ "CREATE UNIQUE INDEX idx_meta_key ON app_meta (key)"
141
+ ]
142
+ }
143
+ ''';
144
+
98
145
  class _RawAdminClient {
99
146
  _RawAdminClient(this.baseUrl);
100
147
  final String baseUrl;
@@ -102,7 +149,7 @@ class _RawAdminClient {
102
149
 
103
150
  Future<String> authenticate(String email, String password) async {
104
151
  final res = await http.post(
105
- Uri.parse('$baseUrl/api/admins/auth-with-password'),
152
+ Uri.parse('$baseUrl/api/collections/_superusers/auth-with-password'),
106
153
  headers: {'content-type': 'application/json'},
107
154
  body: jsonEncode({'identity': email, 'password': password}),
108
155
  );
@@ -209,6 +256,10 @@ class PocketBaseSchemaInstaller {
209
256
  String rename(String json) {
210
257
  if (bundlesCollection != 'bundles') {
211
258
  json = json.replaceAll('"bundles"', '"$bundlesCollection"');
259
+ json = json.replaceAll(
260
+ 'bundles_patches',
261
+ '${bundlesCollection}_patches',
262
+ );
212
263
  }
213
264
  if (channelsCollection != 'channels') {
214
265
  json = json.replaceAll('"channels"', '"$channelsCollection"');
@@ -219,10 +270,13 @@ class PocketBaseSchemaInstaller {
219
270
  return json;
220
271
  }
221
272
 
273
+ final patchesCollectionName = '${bundlesCollection}_patches';
222
274
  return [
223
275
  _CollectionSpec(bundlesCollection, rename(_kBundlesCollection)),
224
276
  _CollectionSpec(channelsCollection, rename(_kChannelsCollection)),
225
277
  _CollectionSpec(auditLogCollection, rename(_kAuditLogCollection)),
278
+ _CollectionSpec(patchesCollectionName, rename(_kBundlePatchesCollection)),
279
+ _CollectionSpec('app_meta', _kMetaCollection),
226
280
  ];
227
281
  }
228
282
  }
@@ -159,49 +159,49 @@ packages:
159
159
  path: "../../plugins/aws"
160
160
  relative: true
161
161
  source: path
162
- version: "0.1.5"
162
+ version: "0.1.7"
163
163
  flutter_ota_kit_cloudflare:
164
164
  dependency: "direct main"
165
165
  description:
166
166
  path: "../../plugins/cloudflare"
167
167
  relative: true
168
168
  source: path
169
- version: "0.1.5"
169
+ version: "0.1.7"
170
170
  flutter_ota_kit_core:
171
171
  dependency: "direct main"
172
172
  description:
173
173
  path: "../core"
174
174
  relative: true
175
175
  source: path
176
- version: "0.1.4"
176
+ version: "0.1.6"
177
177
  flutter_ota_kit_plugin_core:
178
178
  dependency: "direct main"
179
179
  description:
180
180
  path: "../../plugins/plugin-core"
181
181
  relative: true
182
182
  source: path
183
- version: "0.0.6"
183
+ version: "0.0.8"
184
184
  flutter_ota_kit_pocketbase:
185
185
  dependency: "direct main"
186
186
  description:
187
187
  path: "../../plugins/pocketbase"
188
188
  relative: true
189
189
  source: path
190
- version: "0.1.2"
190
+ version: "0.1.4"
191
191
  flutter_ota_kit_postgres:
192
192
  dependency: "direct main"
193
193
  description:
194
194
  path: "../../plugins/postgres"
195
195
  relative: true
196
196
  source: path
197
- version: "0.1.5"
197
+ version: "0.1.7"
198
198
  flutter_ota_kit_supabase:
199
199
  dependency: "direct main"
200
200
  description:
201
201
  path: "../../plugins/supabase"
202
202
  relative: true
203
203
  source: path
204
- version: "0.1.6"
204
+ version: "0.1.8"
205
205
  frontend_server_client:
206
206
  dependency: transitive
207
207
  description:
@@ -6,6 +6,7 @@
6
6
  library;
7
7
 
8
8
  import 'package:flutter_ota_kit_pocketbase/flutter_ota_kit_pocketbase.dart';
9
+ import 'package:http/http.dart' as http;
9
10
 
10
11
  class MockPocketBaseClient implements PocketBaseClient {
11
12
  MockPocketBaseClient(this.store);
@@ -182,6 +183,154 @@ class MockPocketBaseClient implements PocketBaseClient {
182
183
  void close() {
183
184
  _closed = true;
184
185
  }
186
+
187
+ @override
188
+ Future<Map<String, dynamic>> healthDetailed() async => {
189
+ 'code': 200,
190
+ 'message': 'API is healthy.',
191
+ 'data': {'canBackup': false},
192
+ };
193
+
194
+ @override
195
+ Future<List<PocketBaseBackup>> listBackups() async => [];
196
+
197
+ @override
198
+ Future<void> createBackup({String? name}) async {}
199
+
200
+ @override
201
+ Future<void> deleteBackup(String key) async {}
202
+
203
+ @override
204
+ Future<void> restoreBackup(String key) async {}
205
+
206
+ @override
207
+ String backupDownloadUrl(String key, String token) => '';
208
+
209
+ @override
210
+ Future<List<Map<String, dynamic>>> listAdmins() async => [];
211
+
212
+ @override
213
+ Future<Map<String, dynamic>> createAdmin({
214
+ required String email,
215
+ required String password,
216
+ required String passwordConfirm,
217
+ }) async => {'id': 'mock_admin', 'email': email};
218
+
219
+ @override
220
+ Future<Map<String, dynamic>> updateAdmin(
221
+ String id,
222
+ Map<String, dynamic> data,
223
+ ) async => data;
224
+
225
+ @override
226
+ Future<void> deleteAdmin(String id) async {}
227
+
228
+ @override
229
+ Future<List<Map<String, dynamic>>> exportCollection(String collection) async =>
230
+ [];
231
+
232
+ @override
233
+ Future<void> importCollection(
234
+ String collection,
235
+ List<Map<String, dynamic>> records,
236
+ ) async {}
237
+
238
+ @override
239
+ Future<http.Response> rawRequest(
240
+ String method,
241
+ String path, {
242
+ Map<String, dynamic>? body,
243
+ }) async => http.Response('{}', 200);
244
+
245
+ @override
246
+ Future<List<int>> downloadFileUnauth(String url) async => [];
247
+
248
+ @override
249
+ Future<Map<String, dynamic>> listLogs({
250
+ String? filter, String? sort, int page = 1, int perPage = 30,
251
+ }) async => {'items': [], 'totalItems': 0};
252
+
253
+ @override
254
+ Future<Map<String, dynamic>> getLog(String id) async => {};
255
+
256
+ @override
257
+ Future<List<Map<String, dynamic>>> getLogStats({String? filter}) async => [];
258
+
259
+ @override
260
+ Future<void> truncateLogs() async {}
261
+
262
+ @override
263
+ Future<List<Map<String, dynamic>>> listCollections() async => [];
264
+
265
+ @override
266
+ Future<Map<String, dynamic>> getCollection(String nameOrId) async => {};
267
+
268
+ @override
269
+ Future<Map<String, dynamic>> createCollection(
270
+ Map<String, dynamic> body,
271
+ ) async => body;
272
+
273
+ @override
274
+ Future<Map<String, dynamic>> updateCollection(
275
+ String nameOrId,
276
+ Map<String, dynamic> body,
277
+ ) async => body;
278
+
279
+ @override
280
+ Future<void> deleteCollection(String nameOrId) async {}
281
+
282
+ @override
283
+ Future<void> truncateCollection(String nameOrId) async {}
284
+
285
+ @override
286
+ Future<void> importCollections(
287
+ List<Map<String, dynamic>> collections, {
288
+ bool deleteMissing = false,
289
+ }) async {}
290
+
291
+ @override
292
+ Future<void> uploadBackup(List<int> bytes, {String? name}) async {}
293
+
294
+ @override
295
+ Future<List<Map<String, dynamic>>> batch(
296
+ List<Map<String, dynamic>> requests,
297
+ ) async => [];
298
+
299
+ @override
300
+ Future<Map<String, dynamic>> listSettings() async => {};
301
+
302
+ @override
303
+ Future<Map<String, dynamic>> updateSettings(
304
+ Map<String, dynamic> body,
305
+ ) async => body;
306
+
307
+ @override
308
+ Future<void> testS3(String filesystem) async {}
309
+
310
+ @override
311
+ Future<void> testEmail({
312
+ required String email,
313
+ required String template,
314
+ String? collection,
315
+ }) async {}
316
+
317
+ @override
318
+ Future<Map<String, dynamic>> runSql(String query) async => {};
319
+
320
+ @override
321
+ Future<List<Map<String, dynamic>>> listCrons() async => [];
322
+
323
+ @override
324
+ Future<void> runCron(String jobId) async {}
325
+
326
+ @override
327
+ Future<Map<String, dynamic>> getCollectionScaffolds() async => {};
328
+
329
+ @override
330
+ Future<List<Map<String, dynamic>>> listOAuth2Providers() async => [];
331
+
332
+ @override
333
+ Future<Map<String, dynamic>> dryRunViewQuery(String query) async => {};
185
334
  }
186
335
 
187
336
  /// In-memory store backing [MockPocketBaseClient].
@@ -29,7 +29,7 @@ class _MockPocketBase {
29
29
 
30
30
  Future<void> _handle(HttpRequest req) async {
31
31
  final path = req.uri.path;
32
- if (path == '/api/admins/auth-with-password') {
32
+ if (path == '/api/collections/_superusers/auth-with-password') {
33
33
  authCalled = true;
34
34
  final body = jsonDecode(await utf8.decoder.bind(req).join()) as Map;
35
35
  lastAuthEmail = body['identity'] as String?;
@@ -63,6 +63,14 @@ class _MockPocketBase {
63
63
  }
64
64
  }
65
65
 
66
+ const _allCollections = [
67
+ 'bundles',
68
+ 'channels',
69
+ 'audit_log',
70
+ 'bundles_patches',
71
+ 'app_meta',
72
+ ];
73
+
66
74
  void main() {
67
75
  late _MockPocketBase mock;
68
76
  setUp(() async {
@@ -73,7 +81,7 @@ void main() {
73
81
  });
74
82
 
75
83
  test(
76
- 'schema installer creates the three collections on a fresh PB',
84
+ 'schema installer creates all five collections on a fresh PB',
77
85
  () async {
78
86
  final installer = PocketBaseSchemaInstaller(
79
87
  url: mock.baseUrl,
@@ -83,24 +91,21 @@ void main() {
83
91
  final result = await installer.install();
84
92
  expect(mock.authCalled, isTrue);
85
93
  expect(mock.lastAuthEmail, 'admin@x.com');
86
- expect(result.created, containsAll(['bundles', 'channels', 'audit_log']));
94
+ expect(result.created, containsAll(_allCollections));
87
95
  expect(result.skipped, isEmpty);
88
- expect(
89
- mock.createdCollections,
90
- containsAll(['bundles', 'channels', 'audit_log']),
91
- );
96
+ expect(mock.createdCollections, containsAll(_allCollections));
92
97
  },
93
98
  );
94
99
 
95
100
  test('schema installer skips collections that already exist', () async {
96
- mock.existingCollections = ['bundles', 'channels', 'audit_log'];
101
+ mock.existingCollections = _allCollections;
97
102
  final installer = PocketBaseSchemaInstaller(
98
103
  url: mock.baseUrl,
99
104
  adminEmail: 'admin@x.com',
100
105
  adminPassword: 'secret',
101
106
  );
102
107
  final result = await installer.install();
103
- expect(result.skipped, containsAll(['bundles', 'channels', 'audit_log']));
108
+ expect(result.skipped, containsAll(_allCollections));
104
109
  expect(result.created, isEmpty);
105
110
  expect(mock.createdCollections, isEmpty);
106
111
  });
@@ -1 +1,6 @@
1
- ## 0.1.4
1
+ ## 0.1.6
2
+
3
+ - Added `gitCommitHash` field to `UpdateInfo` for commit tracking.
4
+ - General maintenance.
5
+
6
+ ## 0.1.5
@@ -9,6 +9,7 @@ class UpdateInfo {
9
9
  final UpdateStatus status;
10
10
  final String? storageUri;
11
11
  final String? fileHash;
12
+ final String? gitCommitHash;
12
13
 
13
14
  /// Internal rollout metadata; never serialized to update-check clients.
14
15
  final int? rolloutCohortCount;
@@ -21,6 +22,7 @@ class UpdateInfo {
21
22
  required this.status,
22
23
  required this.storageUri,
23
24
  required this.fileHash,
25
+ this.gitCommitHash,
24
26
  this.rolloutCohortCount,
25
27
  this.targetCohorts,
26
28
  });
@@ -1 +1,5 @@
1
- ## 0.1.5
1
+ ## 0.1.7
2
+
3
+ - General maintenance.
4
+
5
+ ## 0.1.6
@@ -1 +1,5 @@
1
- ## 0.1.5
1
+ ## 0.1.7
2
+
3
+ - General maintenance.
4
+
5
+ ## 0.1.6
@@ -1 +1,5 @@
1
- ## 0.0.6
1
+ ## 0.0.8
2
+
3
+ - General maintenance.
4
+
5
+ ## 0.0.7
@@ -1 +1,5 @@
1
- ## 0.1.2
1
+ ## 0.1.4
2
+
3
+ - General maintenance.
4
+
5
+ ## 0.1.3
@@ -16,7 +16,8 @@ export 'src/pocketbase_client.dart'
16
16
  PocketBaseClient,
17
17
  PocketBaseClientFactory,
18
18
  PocketBaseException,
19
- PocketBaseList;
19
+ PocketBaseList,
20
+ PocketBaseBackup;
20
21
  export 'src/pocketbase_database.dart'
21
22
  show pocketbaseDatabase, PocketBaseDatabaseConfig;
22
23
  export 'src/pocketbase_storage.dart'
@@ -96,8 +96,16 @@ class PocketBaseBundleRow {
96
96
  }
97
97
 
98
98
  /// Convert a [PocketBaseBundleRow] into a [Bundle].
99
- Bundle mapRowToBundle(PocketBaseBundleRow row) {
99
+ ///
100
+ /// If [patches] is provided, they are attached as [BundlePatchArtifact]s.
101
+ Bundle mapRowToBundle(
102
+ PocketBaseBundleRow row, {
103
+ List<Map<String, dynamic>>? patches,
104
+ }) {
100
105
  final rawMetadata = normalizeMetadata(row.metadata);
106
+ final patchArtifacts = patches
107
+ ?.map((p) => BundlePatchArtifact.fromJson(p.cast<String, dynamic>()))
108
+ .toList();
101
109
  return Bundle(
102
110
  id: row.id,
103
111
  channel: row.channel,
@@ -114,7 +122,7 @@ Bundle mapRowToBundle(PocketBaseBundleRow row) {
114
122
  manifestStorageUri: row.manifestStorageUri,
115
123
  manifestFileHash: row.manifestFileHash,
116
124
  assetBaseStorageUri: row.assetBaseStorageUri,
117
- patches: const [],
125
+ patches: patchArtifacts,
118
126
  rolloutCohortCount: row.rolloutCohortCount,
119
127
  targetCohorts: row.targetCohorts,
120
128
  );