@_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.
Files changed (176) hide show
  1. package/README.md +1 -1
  2. package/bin/flutter-ota-linux-x64 +0 -0
  3. package/dart-src/packages/cli-tools/.dart_tool/package_config.json +46 -76
  4. package/dart-src/packages/cli-tools/.dart_tool/package_graph.json +140 -166
  5. package/dart-src/packages/cli-tools/README.md +1 -1
  6. package/dart-src/packages/cli-tools/lib/flutter_ota_kit_cli.dart +30 -17
  7. package/dart-src/packages/cli-tools/lib/src/backend.dart +30 -31
  8. package/dart-src/packages/cli-tools/lib/src/cli_base.dart +3 -1
  9. package/dart-src/packages/cli-tools/lib/src/commands/build.dart +44 -43
  10. package/dart-src/packages/cli-tools/lib/src/commands/bundle.dart +265 -239
  11. package/dart-src/packages/cli-tools/lib/src/commands/channel.dart +75 -66
  12. package/dart-src/packages/cli-tools/lib/src/commands/config_command.dart +39 -35
  13. package/dart-src/packages/cli-tools/lib/src/commands/console.dart +30 -27
  14. package/dart-src/packages/cli-tools/lib/src/commands/deploy.dart +88 -72
  15. package/dart-src/packages/cli-tools/lib/src/commands/doctor.dart +66 -29
  16. package/dart-src/packages/cli-tools/lib/src/commands/fingerprint.dart +10 -6
  17. package/dart-src/packages/cli-tools/lib/src/commands/init.dart +299 -190
  18. package/dart-src/packages/cli-tools/lib/src/commands/keys.dart +24 -23
  19. package/dart-src/packages/cli-tools/lib/src/commands/migrate.dart +111 -97
  20. package/dart-src/packages/cli-tools/lib/src/commands/pocketbase.dart +237 -0
  21. package/dart-src/packages/cli-tools/lib/src/commands/rollback.dart +43 -34
  22. package/dart-src/packages/cli-tools/lib/src/commands/storage.dart +57 -53
  23. package/dart-src/packages/cli-tools/lib/src/config.dart +227 -93
  24. package/dart-src/packages/cli-tools/lib/src/operations.dart +9 -8
  25. package/dart-src/packages/cli-tools/lib/src/pack.dart +26 -25
  26. package/dart-src/packages/cli-tools/lib/src/pocketbase/data_bootstrap.dart +39 -0
  27. package/dart-src/packages/cli-tools/lib/src/pocketbase/hooks/bundles.pb.js +56 -0
  28. package/dart-src/packages/cli-tools/lib/src/pocketbase/installer.dart +189 -0
  29. package/dart-src/packages/cli-tools/lib/src/pocketbase/process_manager.dart +123 -0
  30. package/dart-src/packages/cli-tools/lib/src/pocketbase/schema_installer.dart +237 -0
  31. package/dart-src/packages/cli-tools/lib/src/runner.dart +36 -15
  32. package/dart-src/packages/cli-tools/lib/src/sign.dart +13 -5
  33. package/dart-src/packages/cli-tools/lib/src/ui/ui.dart +10 -13
  34. package/dart-src/packages/cli-tools/lib/src/util.dart +3 -1
  35. package/dart-src/packages/cli-tools/pubspec.lock +69 -116
  36. package/dart-src/packages/cli-tools/pubspec.yaml +25 -7
  37. package/dart-src/packages/cli-tools/test/backends_test.dart +62 -32
  38. package/dart-src/packages/cli-tools/test/build_pack_test.dart +4 -4
  39. package/dart-src/packages/cli-tools/test/cli_test.dart +89 -42
  40. package/dart-src/packages/cli-tools/test/mocks/mock_aws_s3_client.dart +8 -13
  41. package/dart-src/packages/cli-tools/test/mocks/mock_d1_client.dart +23 -18
  42. package/dart-src/packages/cli-tools/test/mocks/mock_pocketbase_client.dart +197 -0
  43. package/dart-src/packages/cli-tools/test/mocks/mock_postgres_client.dart +70 -46
  44. package/dart-src/packages/cli-tools/test/pocketbase_installer_test.dart +72 -0
  45. package/dart-src/packages/cli-tools/test/pocketbase_schema_test.dart +107 -0
  46. package/dart-src/packages/cli-tools/test/pocketbase_smoke_test.dart +59 -0
  47. package/dart-src/packages/core/README.md +2 -2
  48. package/dart-src/packages/core/example/lib/main.dart +69 -0
  49. package/dart-src/packages/core/lib/flutter_ota_kit_core.dart +1 -1
  50. package/dart-src/packages/core/lib/src/app_update_info.dart +17 -15
  51. package/dart-src/packages/core/lib/src/bundle.dart +102 -94
  52. package/dart-src/packages/core/lib/src/bundle_artifacts.dart +1 -2
  53. package/dart-src/packages/core/lib/src/bundle_patch_artifact.dart +15 -16
  54. package/dart-src/packages/core/lib/src/changed_asset.dart +24 -22
  55. package/dart-src/packages/core/lib/src/get_bundles_args.dart +13 -13
  56. package/dart-src/packages/core/lib/src/metadata.dart +9 -7
  57. package/dart-src/packages/core/lib/src/patch_info.dart +8 -7
  58. package/dart-src/packages/core/lib/src/platform.dart +4 -3
  59. package/dart-src/packages/core/lib/src/rollout.dart +14 -11
  60. package/dart-src/packages/core/lib/src/semver_range.dart +49 -39
  61. package/dart-src/packages/core/lib/src/semver_version.dart +8 -7
  62. package/dart-src/packages/core/lib/src/status.dart +4 -3
  63. package/dart-src/packages/core/lib/src/update_bundle_params.dart +7 -7
  64. package/dart-src/packages/core/lib/src/uuid.dart +1 -2
  65. package/dart-src/packages/core/pubspec.yaml +6 -7
  66. package/dart-src/packages/core/test/rollout_test.dart +15 -8
  67. package/dart-src/packages/core/test/semver_test.dart +5 -2
  68. package/dart-src/packages/core/test/types_test.dart +26 -24
  69. package/dart-src/packages/core/test/uuid_test.dart +3 -1
  70. package/dart-src/packages/core/tool/dbg.dart +1 -0
  71. package/dart-src/plugins/aws/CHANGELOG.md +6 -0
  72. package/dart-src/plugins/aws/example/lib/main.dart +60 -0
  73. package/dart-src/plugins/aws/lib/flutter_ota_kit_aws.dart +5 -1
  74. package/dart-src/plugins/aws/lib/src/aws_cloudfront_client.dart +11 -16
  75. package/dart-src/plugins/aws/lib/src/aws_cloudfront_signer.dart +7 -2
  76. package/dart-src/plugins/aws/lib/src/aws_database.dart +14 -18
  77. package/dart-src/plugins/aws/lib/src/aws_s3_client.dart +23 -18
  78. package/dart-src/plugins/aws/lib/src/aws_ssm_client.dart +5 -4
  79. package/dart-src/plugins/aws/lib/src/aws_storage.dart +2 -3
  80. package/dart-src/plugins/aws/lib/src/aws_storage_profile.dart +10 -16
  81. package/dart-src/plugins/aws/lib/src/with_cloudfront_signed_url.dart +6 -9
  82. package/dart-src/plugins/aws/pubspec.yaml +15 -10
  83. package/dart-src/plugins/aws/test/aws_database_test.dart +51 -57
  84. package/dart-src/plugins/aws/test/mock_aws_s3_client.dart +8 -13
  85. package/dart-src/plugins/cloudflare/CHANGELOG.md +7 -0
  86. package/dart-src/plugins/cloudflare/example/lib/main.dart +62 -0
  87. package/dart-src/plugins/cloudflare/lib/flutter_ota_kit_cloudflare.dart +4 -2
  88. package/dart-src/plugins/cloudflare/lib/src/cloudflare_worker_database.dart +3 -3
  89. package/dart-src/plugins/cloudflare/lib/src/d1_build_where.dart +3 -2
  90. package/dart-src/plugins/cloudflare/lib/src/d1_bundle_mapper.dart +24 -27
  91. package/dart-src/plugins/cloudflare/lib/src/d1_database_plugin.dart +22 -30
  92. package/dart-src/plugins/cloudflare/lib/src/r2_config.dart +1 -1
  93. package/dart-src/plugins/cloudflare/lib/src/r2_s3_client.dart +21 -29
  94. package/dart-src/plugins/cloudflare/lib/src/r2_storage.dart +7 -7
  95. package/dart-src/plugins/cloudflare/lib/src/r2_storage_profile.dart +7 -15
  96. package/dart-src/plugins/cloudflare/pubspec.yaml +14 -9
  97. package/dart-src/plugins/cloudflare/test/cloudflare_worker_database_test.dart +46 -42
  98. package/dart-src/plugins/cloudflare/test/d1_database_test.dart +100 -90
  99. package/dart-src/plugins/cloudflare/test/mock_d1_client.dart +23 -18
  100. package/dart-src/plugins/cloudflare/test/mock_r2_client.dart +8 -13
  101. package/dart-src/plugins/cloudflare/test/r2_storage_test.dart +1 -4
  102. package/dart-src/plugins/plugin-core/CHANGELOG.md +16 -0
  103. package/dart-src/plugins/plugin-core/example/lib/main.dart +61 -0
  104. package/dart-src/plugins/plugin-core/lib/flutter_ota_kit_plugin_core.dart +3 -7
  105. package/dart-src/plugins/plugin-core/lib/src/asset_storage_layout.dart +5 -5
  106. package/dart-src/plugins/plugin-core/lib/src/bundle_helpers.dart +23 -0
  107. package/dart-src/plugins/plugin-core/lib/src/bundle_storage_layout.dart +16 -10
  108. package/dart-src/plugins/plugin-core/lib/src/bundle_unit_of_work.dart +31 -26
  109. package/dart-src/plugins/plugin-core/lib/src/calculate_pagination.dart +5 -1
  110. package/dart-src/plugins/plugin-core/lib/src/content_addressed_assets.dart +1 -1
  111. package/dart-src/plugins/plugin-core/lib/src/create_blob_database_plugin.dart +78 -71
  112. package/dart-src/plugins/plugin-core/lib/src/create_database_plugin.dart +56 -35
  113. package/dart-src/plugins/plugin-core/lib/src/create_storage_plugin.dart +10 -13
  114. package/dart-src/plugins/plugin-core/lib/src/paginate_bundles.dart +30 -17
  115. package/dart-src/plugins/plugin-core/lib/src/parse_storage_uri.dart +3 -1
  116. package/dart-src/plugins/plugin-core/lib/src/query_bundles.dart +5 -3
  117. package/dart-src/plugins/plugin-core/lib/src/request_update_bundle_state.dart +2 -4
  118. package/dart-src/plugins/plugin-core/lib/src/storage_helpers.dart +18 -0
  119. package/dart-src/plugins/plugin-core/lib/src/types.dart +7 -6
  120. package/dart-src/plugins/plugin-core/pubspec.yaml +12 -9
  121. package/dart-src/plugins/plugin-core/test/asset_storage_layout_test.dart +13 -9
  122. package/dart-src/plugins/plugin-core/test/bundle_unit_of_work_test.dart +28 -15
  123. package/dart-src/plugins/plugin-core/test/create_blob_database_plugin_test.dart +254 -122
  124. package/dart-src/plugins/plugin-core/test/create_database_plugin_test.dart +36 -32
  125. package/dart-src/plugins/plugin-core/test/create_storage_plugin_test.dart +18 -15
  126. package/dart-src/plugins/plugin-core/test/generate_min_bundle_id_test.dart +8 -3
  127. package/dart-src/plugins/plugin-core/test/get_update_info_test.dart +682 -693
  128. package/dart-src/plugins/plugin-core/test/helpers_test.dart +45 -21
  129. package/dart-src/plugins/plugin-core/test/query_bundles_test.dart +76 -13
  130. package/dart-src/plugins/plugin-core/test/uuidv7_test.dart +20 -9
  131. package/dart-src/plugins/pocketbase/CHANGELOG.md +8 -0
  132. package/dart-src/plugins/pocketbase/LICENSE +21 -0
  133. package/dart-src/plugins/pocketbase/README.md +54 -0
  134. package/dart-src/plugins/pocketbase/example/lib/main.dart +62 -0
  135. package/dart-src/plugins/pocketbase/lib/flutter_ota_kit_pocketbase.dart +28 -0
  136. package/dart-src/plugins/pocketbase/lib/src/pocketbase_bundle_mapper.dart +121 -0
  137. package/dart-src/plugins/pocketbase/lib/src/pocketbase_client.dart +349 -0
  138. package/dart-src/plugins/pocketbase/lib/src/pocketbase_config.dart +61 -0
  139. package/dart-src/plugins/pocketbase/lib/src/pocketbase_database.dart +278 -0
  140. package/dart-src/plugins/pocketbase/lib/src/pocketbase_storage.dart +255 -0
  141. package/dart-src/plugins/pocketbase/pubspec.yaml +26 -0
  142. package/dart-src/plugins/pocketbase/test/pb_uri_test.dart +12 -0
  143. package/dart-src/plugins/postgres/CHANGELOG.md +6 -0
  144. package/dart-src/plugins/postgres/example/lib/main.dart +67 -0
  145. package/dart-src/plugins/postgres/lib/flutter_ota_kit_postgres.dart +3 -6
  146. package/dart-src/plugins/postgres/lib/src/postgres_bundle_mapper.dart +32 -52
  147. package/dart-src/plugins/postgres/lib/src/postgres_config.dart +3 -1
  148. package/dart-src/plugins/postgres/lib/src/postgres_database.dart +28 -38
  149. package/dart-src/plugins/postgres/lib/src/postgres_get_update_info.dart +16 -14
  150. package/dart-src/plugins/postgres/lib/src/postgres_storage.dart +26 -14
  151. package/dart-src/plugins/postgres/lib/src/postgres_types.dart +27 -28
  152. package/dart-src/plugins/postgres/pubspec.yaml +14 -9
  153. package/dart-src/plugins/postgres/test/mock_postgres_client.dart +62 -39
  154. package/dart-src/plugins/postgres/test/postgres_bundle_mapper_test.dart +3 -4
  155. package/dart-src/plugins/postgres/test/postgres_database_test.dart +102 -73
  156. package/dart-src/plugins/postgres/test/postgres_storage_runtime_test.dart +60 -0
  157. package/dart-src/plugins/supabase/CHANGELOG.md +17 -0
  158. package/dart-src/plugins/supabase/README.md +2 -2
  159. package/dart-src/plugins/supabase/example/lib/main.dart +63 -0
  160. package/dart-src/plugins/supabase/lib/flutter_ota_kit_supabase.dart +2 -0
  161. package/dart-src/plugins/supabase/lib/src/error_message.dart +11 -0
  162. package/dart-src/plugins/supabase/lib/src/supabase_bundle_mapper.dart +34 -58
  163. package/dart-src/plugins/supabase/lib/src/supabase_client_adapter.dart +3 -13
  164. package/dart-src/plugins/supabase/lib/src/supabase_client_http.dart +34 -52
  165. package/dart-src/plugins/supabase/lib/src/supabase_database.dart +38 -30
  166. package/dart-src/plugins/supabase/lib/src/supabase_edge_function_storage.dart +21 -29
  167. package/dart-src/plugins/supabase/lib/src/supabase_signed_url_batcher.dart +7 -19
  168. package/dart-src/plugins/supabase/lib/src/supabase_storage.dart +18 -29
  169. package/dart-src/plugins/supabase/lib/src/types.dart +26 -26
  170. package/dart-src/plugins/supabase/pubspec.yaml +14 -9
  171. package/dart-src/plugins/supabase/test/mock_supabase_client.dart +57 -57
  172. package/dart-src/plugins/supabase/test/supabase_bundle_mapper_test.dart +30 -27
  173. package/dart-src/plugins/supabase/test/supabase_database_test.dart +26 -35
  174. package/dart-src/plugins/supabase/test/supabase_storage_test.dart +27 -34
  175. package/package.json +2 -2
  176. package/scripts/vendor.sh +1 -1
