@_nazmiforreal/flutter-ota 0.1.12 → 0.1.14

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 (177) 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 +290 -181
  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/CHANGELOG.md +9 -0
  48. package/dart-src/packages/core/README.md +2 -2
  49. package/dart-src/packages/core/example/lib/main.dart +69 -0
  50. package/dart-src/packages/core/lib/flutter_ota_kit_core.dart +1 -1
  51. package/dart-src/packages/core/lib/src/app_update_info.dart +17 -15
  52. package/dart-src/packages/core/lib/src/bundle.dart +102 -94
  53. package/dart-src/packages/core/lib/src/bundle_artifacts.dart +1 -2
  54. package/dart-src/packages/core/lib/src/bundle_patch_artifact.dart +15 -16
  55. package/dart-src/packages/core/lib/src/changed_asset.dart +24 -22
  56. package/dart-src/packages/core/lib/src/get_bundles_args.dart +13 -13
  57. package/dart-src/packages/core/lib/src/metadata.dart +9 -7
  58. package/dart-src/packages/core/lib/src/patch_info.dart +8 -7
  59. package/dart-src/packages/core/lib/src/platform.dart +4 -3
  60. package/dart-src/packages/core/lib/src/rollout.dart +14 -11
  61. package/dart-src/packages/core/lib/src/semver_range.dart +49 -39
  62. package/dart-src/packages/core/lib/src/semver_version.dart +8 -7
  63. package/dart-src/packages/core/lib/src/status.dart +4 -3
  64. package/dart-src/packages/core/lib/src/update_bundle_params.dart +7 -7
  65. package/dart-src/packages/core/lib/src/uuid.dart +1 -2
  66. package/dart-src/packages/core/pubspec.yaml +6 -7
  67. package/dart-src/packages/core/test/rollout_test.dart +15 -8
  68. package/dart-src/packages/core/test/semver_test.dart +5 -2
  69. package/dart-src/packages/core/test/types_test.dart +26 -24
  70. package/dart-src/packages/core/test/uuid_test.dart +3 -1
  71. package/dart-src/packages/core/tool/dbg.dart +1 -0
  72. package/dart-src/plugins/aws/CHANGELOG.md +15 -0
  73. package/dart-src/plugins/aws/example/lib/main.dart +60 -0
  74. package/dart-src/plugins/aws/lib/flutter_ota_kit_aws.dart +5 -1
  75. package/dart-src/plugins/aws/lib/src/aws_cloudfront_client.dart +11 -16
  76. package/dart-src/plugins/aws/lib/src/aws_cloudfront_signer.dart +7 -2
  77. package/dart-src/plugins/aws/lib/src/aws_database.dart +14 -18
  78. package/dart-src/plugins/aws/lib/src/aws_s3_client.dart +23 -18
  79. package/dart-src/plugins/aws/lib/src/aws_ssm_client.dart +5 -4
  80. package/dart-src/plugins/aws/lib/src/aws_storage.dart +2 -3
  81. package/dart-src/plugins/aws/lib/src/aws_storage_profile.dart +10 -16
  82. package/dart-src/plugins/aws/lib/src/with_cloudfront_signed_url.dart +6 -9
  83. package/dart-src/plugins/aws/pubspec.yaml +15 -10
  84. package/dart-src/plugins/aws/test/aws_database_test.dart +51 -57
  85. package/dart-src/plugins/aws/test/mock_aws_s3_client.dart +8 -13
  86. package/dart-src/plugins/cloudflare/CHANGELOG.md +15 -0
  87. package/dart-src/plugins/cloudflare/example/lib/main.dart +62 -0
  88. package/dart-src/plugins/cloudflare/lib/flutter_ota_kit_cloudflare.dart +4 -2
  89. package/dart-src/plugins/cloudflare/lib/src/cloudflare_worker_database.dart +3 -3
  90. package/dart-src/plugins/cloudflare/lib/src/d1_build_where.dart +3 -2
  91. package/dart-src/plugins/cloudflare/lib/src/d1_bundle_mapper.dart +24 -27
  92. package/dart-src/plugins/cloudflare/lib/src/d1_database_plugin.dart +22 -30
  93. package/dart-src/plugins/cloudflare/lib/src/r2_config.dart +1 -1
  94. package/dart-src/plugins/cloudflare/lib/src/r2_s3_client.dart +21 -29
  95. package/dart-src/plugins/cloudflare/lib/src/r2_storage.dart +7 -7
  96. package/dart-src/plugins/cloudflare/lib/src/r2_storage_profile.dart +7 -15
  97. package/dart-src/plugins/cloudflare/pubspec.yaml +14 -9
  98. package/dart-src/plugins/cloudflare/test/cloudflare_worker_database_test.dart +46 -42
  99. package/dart-src/plugins/cloudflare/test/d1_database_test.dart +100 -90
  100. package/dart-src/plugins/cloudflare/test/mock_d1_client.dart +23 -18
  101. package/dart-src/plugins/cloudflare/test/mock_r2_client.dart +8 -13
  102. package/dart-src/plugins/cloudflare/test/r2_storage_test.dart +1 -4
  103. package/dart-src/plugins/plugin-core/CHANGELOG.md +15 -1
  104. package/dart-src/plugins/plugin-core/example/lib/main.dart +61 -0
  105. package/dart-src/plugins/plugin-core/lib/flutter_ota_kit_plugin_core.dart +3 -7
  106. package/dart-src/plugins/plugin-core/lib/src/asset_storage_layout.dart +5 -5
  107. package/dart-src/plugins/plugin-core/lib/src/bundle_helpers.dart +23 -0
  108. package/dart-src/plugins/plugin-core/lib/src/bundle_storage_layout.dart +16 -10
  109. package/dart-src/plugins/plugin-core/lib/src/bundle_unit_of_work.dart +31 -26
  110. package/dart-src/plugins/plugin-core/lib/src/calculate_pagination.dart +5 -1
  111. package/dart-src/plugins/plugin-core/lib/src/content_addressed_assets.dart +1 -1
  112. package/dart-src/plugins/plugin-core/lib/src/create_blob_database_plugin.dart +78 -71
  113. package/dart-src/plugins/plugin-core/lib/src/create_database_plugin.dart +56 -35
  114. package/dart-src/plugins/plugin-core/lib/src/create_storage_plugin.dart +10 -13
  115. package/dart-src/plugins/plugin-core/lib/src/paginate_bundles.dart +30 -17
  116. package/dart-src/plugins/plugin-core/lib/src/parse_storage_uri.dart +3 -1
  117. package/dart-src/plugins/plugin-core/lib/src/query_bundles.dart +5 -3
  118. package/dart-src/plugins/plugin-core/lib/src/request_update_bundle_state.dart +2 -4
  119. package/dart-src/plugins/plugin-core/lib/src/storage_helpers.dart +18 -0
  120. package/dart-src/plugins/plugin-core/lib/src/types.dart +7 -6
  121. package/dart-src/plugins/plugin-core/pubspec.yaml +12 -9
  122. package/dart-src/plugins/plugin-core/test/asset_storage_layout_test.dart +13 -9
  123. package/dart-src/plugins/plugin-core/test/bundle_unit_of_work_test.dart +28 -15
  124. package/dart-src/plugins/plugin-core/test/create_blob_database_plugin_test.dart +254 -122
  125. package/dart-src/plugins/plugin-core/test/create_database_plugin_test.dart +36 -32
  126. package/dart-src/plugins/plugin-core/test/create_storage_plugin_test.dart +18 -15
  127. package/dart-src/plugins/plugin-core/test/generate_min_bundle_id_test.dart +8 -3
  128. package/dart-src/plugins/plugin-core/test/get_update_info_test.dart +682 -693
  129. package/dart-src/plugins/plugin-core/test/helpers_test.dart +45 -21
  130. package/dart-src/plugins/plugin-core/test/query_bundles_test.dart +76 -13
  131. package/dart-src/plugins/plugin-core/test/uuidv7_test.dart +20 -9
  132. package/dart-src/plugins/pocketbase/CHANGELOG.md +8 -0
  133. package/dart-src/plugins/pocketbase/LICENSE +21 -0
  134. package/dart-src/plugins/pocketbase/README.md +54 -0
  135. package/dart-src/plugins/pocketbase/example/lib/main.dart +62 -0
  136. package/dart-src/plugins/pocketbase/lib/flutter_ota_kit_pocketbase.dart +28 -0
  137. package/dart-src/plugins/pocketbase/lib/src/pocketbase_bundle_mapper.dart +121 -0
  138. package/dart-src/plugins/pocketbase/lib/src/pocketbase_client.dart +349 -0
  139. package/dart-src/plugins/pocketbase/lib/src/pocketbase_config.dart +61 -0
  140. package/dart-src/plugins/pocketbase/lib/src/pocketbase_database.dart +278 -0
  141. package/dart-src/plugins/pocketbase/lib/src/pocketbase_storage.dart +255 -0
  142. package/dart-src/plugins/pocketbase/pubspec.yaml +26 -0
  143. package/dart-src/plugins/pocketbase/test/pb_uri_test.dart +12 -0
  144. package/dart-src/plugins/postgres/CHANGELOG.md +16 -0
  145. package/dart-src/plugins/postgres/example/lib/main.dart +67 -0
  146. package/dart-src/plugins/postgres/lib/flutter_ota_kit_postgres.dart +3 -6
  147. package/dart-src/plugins/postgres/lib/src/postgres_bundle_mapper.dart +32 -52
  148. package/dart-src/plugins/postgres/lib/src/postgres_config.dart +3 -1
  149. package/dart-src/plugins/postgres/lib/src/postgres_database.dart +28 -38
  150. package/dart-src/plugins/postgres/lib/src/postgres_get_update_info.dart +16 -14
  151. package/dart-src/plugins/postgres/lib/src/postgres_storage.dart +26 -14
  152. package/dart-src/plugins/postgres/lib/src/postgres_types.dart +27 -28
  153. package/dart-src/plugins/postgres/pubspec.yaml +14 -9
  154. package/dart-src/plugins/postgres/test/mock_postgres_client.dart +62 -39
  155. package/dart-src/plugins/postgres/test/postgres_bundle_mapper_test.dart +3 -4
  156. package/dart-src/plugins/postgres/test/postgres_database_test.dart +102 -73
  157. package/dart-src/plugins/postgres/test/postgres_storage_runtime_test.dart +60 -0
  158. package/dart-src/plugins/supabase/CHANGELOG.md +20 -0
  159. package/dart-src/plugins/supabase/README.md +2 -2
  160. package/dart-src/plugins/supabase/example/lib/main.dart +63 -0
  161. package/dart-src/plugins/supabase/lib/flutter_ota_kit_supabase.dart +2 -0
  162. package/dart-src/plugins/supabase/lib/src/error_message.dart +11 -0
  163. package/dart-src/plugins/supabase/lib/src/supabase_bundle_mapper.dart +34 -58
  164. package/dart-src/plugins/supabase/lib/src/supabase_client_adapter.dart +3 -13
  165. package/dart-src/plugins/supabase/lib/src/supabase_client_http.dart +34 -52
  166. package/dart-src/plugins/supabase/lib/src/supabase_database.dart +38 -30
  167. package/dart-src/plugins/supabase/lib/src/supabase_edge_function_storage.dart +21 -29
  168. package/dart-src/plugins/supabase/lib/src/supabase_signed_url_batcher.dart +7 -19
  169. package/dart-src/plugins/supabase/lib/src/supabase_storage.dart +18 -29
  170. package/dart-src/plugins/supabase/lib/src/types.dart +26 -26
  171. package/dart-src/plugins/supabase/pubspec.yaml +14 -9
  172. package/dart-src/plugins/supabase/test/mock_supabase_client.dart +57 -57
  173. package/dart-src/plugins/supabase/test/supabase_bundle_mapper_test.dart +30 -27
  174. package/dart-src/plugins/supabase/test/supabase_database_test.dart +26 -35
  175. package/dart-src/plugins/supabase/test/supabase_storage_test.dart +27 -34
  176. package/package.json +3 -3
  177. 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.2
15
+ flutter_ota_kit_plugin_core: ^0.0.4
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,19 @@
1
+ ## 0.1.3
2
+
3
+ - Add dartdoc on internal API surfaces (10/10 pana dartdoc coverage).
4
+
5
+ ## 0.1.2
6
+
7
+ - Add `PostgresBundleRow` + `mapRowToBundle` exports for writing custom server
8
+ tooling on top of `flutter_ota_kit_postgres`.
9
+ - Repository moved to `https://github.com/HYPER12755/flutter-ota-kit`.
10
+
11
+ ## 0.1.1
12
+
13
+ - Throw a clear `StateError` from `getDownloadUrl` when no `servingBaseUrl`
14
+ is configured (previously the URL silently failed).
15
+ - Remove dead `IS NULL` filter paths in `_buildWhere`.
16
+
1
17
  ## 0.1.0
2
18
 
3
19
  - 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
+ );