@_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.
- package/bin/flutter-ota-linux-x64 +0 -0
- package/dart-src/packages/cli-tools/.dart_tool/package_config.json +2 -0
- package/dart-src/packages/cli-tools/.dart_tool/package_graph.json +8 -8
- package/dart-src/packages/cli-tools/lib/flutter_ota_kit_cli.dart +2 -2
- package/dart-src/packages/cli-tools/lib/src/commands/pocketbase.dart +1379 -11
- package/dart-src/packages/cli-tools/lib/src/pocketbase/installer.dart +1 -1
- package/dart-src/packages/cli-tools/lib/src/pocketbase/process_manager.dart +104 -9
- package/dart-src/packages/cli-tools/lib/src/pocketbase/schema_installer.dart +59 -5
- package/dart-src/packages/cli-tools/pubspec.lock +7 -7
- package/dart-src/packages/cli-tools/test/mocks/mock_pocketbase_client.dart +149 -0
- package/dart-src/packages/cli-tools/test/pocketbase_schema_test.dart +14 -9
- package/dart-src/packages/core/CHANGELOG.md +6 -1
- package/dart-src/packages/core/lib/src/app_update_info.dart +2 -0
- package/dart-src/plugins/aws/CHANGELOG.md +5 -1
- package/dart-src/plugins/cloudflare/CHANGELOG.md +5 -1
- package/dart-src/plugins/plugin-core/CHANGELOG.md +5 -1
- package/dart-src/plugins/pocketbase/CHANGELOG.md +5 -1
- package/dart-src/plugins/pocketbase/lib/flutter_ota_kit_pocketbase.dart +2 -1
- package/dart-src/plugins/pocketbase/lib/src/pocketbase_bundle_mapper.dart +10 -2
- package/dart-src/plugins/pocketbase/lib/src/pocketbase_client.dart +614 -4
- package/dart-src/plugins/pocketbase/lib/src/pocketbase_database.dart +262 -42
- package/dart-src/plugins/pocketbase/lib/src/pocketbase_storage.dart +9 -2
- package/dart-src/plugins/postgres/CHANGELOG.md +5 -1
- package/dart-src/plugins/supabase/CHANGELOG.md +6 -1
- package/dart-src/plugins/supabase/lib/src/supabase_database.dart +4 -0
- 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.
|
|
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;
|
|
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
|
-
|
|
98
|
-
|
|
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
|
-
|
|
121
|
-
|
|
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
|
|
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
|
-
/// - `
|
|
9
|
-
///
|
|
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/
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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/
|
|
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
|
|
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(
|
|
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 =
|
|
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(
|
|
108
|
+
expect(result.skipped, containsAll(_allCollections));
|
|
104
109
|
expect(result.created, isEmpty);
|
|
105
110
|
expect(mock.createdCollections, isEmpty);
|
|
106
111
|
});
|
|
@@ -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
|
});
|
|
@@ -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
|
-
|
|
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:
|
|
125
|
+
patches: patchArtifacts,
|
|
118
126
|
rolloutCohortCount: row.rolloutCohortCount,
|
|
119
127
|
targetCohorts: row.targetCohorts,
|
|
120
128
|
);
|