@@ -0,0 +1,255 @@
1
+ /// PocketBase-backed [StoragePlugin] implementation.
2
+ ///
3
+ /// Uses PocketBase's built-in file storage (per-collection buckets) for
4
+ /// bundle artifacts. Each bundle record carries a single `artifact` file
5
+ /// field that stores the patch zip; the storage URI is a `pb://` URL that
6
+ /// the runtime resolves to a signed download URL on the client.
7
+ library;
8
+
9
+ import 'dart:io';
10
+
11
+ import 'package:flutter_ota_kit_plugin_core/flutter_ota_kit_plugin_core.dart';
12
+
13
+ import 'pocketbase_client.dart';
14
+
15
+ /// Configuration for the PocketBase storage plugin.
16
+ class PocketBaseStorageConfig {
17
+ const PocketBaseStorageConfig({
18
+ required this.url,
19
+ required this.adminEmail,
20
+ required this.adminPassword,
21
+ required this.bundlesCollection,
22
+ this.bundlesBucket = 'bundles',
23
+ this.basePath,
24
+ this.clientFactory,
25
+ });
26
+
27
+ final String url;
28
+ final String adminEmail;
29
+ final String adminPassword;
30
+ final String bundlesCollection;
31
+ final String bundlesBucket;
32
+ final String? basePath;
33
+ final PocketBaseClientFactory? clientFactory;
34
+ }
35
+
36
+ /// Parse a `pb://bundles/{recordId}/{filename}` URI back into its parts.
37
+ ({String recordId, String filename, String? bucket})? parsePocketBaseStorageUri(
38
+ String storageUri,
39
+ ) {
40
+ // Accepts both `pb://{bucket}/{id}/{file}` and `pb:///{id}/{file}` (no
41
+ // bucket). The leading double-slash with an optional bucket prefix is
42
+ // matched explicitly.
43
+ final match = RegExp(r'^pb://(?:([^/]*)/)?([^/]+)/(.+)$')
44
+ .firstMatch(storageUri);
45
+ if (match == null) return null;
46
+ final bucket = match.group(1);
47
+ return (
48
+ recordId: match.group(2)!,
49
+ filename: match.group(3)!,
50
+ bucket: (bucket == null || bucket.isEmpty) ? null : bucket,
51
+ );
52
+ }
53
+
54
+ /// Stringify a PocketBase file storage URI.
55
+ String buildPocketBaseStorageUri(
56
+ String recordId,
57
+ String filename, [
58
+ String? bucket,
59
+ ]) => 'pb://${bucket ?? ''}/${recordId}/$filename';
60
+
61
+ /// PocketBase-backed [StoragePlugin].
62
+ class _PocketBaseStorage implements StoragePlugin {
63
+ _PocketBaseStorage(this.config, this.client);
64
+
65
+ final PocketBaseStorageConfig config;
66
+ final PocketBaseClient client;
67
+
68
+ @override
69
+ String get name => 'pocketbaseStorage';
70
+
71
+ @override
72
+ String get supportedProtocol => 'pb:';
73
+
74
+ @override
75
+ StoragePluginProfiles get profiles => StoragePluginProfiles(
76
+ node: _PocketBaseNodeStorage(this),
77
+ runtime: _PocketBaseRuntimeStorage(this),
78
+ );
79
+ }
80
+
81
+ /// Runtime profile: URL-oriented operations used by the device SDK
82
+ /// (resolve a signed download URL for a `pb://...` storage URI).
83
+ class _PocketBaseRuntimeStorage implements RuntimeStorageProfile {
84
+ _PocketBaseRuntimeStorage(this.parent);
85
+
86
+ final _PocketBaseStorage parent;
87
+
88
+ PocketBaseClient get _client => parent.client;
89
+ PocketBaseStorageConfig get _config => parent.config;
90
+
91
+ @override
92
+ Future<Map<String, String>> getDownloadUrl(String storageUri) async {
93
+ final parsed = parsePocketBaseStorageUri(storageUri);
94
+ if (parsed == null) {
95
+ throw FormatException('Not a pb:// URI: $storageUri');
96
+ }
97
+ // PocketBase file downloads require a short-lived token when the
98
+ // collection's file rules restrict public reads. The server can mint one
99
+ // via `POST /api/files/token` with admin auth.
100
+ final token = await _client.getFileToken(parsed.recordId, parsed.filename);
101
+ final url = _client.fileUrl(
102
+ _config.bundlesCollection,
103
+ parsed.recordId,
104
+ parsed.filename,
105
+ token.isEmpty ? null : token,
106
+ );
107
+ return {'fileUrl': url, 'storageUri': storageUri};
108
+ }
109
+
110
+ @override
111
+ Future<String?> readText(String storageUri) async {
112
+ // PB serves binary files only; text reads aren't supported.
113
+ return null;
114
+ }
115
+ }
116
+
117
+ /// Node profile: filesystem-aware operations.
118
+ class _PocketBaseNodeStorage implements NodeStorageProfile {
119
+ _PocketBaseNodeStorage(this.parent);
120
+
121
+ final _PocketBaseStorage parent;
122
+
123
+ PocketBaseClient get _client => parent.client;
124
+ PocketBaseStorageConfig get _config => parent.config;
125
+
126
+ String _keyToFilename(String key) =>
127
+ key.replaceAll('/', '_').replaceAll('\\', '_');
128
+
129
+ String _baseKey() {
130
+ final bp = _config.basePath;
131
+ return (bp == null || bp.isEmpty) ? '' : '$bp/';
132
+ }
133
+
134
+ String _fullKey(String key) => '${_baseKey()}$key';
135
+
136
+ @override
137
+ Future<Map<String, String>> upload(String key, String filePath) async {
138
+ final file = File(filePath);
139
+ if (!await file.exists()) {
140
+ throw FileSystemException('Artifact not found', filePath);
141
+ }
142
+ final bytes = await file.readAsBytes();
143
+ final filename = _keyToFilename(_fullKey(key));
144
+ // PocketBase file storage attaches files to records; use the storage key
145
+ // itself as the record id so callers can pass the bundle id (which the
146
+ // CLI already does) and find the file back via `exists()`.
147
+ final recordId = key;
148
+ final existing = await _client.recordExists(
149
+ _config.bundlesCollection,
150
+ recordId,
151
+ );
152
+ if (!existing) {
153
+ await _client.createRecord<dynamic>(_config.bundlesCollection, {
154
+ 'id': recordId,
155
+ 'channel': 'pending',
156
+ 'platform': 'android',
157
+ 'enabled': false,
158
+ 'should_force_update': false,
159
+ 'file_hash': '',
160
+ 'storage_uri': '',
161
+ 'rollout_cohort_count': 1000,
162
+ }, (j) => j);
163
+ }
164
+ await _client.uploadFile<dynamic>(
165
+ _config.bundlesCollection,
166
+ recordId,
167
+ 'artifact',
168
+ filename,
169
+ bytes,
170
+ fromJson: (j) => j,
171
+ );
172
+ final uri = buildPocketBaseStorageUri(recordId, filename);
173
+ return {'key': _fullKey(key), 'storageUri': uri};
174
+ }
175
+
176
+ @override
177
+ Future<bool> exists(String storageUri) async {
178
+ final parsed = parsePocketBaseStorageUri(storageUri);
179
+ if (parsed == null) return false;
180
+ return _client.recordExists(_config.bundlesCollection, parsed.recordId);
181
+ }
182
+
183
+ @override
184
+ Future<void> delete(String storageUri) async {
185
+ final parsed = parsePocketBaseStorageUri(storageUri);
186
+ if (parsed == null) return;
187
+ await _client.deleteRecord(_config.bundlesCollection, parsed.recordId);
188
+ }
189
+
190
+ @override
191
+ Future<void> downloadFile(String storageUri, String filePath) async {
192
+ final parsed = parsePocketBaseStorageUri(storageUri);
193
+ if (parsed == null) {
194
+ throw FormatException('Not a pb:// URI: $storageUri');
195
+ }
196
+ final token = await _client.getFileToken(parsed.recordId, parsed.filename);
197
+ final url = _client.fileUrl(
198
+ _config.bundlesCollection,
199
+ parsed.recordId,
200
+ parsed.filename,
201
+ token.isEmpty ? null : token,
202
+ );
203
+ final bytes = await _client.downloadFile(url);
204
+ final out = File(filePath);
205
+ await out.parent.create(recursive: true);
206
+ await out.writeAsBytes(bytes);
207
+ }
208
+
209
+ @override
210
+ Future<List<StorageObject>> listObjects([String? prefix]) async {
211
+ final res = await _client.listRecords<dynamic>(
212
+ _config.bundlesCollection,
213
+ (j) => j,
214
+ perPage: 500,
215
+ );
216
+ final all = res.items.cast<Map>();
217
+ return all
218
+ .map((j) {
219
+ final id = j['id']?.toString() ?? '';
220
+ final filename = j['artifact']?.toString() ?? '';
221
+ if (filename.isEmpty) return null;
222
+ final key = filename.replaceFirst(_baseKey(), '');
223
+ if (prefix != null && prefix.isNotEmpty && !key.startsWith(prefix)) {
224
+ return null;
225
+ }
226
+ return StorageObject(
227
+ key: key,
228
+ storageUri: buildPocketBaseStorageUri(id, filename),
229
+ size: 0,
230
+ );
231
+ })
232
+ .whereType<StorageObject>()
233
+ .toList();
234
+ }
235
+
236
+ @override
237
+ Future<void> deleteObjects(List<String> keys) async {
238
+ for (final key in keys) {
239
+ await _client.deleteRecord(_config.bundlesCollection, key);
240
+ }
241
+ }
242
+ }
243
+
244
+ /// Build a `pocketbaseStorage` plugin.
245
+ StoragePlugin pocketbaseStorage(PocketBaseStorageConfig config) {
246
+ final client = config.clientFactory != null
247
+ ? config.clientFactory!(
248
+ config.url,
249
+ config.adminEmail,
250
+ config.adminPassword,
251
+ )
252
+ : PocketBaseClient(config.url);
253
+ client.adminCredentials(config.adminEmail, config.adminPassword);
254
+ return _PocketBaseStorage(config, client);
255
+ }
@@ -0,0 +1,26 @@
1
+ name: flutter_ota_kit_pocketbase
2
+ description: PocketBase plugin for flutter_ota_kit — local-first self-hostable backend with admin UI, auth, file storage, and realtime.
3
+ version: 0.1.0
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
8
+ license: MIT
9
+
10
+ environment:
11
+ sdk: ">=3.13.2 <4.0.0"
12
+
13
+ dependencies:
14
+ flutter_ota_kit_core: ^0.1.1
15
+ flutter_ota_kit_plugin_core: ^0.0.3
16
+ http: ^1.2.0
17
+
18
+ dev_dependencies:
19
+ test: ^1.25.0
20
+ lints: ^4.0.0
21
+
22
+ dependency_overrides:
23
+ flutter_ota_kit_core:
24
+ path: ../../packages/core
25
+ flutter_ota_kit_plugin_core:
26
+ path: ../plugin-core
@@ -0,0 +1,12 @@
1
+ import 'package:flutter_ota_kit_pocketbase/flutter_ota_kit_pocketbase.dart';
2
+ import 'package:test/test.dart';
3
+
4
+ void main() {
5
+ test('buildPocketBaseStorageUri with no bucket parses back', () {
6
+ final uri = buildPocketBaseStorageUri('abc', 'file.zip');
7
+ final parsed = parsePocketBaseStorageUri(uri);
8
+ expect(parsed, isNotNull);
9
+ expect(parsed!.recordId, 'abc');
10
+ expect(parsed.filename, 'file.zip');
11
+ });
12
+ }
@@ -1,3 +1,9 @@
1
+ ## 0.1.1
2
+
3
+ - Add `PostgresBundleRow` + `mapRowToBundle` exports for writing custom
4
+ server tooling on top of `flutter_ota_kit`.
5
+ - Repository moved to `https://github.com/HYPER12755/flutter-ota-kit`.
6
+
1
7
  ## 0.1.0
