@_nazmiforreal/flutter-ota 0.1.14 → 0.1.16

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 (39) hide show
  1. package/bin/flutter-ota-linux-x64 +0 -0
  2. package/bin/flutter-ota.js +23 -72
  3. package/dart-src/packages/cli-tools/.dart_tool/package_config.json +0 -2
  4. package/dart-src/packages/cli-tools/lib/flutter_ota_kit_cli.dart +2 -2
  5. package/dart-src/packages/cli-tools/lib/src/commands/build.dart +1 -1
  6. package/dart-src/packages/cli-tools/lib/src/commands/bundle.dart +112 -124
  7. package/dart-src/packages/cli-tools/lib/src/commands/channel.dart +20 -28
  8. package/dart-src/packages/cli-tools/lib/src/commands/config_command.dart +40 -18
  9. package/dart-src/packages/cli-tools/lib/src/commands/console.dart +10 -11
  10. package/dart-src/packages/cli-tools/lib/src/commands/deploy.dart +30 -33
  11. package/dart-src/packages/cli-tools/lib/src/commands/doctor.dart +43 -41
  12. package/dart-src/packages/cli-tools/lib/src/commands/fingerprint.dart +9 -10
  13. package/dart-src/packages/cli-tools/lib/src/commands/init.dart +32 -35
  14. package/dart-src/packages/cli-tools/lib/src/commands/keys.dart +3 -6
  15. package/dart-src/packages/cli-tools/lib/src/commands/migrate.dart +59 -50
  16. package/dart-src/packages/cli-tools/lib/src/commands/pocketbase.dart +32 -37
  17. package/dart-src/packages/cli-tools/lib/src/commands/rollback.dart +13 -16
  18. package/dart-src/packages/cli-tools/lib/src/commands/storage.dart +12 -20
  19. package/dart-src/packages/cli-tools/lib/src/config.dart +10 -3
  20. package/dart-src/packages/cli-tools/lib/src/operations.dart +5 -0
  21. package/dart-src/packages/cli-tools/lib/src/ui/ui.dart +225 -31
  22. package/dart-src/packages/cli-tools/lib/src/util.dart +7 -3
  23. package/dart-src/packages/cli-tools/test/cli_test.dart +6 -0
  24. package/dart-src/plugins/supabase/supabase/migrations/20250103114225_init.sql +7 -3
  25. package/dart-src/plugins/supabase/supabase/migrations/20250314000000_hot-updater_0.13.0.sql +7 -4
  26. package/dart-src/plugins/supabase/supabase/migrations/20250516000000_hot-updater_0.18.0.sql +18 -6
  27. package/dart-src/plugins/supabase/supabase/migrations/20251014000000_hot-updater_0.21.0.sql +4 -2
  28. package/dart-src/plugins/supabase/supabase/migrations/20260401000000_hot-updater_0.29.0.sql +4 -2
  29. package/dart-src/plugins/supabase/supabase/migrations/20260520014100_hot-updater_rls.sql +22 -6
  30. package/migrations/supabase/20250103114225_init.sql +7 -3
  31. package/migrations/supabase/20250314000000_hot-updater_0.13.0.sql +7 -4
  32. package/migrations/supabase/20250516000000_hot-updater_0.18.0.sql +18 -6
  33. package/migrations/supabase/20251014000000_hot-updater_0.21.0.sql +4 -2
  34. package/migrations/supabase/20260401000000_hot-updater_0.29.0.sql +4 -2
  35. package/migrations/supabase/20260520014100_hot-updater_rls.sql +22 -6
  36. package/package.json +1 -1
  37. package/scripts/vendor.sh +32 -0
  38. package/migrations/aws/README.md +0 -7
  39. package/migrations/cloudflare/bundles.sql +0 -42
@@ -4,12 +4,15 @@ import 'dart:async';
4
4
  import 'dart:io';
5
5
  import 'dart:math' as math;
6
6
 
7
- /// Terminal UI toolkit for the flutter_ota_kit CLI: colored text, banners,
8
- /// bordered boxes, step logs, dynamic spinners and progress bars.
7
+ /// Terminal UI toolkit for the flutter-ota CLI.
9
8
  ///