2
8
 
3
9
  - Initial release of `flutter_ota_kit_postgres` as part of the flutter_ota_kit OTA toolkit.
@@ -0,0 +1,67 @@
1
+ // flutter_ota_kit_postgres example
2
+ //
3
+ // `flutter_ota_kit_postgres` is a Postgres backend for `flutter_ota_kit`.
4
+ // It provides a `postgresDatabase` factory and a `postgresStorage`
5
+ // factory that talk to a vanilla Postgres database (no Supabase, no
6
+ // Cloudflare — just `pg`).
7
+ //
8
+ // Most apps don't import this package directly; they wire it through
9
+ // `flutter_ota_kit.configurePostgres(...)` in their app boot path. This
10
+ // file shows the low-level config types in case you're writing a custom
11
+ // server, a migration script, or a CLI extension.
12
+ //
13
+ // Run: `dart run example/main.dart`
14
+
15
+ import 'package:flutter_ota_kit_postgres/flutter_ota_kit_postgres.dart';
16
+ import 'package:flutter_ota_kit_core/flutter_ota_kit_core.dart';
17
+
18
+ void main() {
19
+ // ── 1. Database config (vanilla Postgres connection). ─────────────
20
+ // `PostgresConfig` is a thin wrapper around the `postgres` package
21
+ // connection parameters. The `host`/`port`/`database` triple is the
22
+ // canonical form; username/password are required for non-trust auth.
23
+ const dbConfig = PostgresConfig(
24
+ host: 'db.example.com',
25
+ port: 5432,
26
+ database: 'ota',
27
+ username: 'ota',
28
+ password: 'secret',
29
+ );
30
+ print('Postgres: ${dbConfig.host}:${dbConfig.port}/${dbConfig.database}');
31
+
32
+ // ── 2. Storage config (for storing patch blobs). ──────────────────
33
+ // The `servingBaseUrl` is the public-facing URL that the device SDK
34
+ // can use to download patches. Omit it only if you proxy the
35
+ // download through your own update-check endpoint.
36
+ const storageConfig = PostgresStorageConfig(
37
+ db: dbConfig,
38
+ servingBaseUrl: 'https://cdn.example.com/flutter-ota',
39
+ );
40
+ print('Serving base URL: ${storageConfig.servingBaseUrl}');
41
+
42
+ // ── 3. Row-to-Bundle mapping (server-side helper). ────────────────
43
+ // Use this when reading bundles from a custom query and passing them
44
+ // to the SDK's `PatchInfo.fromJson`. The mapper handles both
45
+ // camelCase and snake_case column names.
46
+ const row = PostgresBundleRow(
47
+ id: '01a059a6-7d3c-4f3e-8a2c-9c2e3a4b5d6e',
48
+ channel: 'production',
49
+ enabled: true,
50
+ shouldForceUpdate: false,
51
+ fileHash: '7c4a8d09ca3762af61e59520943dc26494f8941b',
52
+ platform: Platform.android,
53
+ storageUri:
54
+ 'postgres-blob://flutter-ota-bundles/'
55
+ '01a059a6/patch.zip',
56
+ targetAppVersion: '1.0.0',
57
+ rolloutCohortCount: 100,
58
+ );
59
+ final bundle = mapRowToBundle(row);
60
+ print('Mapped bundle: ${bundle.id} (${bundle.storageUri})');
61
+
62
+ // ── 4. Bundle → SQL values (for inserts/updates). ────────────────
63
+ // Returns snake_case column→value maps ready to feed to a `pg` query.
64
+ final values = bundleToRowValues(bundle);
65
+ print('SQL column count: ${values.length}');
66
+ print(' file_hash = ${values['file_hash']}');
67
+ }
@@ -3,15 +3,12 @@
3
3
  /// Faithful port of hot-updater `plugins/postgres`.
4
4
  library;
5
5
 
6
- export 'src/postgres_config.dart'
7
- show PostgresConfig, PostgresClientFactory;
8
- export 'src/postgres_client.dart'
9
- show PostgresClientLike, PostgresClient;
6
+ export 'src/postgres_config.dart' show PostgresConfig, PostgresClientFactory;
7
+ export 'src/postgres_client.dart' show PostgresClientLike, PostgresClient;
10
8
  export 'src/postgres_database.dart' show postgresDatabase;
11
9
  export 'src/postgres_storage.dart'
12
10
  show postgresStorage, PostgresStorageConfig, resolvePostgresStorageClient;
13
11
  export 'src/postgres_get_update_info.dart' show getUpdateInfo;
14
- export 'src/postgres_types.dart'
15
- show PostgresBundleRow, PostgresBundlePatchRow;
12
+ export 'src/postgres_types.dart' show PostgresBundleRow, PostgresBundlePatchRow;
16
13
  export 'src/postgres_bundle_mapper.dart'
17
14
  show mapRowToBundle, bundleToRowValues, bundleToPatchRows;
@@ -2,8 +2,6 @@
2
2
  /// (the `mapRowToBundle` / `bundleToRowValues` / `bundleToPatchRows` helpers).
3
3
  library;
4
4
 