10
- /// Everything degrades gracefully to plain text when stdout is not a TTY or the
11
- /// `NO_COLOR` env var is set, so pipes/CI/tests stay clean. Boxes auto-size to
12
- /// the terminal width and wrap long content so they never render broken.
9
+ /// Provides colored text, banners, bordered boxes, step logs, spinners,
10
+ /// thin progress bars, and column-aligned tables.
11
+ ///
12
+ /// Everything degrades gracefully to plain text when stdout is not a TTY
13
+ /// or the `NO_COLOR` env var is set.
14
+
15
+ // ── ANSI codes ───────────────────────────────────────────────────────────────
13
16
 
14
17
  const _kReset = '\x1b[0m';
15
18
  const _kRed = '\x1b[31m';
@@ -23,6 +26,8 @@ const _kBold = '\x1b[1m';
23
26
 
24
27
  const List<String> _frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
25
28
 
29
+ // ── TTY detection ────────────────────────────────────────────────────────────
30
+
26
31
  bool get _noColor {
27
32
  final v = Platform.environment['NO_COLOR'];
28
33
  return v != null && v.isNotEmpty;
@@ -31,6 +36,8 @@ bool get _noColor {
31
36
  bool get _colorOn =>
32
37
  !_noColor && stdout.hasTerminal && stdout.supportsAnsiEscapes;
33
38
 
39
+ // ── Color helpers ────────────────────────────────────────────────────────────
40
+
34
41
  String _c(String code, String s) => _colorOn ? '$code$s$_kReset' : s;
35
42
 
36
43
  String red(String s) => _c(_kRed, s);
@@ -43,7 +50,19 @@ String gray(String s) => _c(_kGray, s);
43
50
  String dim(String s) => _c(_kGray, s);
44
51
  String bold(String s) => _c(_kBold, s);
45
52
 
46
- int get _cols => stdout.hasTerminal ? stdout.terminalColumns : 80;
53
+ // ── Terminal width ───────────────────────────────────────────────────────────
54
+
55
+ int get _cols {
56
+ if (stdout.hasTerminal) return stdout.terminalColumns;
57
+ final envCols = Platform.environment['COLUMNS'];
58
+ if (envCols != null) {
59
+ final parsed = int.tryParse(envCols);
60
+ if (parsed != null && parsed > 0) return parsed;
61
+ }
62
+ return 80;
63
+ }
64
+
65
+ // ── Visible width (ANSI-aware) ──────────────────────────────────────────────
47
66
 
48
67
  final _ansi = RegExp('\x1b\\[[0-9;]*m');
49
68
 
@@ -51,26 +70,38 @@ int _dispWidth(String s) => s.replaceAll(_ansi, '').length;
51
70
 
52
71
  void _echo(String s) => stdout.writeln(s);
53
72
 
73
+ // ── Key-value pair ───────────────────────────────────────────────────────────
74
+
54
75
  /// `key value` laid out in a fixed 18-char column (visible-width aware).
76
+ /// Long values are truncated with `…` to prevent box overflow.
55
77
  String kv(String k, String v) {
56
78
  final pad = math.max(0, 18 - _dispWidth(k));
57
- return '${dim(k)}${' ' * pad} $v';
79
+ final maxVal = math.max(0, _cols - 24 - _dispWidth(k));
80
+ final display = maxVal > 0 && _dispWidth(v) > maxVal
81
+ ? '${_cut(v, maxVal - 1)}…'
82
+ : v;
83
+ return '${dim(k)}${' ' * pad} $display';
58
84
  }
59
85
 
86
+ // ── Banner ───────────────────────────────────────────────────────────────────
87
+
60
88
  void banner(String name) {
61
89
  _echo('');
62
90
  _echo(
63
- ' ${cyan('▶')} ${cyan(bold('flutter_ota_kit'))} ${dim('·')} ${bold(name)}',
91
+ ' ${cyan('▶')} ${cyan(bold('flutter-ota'))} ${dim('·')} ${bold(name)}',
64
92
  );
65
- _echo(' ${dim('─' * math.min(_cols - 4, 60))}');
93
+ final sepLen = math.max(20, math.min(_cols - 4, 72));
94
+ _echo(' ${dim('─' * sepLen)}');
66
95
  }
67
96
 
68
- void step(String msg) => _echo(' ${green('✓')} $msg');
69
-
70
- void warn(String msg) => _echo(' ${yellow('!')} ${yellow(msg)}');
97
+ // ── Single-line status ──────────────────────────────────────────────────────
71
98
 
99
+ void step(String msg) => _echo(' ${green('✓')} $msg');
100
+ void warn(String msg) => _echo(' ${yellow('⚠')} ${yellow(msg)}');
72
101
  void err(String msg) => _echo(' ${red('✗')} ${red(msg)}');
73
102
 
103
+ // ── Spinner ──────────────────────────────────────────────────────────────────
104
+
74
105
  /// Run [task] while showing a live spinner; replaces the line with a
75
106
  /// `${green('✓')} done` summary when finished (or `✗` on error).
76
107
  Future<T> spinner<T>(
@@ -101,6 +132,8 @@ Future<T> spinner<T>(
101
132
  }
102
133
  }
103
134
 
135
+ // ── Word wrap ────────────────────────────────────────────────────────────────
136
+
104
137
  List<String> _wrap(String text, int inner) {
105
138
  if (text.isEmpty) return [''];
106
139
  final result = <String>[];
@@ -148,6 +181,11 @@ String _cut(String s, int inner) {
148
181
  String _splitLong(String word, int inner, List<String> out) {
149
182
  var rem = word;
150
183
  while (_dispWidth(rem) > inner) {
184
+ if (_dispWidth(rem) <= inner + 2) {
185
+ // Truncate with … instead of hard-breaking mid-word.
186
+ out.add('${_cut(rem, inner - 1)}…');
187
+ return '';
188
+ }
151
189
  final cut = _cut(rem, inner);
152
190
  out.add(cut);
153
191
  rem = rem.substring(cut.length);
@@ -155,14 +193,19 @@ String _splitLong(String word, int inner, List<String> out) {
155
193
  return rem;
156
194
  }
157
195
 
196
+ // ── Box ──────────────────────────────────────────────────────────────────────
197
+
198
+ /// Mathematically precise box width: single formula for both title and content.
158
199
  int _boxWidth(List<String> lines, String title) {
159
- var longest = _dispWidth(title) + 4;
200
+ var longest = 0;
160
201
  for (final l in lines) {
161
202
  for (final w in l.split('\n')) {
162
203
  longest = math.max(longest, _dispWidth(w));
163
204
  }
164
205
  }
165
- final target = math.max(longest + 4, 40);
206
+ final titleNeeded = _dispWidth(title) + 4;
207
+ final contentNeeded = longest + 4;
208
+ final target = math.max(titleNeeded, math.max(contentNeeded, 40));
166
209
  return math.max(math.min(_cols - 1, target), 20);
167
210
  }
168
211
 
@@ -171,18 +214,25 @@ int _boxWidth(List<String> lines, String title) {
171
214
  void box(String title, List<String> lines) {
172
215
  final width = _boxWidth(lines, title);
173
216
  final inner = width - 4;
217
+
218
+ // Wrap content to fit inner width.
174
219
  final wrapped = <String>[];
175
220
  for (final l in lines) {
176
- wrapped.addAll(_wrap(l, inner));
221
+ if (l.isEmpty) {
222
+ wrapped.add('');
223
+ } else {
224
+ wrapped.addAll(_wrap(l, inner));
225
+ }
177
226
  }
178
227
 
228
+ // Title centered in top border.
179
229
  final titleText = ' $title ';
180
230
  final tVisible = _dispWidth(titleText);
181
- var dashes = math.max(2, width - 2 - tVisible);
182
- final left = math.max(1, dashes ~/ 2);
183
- final right = dashes - left;
184
- final top = '╭${'─' * left}$titleText${'─' * right}╮';
185
- _echo(top);
231
+ final totalDashes = width - 2 - tVisible;
232
+ final leftDashes = math.max(1, totalDashes ~/ 2);
233
+ final rightDashes = totalDashes - leftDashes;
234
+
235
+ _echo('╭${'─' * leftDashes}$titleText${'─' * rightDashes}╮');
186
236
  for (final l in wrapped) {
187
237
  final pad = math.max(0, inner - _dispWidth(l));
188
238
  _echo('│ $l${' ' * pad} │');
@@ -190,44 +240,188 @@ void box(String title, List<String> lines) {
190
240
  _echo('╰${'─' * (width - 2)}╯');
191
241
  }
192
242
 
193
- /// Live progress bar with an animated spinner prefix. Call [update] as work
194
- /// progresses; it renders a `[████░░] 45%` bar that overwrites the same line.
243
+ // ── Steps ────────────────────────────────────────────────────────────────────
244
+
245
+ /// Multi-step progress tracker with uv-style clean output.
246
+ ///
247
+ /// Each step prints a single line: `✓ label` (done), `✗ label` (failed),
248
+ /// or `· label` (skipped). After all steps, call [summary] to print a
249
+ /// boxed summary with counts and elapsed time.
250
+ ///
251
+ /// Example:
252
+ /// ```dart
253
+ /// final s = Steps('migrate · supabase');
254
+ /// s.success('Applied 20250103_init.sql');
255
+ /// s.skip('Skipped 20251014_0.21.0.sql');
256
+ /// s.summary(); // prints "2 applied · 1 skipped · 0 errors · 245ms"
257
+ /// ```
258
+ class Steps {
259
+ Steps(this.title) : _sw = Stopwatch()..start();
260
+
261
+ final String title;
262
+ final Stopwatch _sw;
263
+ int _done = 0;
264
+ int _failed = 0;
265
+ int _skipped = 0;
266
+
267
+ /// Whether any step has failed.
268
+ bool get hasErrors => _failed > 0;
269
+
270
+ /// Step completed successfully.
271
+ void success(String label) {
272
+ _done++;
273
+ _echo(' ${green('✓')} $label');
274
+ }
275
+
276
+ /// Step failed.
277
+ void fail(String label) {
278
+ _failed++;
279
+ _echo(' ${red('✗')} $label');
280
+ }
281
+
282
+ /// Step skipped (e.g. already applied).
283
+ void skip(String label) {
284
+ _skipped++;
285
+ _echo(' ${dim('·')} $label');
286
+ }
287
+
288
+ /// Print boxed summary: "N applied · N skipped · N errors · TIME"
289
+ void summary() {
290
+ _sw.stop();
291
+ final ms = _sw.elapsedMilliseconds;
292
+ final time = ms >= 1000
293
+ ? '${(ms / 1000).toStringAsFixed(1)}s'
294
+ : '${ms}ms';
295
+ final parts = <String>[];
296
+ if (_done > 0) parts.add('${_done} completed');
297
+ if (_skipped > 0) parts.add('${_skipped} skipped');
298
+ if (_failed > 0) parts.add('${red('${_failed} error${_failed > 1 ? 's' : ''}')}');
299
+ if (parts.isEmpty) parts.add('no steps');
300
+ parts.add(time);
301
+ box('summary', [parts.join(' · ')]);
302
+ }
303
+ }
304
+
305
+ // ── Progress Bar (thin, single-line) ────────────────────────────────────────
306
+
307
+ /// Thin single-line progress bar with spinner, label, bar, percent, and time.
308
+ ///
309
+ /// Renders as: `⠹ Label ████░░░░ 50% 1.3s`
310
+ /// On finish: `✓ Label in 1.3s`
195
311
  class ProgressBar {
196
312
  ProgressBar(this.total, this.label);
197
313
 
198
314
  int total;
199
- final String label;
315
+ String label;
200
316
  int _current = 0;
201
317
  int _spin = 0;
202
318
  bool _closed = false;
319
+ final Stopwatch _sw = Stopwatch()..start();
320
+ late String _label = label;
203
321
 
204
- void update(int current, [String? label]) {
322
+ void update(int current, [String? newLabel]) {
205
323
  _current = current;
206
- if (label != null) _label = label;
324
+ if (newLabel != null) _label = newLabel;
207
325
  _draw();
208
326
  if (total > 0 && _current >= total) _finish();
209
327
  }
210
328
 
211
- late String _label = label;
212
-
213
329
  void _draw() {
214
330
  if (_closed) return;
215
331
  if (!_colorOn) {
216
332
  _echo(' $_label: $_current/$total');
217
333
  return;
218
334
  }
219
- const barW = 28;
335
+
220
336
  final pct = total > 0 ? (_current / total).clamp(0.0, 1.0) : 1.0;
337
+
338
+ // Thin bar: width scales to terminal, max 20 chars.
339
+ final barW = math.min(20, math.max(8, _cols - 40));
221
340
  final filled = (barW * pct).round();
222
- final bar = '${green('█' * filled)}${gray('·' * (barW - filled))}';
341
+ final empty = barW - filled;
342
+ final bar = '${yellow('█' * filled)}${gray('░' * empty)}';
343
+
223
344
  _spin = (_spin + 1) % _frames.length;
224
345
  final pctStr = '${(pct * 100).round()}%'.padLeft(4);
225
- stdout.write('\r\x1b[K ${cyan(_frames[_spin])} $_label [$bar] $pctStr');
346
+
347
+ stdout.write(
348
+ '\r\x1b[K ${cyan(_frames[_spin])} $_label $bar $pctStr',
349
+ );
226
350
  }
227
351
 
228
352
  void _finish() {
229
353
  if (_closed) return;
230
354
  _closed = true;
231
- if (_colorOn) stdout.write('\n');
355
+ _sw.stop();
356
+ final ms = _sw.elapsedMilliseconds;
357
+ final time = ms >= 1000
358
+ ? '${(ms / 1000).toStringAsFixed(1)}s'
359
+ : '${ms}ms';
360
+ if (_colorOn) {
361
+ stdout.write('\r\x1b[K ${green('✓')} $_label in $time\n');
362
+ } else {
363
+ _echo(' ✓ $_label in $time');
364
+ }
365
+ }
366
+
367
+ /// Force finish (for when total is not reached).
368
+ void close() {
369
+ if (!_closed) _finish();
370
+ }
371
+ }
372
+
373
+ // ── Table ────────────────────────────────────────────────────────────────────
374
+
375
+ /// Column-aligned table inside a bordered box.
376
+ ///
377
+ /// Example:
378
+ /// ```dart
379
+ /// table('3 bundles', ['#', 'ID', 'CHANNEL', 'ON'], [
380
+ /// ['0', 'abc-123', 'production', '✓'],
381
+ /// ['1', 'def-456', 'staging', '✗'],
382
+ /// ]);
383
+ /// ```
384
+ void table(String title, List<String> headers, List<List<String>> rows) {
385
+ if (headers.isEmpty) return;
386
+
387
+ // Calculate column widths from content.
388
+ final colCount = headers.length;
389
+ final colWidths = List<int>.filled(colCount, 0);
390
+ for (var i = 0; i < colCount; i++) {
391
+ colWidths[i] = _dispWidth(headers[i]);
392
+ }
393
+ for (final row in rows) {
394
+ for (var i = 0; i < colCount && i < row.length; i++) {
395
+ colWidths[i] = math.max(colWidths[i], _dispWidth(row[i]));
396
+ }
397
+ }
398
+
399
+ // Build formatted lines.
400
+ final formatted = <String>[];
401
+
402
+ // Header line.
403
+ final headerParts = <String>[];
404
+ for (var i = 0; i < colCount; i++) {
405
+ headerParts.add(headers[i].padRight(colWidths[i]));
406
+ }
407
+ formatted.add(bold(headerParts.join(' ')));
408
+
409
+ // Separator line.
410
+ final sepParts = <String>[];
411
+ for (var i = 0; i < colCount; i++) {
412
+ sepParts.add('─' * colWidths[i]);
232
413
  }
414
+ formatted.add(dim(sepParts.join(' ')));
415
+
416
+ // Data rows.
417
+ for (final row in rows) {
418
+ final parts = <String>[];
419
+ for (var i = 0; i < colCount; i++) {
420
+ final cell = i < row.length ? row[i] : '';
421
+ parts.add(cell.padRight(colWidths[i]));
422
+ }
423
+ formatted.add(parts.join(' '));
424
+ }
425
+
426
+ box(title, formatted);
233
427
  }
@@ -51,9 +51,13 @@ String generateFingerprint(String directory) {
51
51
  for (final entity in dir.listSync(recursive: true)) {
52
52
  if (entity is! File) continue;
53
53
  final relative = p.relative(entity.path, from: dir.path);
54
- final content = File(entity.path).readAsBytesSync();
55
- final hash = sha256Hex(content);
56
- entries.add('$relative:$hash');
54
+ try {
55
+ final content = File(entity.path).readAsBytesSync();
56
+ final hash = sha256Hex(content);
57
+ entries.add('$relative:$hash');
58
+ } on FileSystemException {
59
+ // Skip unreadable files (sockets, pipes, broken symlinks).
60
+ }
57
61
  }
58
62
  entries.sort();
59
63
  return sha256Hex(utf8.encode(entries.join('\n')));
@@ -381,6 +381,7 @@ void main() {
381
381
  source: dir.path,
382
382
  channel: 'production',
383
383
  platform: 'android',
384
+ targetAppVersion: '1.0.0',
384
385
  ),
385
386
  );
386
387
  expect(bundle.enabled, isTrue);
@@ -402,6 +403,7 @@ void main() {
402
403
  source: dir.path,
403
404
  channel: 'production',
404
405
  platform: 'android',
406
+ targetAppVersion: '1.0.0',
405
407
  ),
406
408
  );
407
409
  final res = await listBundles(
@@ -429,6 +431,7 @@ void main() {
429
431
  source: dir.path,
430
432
  channel: 'production',
431
433
  platform: 'android',
434
+ targetAppVersion: '1.0.0',
432
435
  ),
433
436
  );
434
437
  await deleteBundle(backend, deployed.id);
@@ -448,6 +451,7 @@ void main() {
448
451
  source: dir.path,
449
452
  channel: 'production',
450
453
  platform: 'android',
454
+ targetAppVersion: '1.0.0',
451
455
  ),
452
456
  );
453
457
  final second = await deployBundle(
@@ -456,6 +460,7 @@ void main() {
456
460
  source: dir.path,
457
461
  channel: 'production',
458
462
  platform: 'android',
463
+ targetAppVersion: '2.0.0',
459
464
  ),
460
465
  );
461
466
  final disabled = await rollbackChannel(backend, 'production');
@@ -477,6 +482,7 @@ void main() {
477
482
  source: dir.path,
478
483
  channel: 'production',
479
484
  platform: 'android',
485
+ targetAppVersion: '1.0.0',
480
486
  ),
481
487
  );
482
488
  await promoteBundle(backend, b.id, 'staging');
@@ -1,8 +1,12 @@
1
1
  -- HotUpdater.bundles
2
2
 
3
- CREATE TYPE platforms AS ENUM ('ios', 'android');
3
+ DO $$ BEGIN
4
+ CREATE TYPE platforms AS ENUM ('ios', 'android');
5
+ EXCEPTION
6
+ WHEN duplicate_object THEN NULL;
7
+ END $$;
4
8
 
5
- CREATE TABLE bundles (
9
+ CREATE TABLE IF NOT EXISTS bundles (
6
10
  id uuid PRIMARY KEY,
7
11
  platform platforms NOT NULL,
8
12
  target_app_version text NOT NULL,
@@ -14,7 +18,7 @@ CREATE TABLE bundles (
14
18
  message text
15
19
  );
16
20
 
17
- CREATE INDEX bundles_target_app_version_idx ON bundles(target_app_version);
21
+ CREATE INDEX IF NOT EXISTS bundles_target_app_version_idx ON bundles(target_app_version);
18
22
 
19
23
 
20
24
  -- HotUpdater.get_update_info
@@ -88,10 +88,13 @@ $$;
88
88
 
89
89
  -- HotUpdater.bundles
90
90
  ALTER TABLE bundles
91
- ADD COLUMN channel text NOT NULL DEFAULT 'production';
91
+ ADD COLUMN IF NOT EXISTS channel text NOT NULL DEFAULT 'production';
92
92
 
93
- ALTER TABLE bundles
94
- DROP COLUMN file_url;
93
+ DO $$ BEGIN
94
+ ALTER TABLE bundles DROP COLUMN file_url;
95
+ EXCEPTION
96
+ WHEN undefined_column THEN NULL;
97
+ END $$;
95
98
 
96
99
  -- HotUpdater.get_target_app_version_list
97
100
 
@@ -131,4 +134,4 @@ BEGIN
131
134
  END;
132
135
  $$;
133
136
 
134
- CREATE INDEX bundles_channel_idx ON bundles(channel);
137
+ CREATE INDEX IF NOT EXISTS bundles_channel_idx ON bundles(channel);
@@ -8,14 +8,26 @@ UPDATE bundles
8
8
  SET storage_uri = 'supabase-storage://%%BUCKET_NAME%%/' || id || '/bundle.zip'
9
9
  WHERE storage_uri IS NULL;
10
10
 
11
- ALTER TABLE bundles ALTER COLUMN storage_uri SET NOT NULL;
12
- ALTER TABLE bundles ALTER COLUMN target_app_version DROP NOT NULL;
11
+ DO $$ BEGIN
12
+ ALTER TABLE bundles ALTER COLUMN storage_uri SET NOT NULL;
13
+ EXCEPTION
14
+ WHEN null_value_not_allowed THEN NULL;
15
+ END $$;
16
+ DO $$ BEGIN
17
+ ALTER TABLE bundles ALTER COLUMN target_app_version DROP NOT NULL;
18
+ EXCEPTION
19
+ WHEN null_value_not_allowed THEN NULL;
20
+ END $$;
13
21
 
14
- ALTER TABLE bundles ADD CONSTRAINT check_version_or_fingerprint CHECK (
15
- (target_app_version IS NOT NULL) OR (fingerprint_hash IS NOT NULL)
16
- );
22
+ DO $$ BEGIN
23
+ ALTER TABLE bundles ADD CONSTRAINT check_version_or_fingerprint CHECK (
24
+ (target_app_version IS NOT NULL) OR (fingerprint_hash IS NOT NULL)
25
+ );
26
+ EXCEPTION
27
+ WHEN duplicate_object THEN NULL;
28
+ END $$;
17
29
 
18
- CREATE INDEX bundles_fingerprint_hash_idx ON bundles(fingerprint_hash);
30
+ CREATE INDEX IF NOT EXISTS bundles_fingerprint_hash_idx ON bundles(fingerprint_hash);
19
31
 
20
32
  DROP FUNCTION IF EXISTS get_update_info;
21
33
 
@@ -1,5 +1,7 @@
1
- DROP FUNCTION IF EXISTS get_update_info_by_fingerprint_hash;
2
- DROP FUNCTION IF EXISTS get_update_info_by_app_version;
1
+ DO $$ BEGIN DROP FUNCTION IF EXISTS get_update_info_by_fingerprint_hash(platforms, uuid, uuid, text, text); EXCEPTION WHEN undefined_function THEN NULL; END $$;
2
+ DO $$ BEGIN DROP FUNCTION IF EXISTS get_update_info_by_fingerprint_hash(platforms, uuid, uuid, text, text, text); EXCEPTION WHEN undefined_function THEN NULL; END $$;
3
+ DO $$ BEGIN DROP FUNCTION IF EXISTS get_update_info_by_app_version(platforms, text, uuid, uuid, text, text[]); EXCEPTION WHEN undefined_function THEN NULL; END $$;
4
+ DO $$ BEGIN DROP FUNCTION IF EXISTS get_update_info_by_app_version(platforms, text, uuid, uuid, text, text[], text); EXCEPTION WHEN undefined_function THEN NULL; END $$;
3
5
 
4
6
  -- HotUpdater.get_update_info
5
7
  CREATE OR REPLACE FUNCTION get_update_info_by_fingerprint_hash (
@@ -251,7 +251,8 @@ $$;
251
251
 
252
252
  -- HotUpdater.get_update_info_by_fingerprint_hash
253
253
 
254
- DROP FUNCTION IF EXISTS get_update_info_by_fingerprint_hash;
254
+ DO $$ BEGIN DROP FUNCTION IF EXISTS get_update_info_by_fingerprint_hash(platforms, uuid, uuid, text, text); EXCEPTION WHEN undefined_function THEN NULL; END $$;
255
+ DO $$ BEGIN DROP FUNCTION IF EXISTS get_update_info_by_fingerprint_hash(platforms, uuid, uuid, text, text, text); EXCEPTION WHEN undefined_function THEN NULL; END $$;
255
256
 
256
257
  CREATE OR REPLACE FUNCTION get_update_info_by_fingerprint_hash (
257
258
  app_platform platforms,
@@ -377,7 +378,8 @@ $$;
377
378
 
378
379
  -- HotUpdater.get_update_info_by_app_version
379
380
 
380
- DROP FUNCTION IF EXISTS get_update_info_by_app_version;
381
+ DO $$ BEGIN DROP FUNCTION IF EXISTS get_update_info_by_app_version(platforms, text, uuid, uuid, text, text[]); EXCEPTION WHEN undefined_function THEN NULL; END $$;
382
+ DO $$ BEGIN DROP FUNCTION IF EXISTS get_update_info_by_app_version(platforms, text, uuid, uuid, text, text[], text); EXCEPTION WHEN undefined_function THEN NULL; END $$;
381
383
 
382
384
  CREATE OR REPLACE FUNCTION get_update_info_by_app_version (
383
385
  app_platform platforms,
@@ -1,7 +1,11 @@
1
1
  -- HotUpdater.supabase_rls
2
2
 
3
3
  ALTER TABLE public.bundles ENABLE ROW LEVEL SECURITY;
4
- ALTER TABLE public.bundle_patches ENABLE ROW LEVEL SECURITY;
4
+ DO $$ BEGIN
5
+ ALTER TABLE public.bundle_patches ENABLE ROW LEVEL SECURITY;
6
+ EXCEPTION
7
+ WHEN undefined_table THEN NULL;
8
+ END $$;
5
9
 
6
10
  ALTER FUNCTION public.get_target_app_version_list(public.platforms, uuid)
7
11
  SET search_path = public, pg_catalog;
@@ -50,10 +54,22 @@ ALTER FUNCTION public.get_update_info_by_app_version(
50
54
  -- OTA update checks are unauthenticated by design (the device only carries the
51
55
  -- anon key), so bundle metadata must be publicly readable. hot-updater's hosted
52
56
  -- Supabase flow relies on these policies existing.
53
- CREATE POLICY "allow public read bundles" ON public.bundles FOR SELECT USING (true);
54
- CREATE POLICY "allow public read bundle_patches" ON public.bundle_patches FOR SELECT USING (true);
57
+ DO $$ BEGIN
58
+ CREATE POLICY "allow public read bundles" ON public.bundles FOR SELECT USING (true);
59
+ EXCEPTION
60
+ WHEN duplicate_object THEN NULL;
61
+ END $$;
62
+ DO $$ BEGIN
63
+ CREATE POLICY "allow public read bundle_patches" ON public.bundle_patches FOR SELECT USING (true);
64
+ EXCEPTION
65
+ WHEN duplicate_object THEN NULL;
66
+ END $$;
55
67
 
56
68
  -- Allow anon/authenticated to resolve signed download URLs for the bundles bucket.
57
- CREATE POLICY "public bundles read" ON storage.objects
58
- FOR SELECT TO anon, authenticated
59
- USING (bucket_id = 'bundles');
69
+ DO $$ BEGIN
70
+ CREATE POLICY "public bundles read" ON storage.objects
71
+ FOR SELECT TO anon, authenticated
72
+ USING (bucket_id = 'bundles');
73
+ EXCEPTION
74
+ WHEN duplicate_object THEN NULL;
75
+ END $$;
@@ -1,8 +1,12 @@
1
1
  -- HotUpdater.bundles
2
2
 
3
- CREATE TYPE platforms AS ENUM ('ios', 'android');
3
+ DO $$ BEGIN
4
+ CREATE TYPE platforms AS ENUM ('ios', 'android');
5
+ EXCEPTION
6
+ WHEN duplicate_object THEN NULL;
7
+ END $$;
4
8
 
5
- CREATE TABLE bundles (
9
+ CREATE TABLE IF NOT EXISTS bundles (
6
10
  id uuid PRIMARY KEY,
7
11
  platform platforms NOT NULL,
8
12
  target_app_version text NOT NULL,
@@ -14,7 +18,7 @@ CREATE TABLE bundles (
14
18
  message text
15
19
  );
16
20
 
17
- CREATE INDEX bundles_target_app_version_idx ON bundles(target_app_version);
21
+ CREATE INDEX IF NOT EXISTS bundles_target_app_version_idx ON bundles(target_app_version);
18
22
 
19
23
 
20
24
  -- HotUpdater.get_update_info
@@ -88,10 +88,13 @@ $$;
88
88
 
89
89
  -- HotUpdater.bundles
90
90
  ALTER TABLE bundles
91
- ADD COLUMN channel text NOT NULL DEFAULT 'production';
91
+ ADD COLUMN IF NOT EXISTS channel text NOT NULL DEFAULT 'production';
92
92
 
93
- ALTER TABLE bundles
94
- DROP COLUMN file_url;
93
+ DO $$ BEGIN
94
+ ALTER TABLE bundles DROP COLUMN file_url;
95
+ EXCEPTION
96
+ WHEN undefined_column THEN NULL;
97
+ END $$;
95
98
 
96
99
  -- HotUpdater.get_target_app_version_list
97
100
 
@@ -131,4 +134,4 @@ BEGIN
131
134
  END;
132
135
  $$;
133
136
 
134
- CREATE INDEX bundles_channel_idx ON bundles(channel);
137
+ CREATE INDEX IF NOT EXISTS bundles_channel_idx ON bundles(channel);