5
- import 'dart:convert';
6
-
7
5
  import 'package:flutter_ota_kit_core/flutter_ota_kit_core.dart'
8
6
  show
9
7
  Bundle,
@@ -16,36 +14,17 @@ import 'package:flutter_ota_kit_core/flutter_ota_kit_core.dart'
16
14
  getManifestStorageUri,
17
15
  stripBundleArtifactMetadata;
18
16
 
19
- import 'postgres_types.dart';
20
-
21
- /// Normalize metadata from various shapes to a plain map.
22
- Map<String, Object?>? _normalizeMetadata(Object? value) {
23
- if (value == null) return null;
24
-
25
- if (value is String) {
26
- try {
27
- final parsed = jsonDecode(value);
28
- return _normalizeMetadata(parsed);
29
- } catch (_) {
30
- return null;
31
- }
32
- }
17
+ import 'package:flutter_ota_kit_plugin_core/flutter_ota_kit_plugin_core.dart'
18
+ show buildBundlePatchId, normalizeMetadata;
33
19
 
34
- if (value is Map) return value.cast<String, Object?>();
35
-
36
- return null;
37
- }
38
-
39
- /// Build the compound primary key for a patch row.
40
- String _buildBundlePatchId(String bundleId, String baseBundleId) =>
41
- '$bundleId:$baseBundleId';
20
+ import 'postgres_types.dart';
42
21
 
43
22
  /// Map a Postgres bundle row + optional patch rows to a [Bundle].
44
23
  Bundle mapRowToBundle(
45
24
  PostgresBundleRow data, [
46
25
  List<PostgresBundlePatchRow> patchRows = const [],
47
26
  ]) {
48
- final rawMetadata = _normalizeMetadata(data.metadata);
27
+ final rawMetadata = normalizeMetadata(data.metadata);
49
28
  final patches = patchRows.toList()
50
29
  ..sort((a, b) {
51
30
  final cmp = a.orderIndex.compareTo(b.orderIndex);
@@ -54,12 +33,14 @@ Bundle mapRowToBundle(
54
33
  });
55
34
 
56
35
  final patchArtifacts = patches
57
- .map((p) => BundlePatchArtifact(
58
- baseBundleId: p.baseBundleId,
59
- baseFileHash: p.baseFileHash,
60
- patchFileHash: p.patchFileHash,
61
- patchStorageUri: p.patchStorageUri,
62
- ))
36
+ .map(
37
+ (p) => BundlePatchArtifact(
38
+ baseBundleId: p.baseBundleId,
39
+ baseFileHash: p.baseFileHash,
40
+ patchFileHash: p.patchFileHash,
41
+ patchStorageUri: p.patchStorageUri,
42
+ ),
43
+ )
63
44
  .toList();
64
45
 
65
46
  final primaryPatch = patchArtifacts.isNotEmpty ? patchArtifacts.first : null;
@@ -76,8 +57,7 @@ Bundle mapRowToBundle(
76
57
  targetAppVersion: data.targetAppVersion,
77
58
  fingerprintHash: data.fingerprintHash,
78
59
  storageUri: data.storageUri,
79
- metadata:
80
- rawMetadata != null ? BundleMetadata.fromJson(rawMetadata) : null,
60
+ metadata: rawMetadata != null ? BundleMetadata.fromJson(rawMetadata) : null,
81
61
  manifestStorageUri: data.manifestStorageUri,
82
62
  manifestFileHash: data.manifestFileHash,
83
63
  assetBaseStorageUri: data.assetBaseStorageUri,
@@ -93,24 +73,24 @@ Bundle mapRowToBundle(
93
73
 
94
74
  /// Map a [Bundle] to a `bundles` row for insert/update (snake_case columns).
95
75
  Map<String, dynamic> bundleToRowValues(Bundle bundle) => {
96
- 'id': bundle.id,
97
- 'enabled': bundle.enabled,
98
- 'should_force_update': bundle.shouldForceUpdate,
99
- 'file_hash': bundle.fileHash,
100
- 'git_commit_hash': bundle.gitCommitHash,
101
- 'message': bundle.message,
102
- 'platform': bundle.platform.value,
103
- 'target_app_version': bundle.targetAppVersion,
104
- 'channel': bundle.channel,
105
- 'storage_uri': bundle.storageUri,
106
- 'fingerprint_hash': bundle.fingerprintHash,
107
- 'metadata': stripBundleArtifactMetadata(bundle.metadata?.toJson()) ?? {},
108
- 'manifest_storage_uri': getManifestStorageUri(bundle),
109
- 'manifest_file_hash': getManifestFileHash(bundle),
110
- 'asset_base_storage_uri': getAssetBaseStorageUri(bundle),
111
- 'rollout_cohort_count': bundle.rolloutCohortCount,
112
- 'target_cohorts': bundle.targetCohorts,
113
- };
76
+ 'id': bundle.id,
77
+ 'enabled': bundle.enabled,
78
+ 'should_force_update': bundle.shouldForceUpdate,
79
+ 'file_hash': bundle.fileHash,
80
+ 'git_commit_hash': bundle.gitCommitHash,
81
+ 'message': bundle.message,
82
+ 'platform': bundle.platform.value,
83
+ 'target_app_version': bundle.targetAppVersion,
84
+ 'channel': bundle.channel,
85
+ 'storage_uri': bundle.storageUri,
86
+ 'fingerprint_hash': bundle.fingerprintHash,
87
+ 'metadata': stripBundleArtifactMetadata(bundle.metadata?.toJson()) ?? {},
88
+ 'manifest_storage_uri': getManifestStorageUri(bundle),
89
+ 'manifest_file_hash': getManifestFileHash(bundle),
90
+ 'asset_base_storage_uri': getAssetBaseStorageUri(bundle),
91
+ 'rollout_cohort_count': bundle.rolloutCohortCount,
92
+ 'target_cohorts': bundle.targetCohorts,
93
+ };
114
94
 
115
95
  /// Map a [Bundle] to its `bundle_patches` rows for insert.
116
96
  List<Map<String, dynamic>> bundleToPatchRows(Bundle bundle) {
@@ -118,7 +98,7 @@ List<Map<String, dynamic>> bundleToPatchRows(Bundle bundle) {
118
98
  return [
119
99
  for (var i = 0; i < patchArtifacts.length; i++)
120
100
  PostgresBundlePatchRow(
121
- id: _buildBundlePatchId(bundle.id, patchArtifacts[i].baseBundleId),
101
+ id: buildBundlePatchId(bundle.id, patchArtifacts[i].baseBundleId),
122
102
  bundleId: bundle.id,
123
103
  baseBundleId: patchArtifacts[i].baseBundleId,
124
104
  baseFileHash: patchArtifacts[i].baseFileHash,
@@ -32,4 +32,6 @@ class PostgresConfig {
32
32
  }
33
33
 
34
34
  /// Builds a [PostgresClientLike] from a [PostgresConfig].
35
- typedef PostgresClientFactory = PostgresClientLike Function(PostgresConfig config);
35
+ typedef PostgresClientFactory = PostgresClientLike Function(
36
+ PostgresConfig config,
37
+ );
@@ -33,7 +33,8 @@ import 'postgres_types.dart';
33
33
  DatabasePlugin Function() Function(
34
34
  PostgresConfig config, [
35
35
  DatabasePluginHooks? hooks,
36
- ]) postgresDatabase = createDatabasePlugin<PostgresConfig>(
36
+ ])
37
+ postgresDatabase = createDatabasePlugin<PostgresConfig>(
37
38
  name: 'postgresDatabase',
38
39
  factory: (config) {
39
40
  final client = config.clientFactory != null
@@ -95,18 +96,10 @@ DatabasePlugin Function() Function(
95
96
  add('enabled = ?', where.enabled);
96
97
  }
97
98
  if (where.fingerprintHash != null) {
98
- if (where.fingerprintHash == null) {
99
- conditions.add('fingerprint_hash IS NULL');
100
- } else {
101
- add('fingerprint_hash = ?', where.fingerprintHash);
102
- }
99
+ add('fingerprint_hash = ?', where.fingerprintHash);
103
100
  }
104
101
  if (where.targetAppVersion != null) {
105
- if (where.targetAppVersion == null) {
106
- conditions.add('target_app_version IS NULL');
107
- } else {
108
- add('target_app_version = ?', where.targetAppVersion);
109
- }
102
+ add('target_app_version = ?', where.targetAppVersion);
110
103
  }
111
104
  if (where.targetAppVersionIn != null) {
112
105
  add('target_app_version = ANY(?)', where.targetAppVersionIn);
@@ -134,12 +127,10 @@ class _PostgresDatabasePlugin implements AbstractDatabasePlugin {
134
127
  final PostgresClientLike client;
135
128
  final Future<Map<String, List<PostgresBundlePatchRow>>> Function(
136
129
  List<String> bundleIds,
137
- ) fetchPatchMap;
130
+ )
131
+ fetchPatchMap;
138
132
 
139
- _PostgresDatabasePlugin({
140
- required this.client,
141
- required this.fetchPatchMap,
142
- });
133
+ _PostgresDatabasePlugin({required this.client, required this.fetchPatchMap});
143
134
 
144
135
  @override
145
136
  bool get supportsCursorPagination => false;
@@ -151,17 +142,16 @@ class _PostgresDatabasePlugin implements AbstractDatabasePlugin {
151
142
  @override
152
143
  Future<Bundle?> getBundleById(String bundleId) async {
153
144
  final results = await Future.wait([
154
- client.execute(
155
- 'SELECT * FROM bundles WHERE id = @p0',
156
- {'p0': bundleId},
157
- ),
145
+ client.execute('SELECT * FROM bundles WHERE id = @p0', {'p0': bundleId}),
158
146
  fetchPatchMap([bundleId]),
159
147
  ]);
160
148
 
161
149
  final data = results[0] as List<Map<String, dynamic>>;
162
150
  if (data.isEmpty) return null;
163
151
  final row = PostgresBundleRow.fromJson(data.first);
164
- final patches = (results[1] as Map<String, List<PostgresBundlePatchRow>>)[bundleId] ?? [];
152
+ final patches =
153
+ (results[1] as Map<String, List<PostgresBundlePatchRow>>)[bundleId] ??
154
+ [];
165
155
  return mapRowToBundle(row, patches);
166
156
  }
167
157
 
@@ -174,9 +164,11 @@ class _PostgresDatabasePlugin implements AbstractDatabasePlugin {
174
164
  final orderBy =
175
165
  options.orderBy ?? const DatabaseBundleQueryOrder(direction: 'desc');
176
166
  final offset =
177
- options.offset ?? (options.page != null ? (options.page! - 1) * limit : 0);
167
+ options.offset ??
168
+ (options.page != null ? (options.page! - 1) * limit : 0);
178
169
 
179
- if ((where?.targetAppVersionIn != null && where!.targetAppVersionIn!.isEmpty) ||
170
+ if ((where?.targetAppVersionIn != null &&
171
+ where!.targetAppVersionIn!.isEmpty) ||
180
172
  (where?.id?.ins != null && where!.id!.ins!.isEmpty)) {
181
173
  return Paginated(
182
174
  data: const [],
@@ -230,18 +222,15 @@ class _PostgresDatabasePlugin implements AbstractDatabasePlugin {
230
222
  }
231
223
 
232
224
  @override
233
- Future<void> commitBundle({
234
- required List<BundleChange> changedSets,
235
- }) async {
225
+ Future<void> commitBundle({required List<BundleChange> changedSets}) async {
236
226
  if (changedSets.isEmpty) return;
237
227
 
238
228
  await client.runTx((tx) async {
239
229
  for (final op in changedSets) {
240
230
  if (op.operation == BundleChangeOperation.delete) {
241
- await tx.execute(
242
- 'DELETE FROM bundle_patches WHERE bundle_id = @p0',
243
- {'p0': op.data.id},
244
- );
231
+ await tx.execute('DELETE FROM bundle_patches WHERE bundle_id = @p0', {
232
+ 'p0': op.data.id,
233
+ });
245
234
  await tx.execute(
246
235
  'DELETE FROM bundle_patches WHERE base_bundle_id = @p0',
247
236
  {'p0': op.data.id},
@@ -258,8 +247,9 @@ class _PostgresDatabasePlugin implements AbstractDatabasePlugin {
258
247
  final values = bundleToRowValues(bundle);
259
248
  final columns = values.keys.toList();
260
249
  final insertCols = columns.join(', ');
261
- final placeholders =
262
- columns.map((c) => '@${c.replaceAll('.', '_')}').join(', ');
250
+ final placeholders = columns
251
+ .map((c) => '@${c.replaceAll('.', '_')}')
252
+ .join(', ');
263
253
  final updateSets = columns
264
254
  .where((c) => c != 'id')
265
255
  .map((c) => '$c = EXCLUDED.$c')
@@ -270,17 +260,17 @@ class _PostgresDatabasePlugin implements AbstractDatabasePlugin {
270
260
  {for (final c in columns) c.replaceAll('.', '_'): values[c]},
271
261
  );
272
262
 
273
- await tx.execute(
274
- 'DELETE FROM bundle_patches WHERE bundle_id = @p0',
275
- {'p0': bundle.id},
276
- );
263
+ await tx.execute('DELETE FROM bundle_patches WHERE bundle_id = @p0', {
264
+ 'p0': bundle.id,
265
+ });
277
266
  final patchRows = bundleToPatchRows(bundle);
278
267
  if (patchRows.isNotEmpty) {
279
268
  for (final pr in patchRows) {
280
269
  final patchCols = pr.keys.toList();
281
270
  final patchInsert = patchCols.join(', ');
282
- final patchPlaceholders =
283
- patchCols.map((c) => '@${c.replaceAll('.', '_')}').join(', ');
271
+ final patchPlaceholders = patchCols
272
+ .map((c) => '@${c.replaceAll('.', '_')}')
273
+ .join(', ');
284
274
  await tx.execute(
285
275
  'INSERT INTO bundle_patches ($patchInsert) '
286
276
  'VALUES ($patchPlaceholders) '