@_nazmiforreal/flutter-ota 0.1.19 → 0.1.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dart-src/packages/cli-tools/lib/flutter_ota_kit_cli.dart +4 -3
- package/dart-src/packages/cli-tools/lib/src/cli_base.dart +3 -2
- package/dart-src/packages/cli-tools/lib/src/commands/bundle.dart +22 -10
- package/dart-src/packages/cli-tools/lib/src/commands/channel.dart +16 -5
- package/dart-src/packages/cli-tools/lib/src/commands/config_command.dart +3 -0
- package/dart-src/packages/cli-tools/lib/src/commands/console.dart +7 -8
- package/dart-src/packages/cli-tools/lib/src/commands/deploy.dart +12 -3
- package/dart-src/packages/cli-tools/lib/src/commands/doctor.dart +129 -6
- package/dart-src/packages/cli-tools/lib/src/commands/fingerprint.dart +2 -1
- package/dart-src/packages/cli-tools/lib/src/commands/init.dart +23 -14
- package/dart-src/packages/cli-tools/lib/src/commands/keys.dart +4 -0
- package/dart-src/packages/cli-tools/lib/src/commands/migrate.dart +266 -26
- package/dart-src/packages/cli-tools/lib/src/commands/pocketbase.dart +1422 -16
- package/dart-src/packages/cli-tools/lib/src/commands/rollback.dart +4 -1
- package/dart-src/packages/cli-tools/lib/src/commands/storage.dart +10 -9
- package/dart-src/packages/cli-tools/lib/src/config.dart +14 -19
- package/dart-src/packages/cli-tools/lib/src/operations.dart +1 -1
- package/dart-src/packages/cli-tools/lib/src/pocketbase/installer.dart +1 -1
- package/dart-src/packages/cli-tools/lib/src/pocketbase/process_manager.dart +113 -10
- package/dart-src/packages/cli-tools/lib/src/pocketbase/schema_installer.dart +59 -6
- package/dart-src/packages/cli-tools/lib/src/ui/ui.dart +2 -2
- package/dart-src/packages/cli-tools/test/mocks/mock_pocketbase_client.dart +149 -0
- package/dart-src/packages/cli-tools/test/pocketbase_schema_test.dart +14 -9
- package/dart-src/packages/core/CHANGELOG.md +6 -1
- package/dart-src/packages/core/lib/flutter_ota_kit_core.dart +2 -0
- package/dart-src/packages/core/lib/src/app_update_info.dart +73 -1
- package/dart-src/packages/core/lib/src/bundle.dart +62 -17
- package/dart-src/packages/core/lib/src/bundle_artifacts.dart +7 -3
- package/dart-src/packages/core/lib/src/bundle_patch_artifact.dart +16 -0
- package/dart-src/packages/core/lib/src/changed_asset.dart +33 -3
- package/dart-src/packages/core/lib/src/get_bundles_args.dart +24 -2
- package/dart-src/packages/core/lib/src/platform.dart +8 -0
- package/dart-src/packages/core/lib/src/semver.dart +0 -3
- package/dart-src/packages/core/lib/src/status.dart +20 -1
- package/dart-src/packages/core/lib/src/strategy.dart +10 -0
- package/dart-src/packages/core/lib/src/update_bundle_params.dart +15 -0
- package/dart-src/packages/core/lib/src/uuid.dart +11 -0
- package/dart-src/plugins/aws/CHANGELOG.md +5 -1
- package/dart-src/plugins/aws/lib/src/aws_cloudfront_client.dart +2 -2
- package/dart-src/plugins/cloudflare/CHANGELOG.md +5 -1
- package/dart-src/plugins/plugin-core/CHANGELOG.md +5 -1
- package/dart-src/plugins/plugin-core/lib/src/asset_storage_layout.dart +3 -0
- package/dart-src/plugins/plugin-core/lib/src/parse_storage_uri.dart +17 -0
- package/dart-src/plugins/plugin-core/lib/src/types.dart +94 -3
- package/dart-src/plugins/pocketbase/CHANGELOG.md +5 -1
- package/dart-src/plugins/pocketbase/lib/flutter_ota_kit_pocketbase.dart +2 -1
- package/dart-src/plugins/pocketbase/lib/src/pocketbase_bundle_mapper.dart +10 -2
- package/dart-src/plugins/pocketbase/lib/src/pocketbase_client.dart +617 -4
- package/dart-src/plugins/pocketbase/lib/src/pocketbase_database.dart +262 -42
- package/dart-src/plugins/pocketbase/lib/src/pocketbase_storage.dart +9 -2
- package/dart-src/plugins/postgres/CHANGELOG.md +5 -1
- package/dart-src/plugins/supabase/CHANGELOG.md +6 -1
- package/dart-src/plugins/supabase/lib/src/supabase_database.dart +4 -0
- package/package.json +2 -2
- package/bin/flutter-ota-linux-x64 +0 -0
- package/bin/flutter-ota.js +0 -36
- package/dart-src/packages/cli-tools/.dart_tool/package_config.json +0 -458
- package/dart-src/packages/cli-tools/.dart_tool/package_graph.json +0 -707
- package/dart-src/packages/cli-tools/pubspec.lock +0 -590
|
@@ -41,6 +41,9 @@ class PocketBaseClient {
|
|
|
41
41
|
String _token;
|
|
42
42
|
final http.Client _http;
|
|
43
43
|
|
|
44
|
+
/// The current admin auth token (empty if not authenticated).
|
|
45
|
+
String get authToken => _token;
|
|
46
|
+
|
|
44
47
|
static String _normalizeUrl(String url) {
|
|
45
48
|
var u = url.trim();
|
|
46
49
|
if (u.endsWith('/')) u = u.substring(0, u.length - 1);
|
|
@@ -53,16 +56,16 @@ class PocketBaseClient {
|
|
|
53
56
|
_adminPassword = password;
|
|
54
57
|
}
|
|
55
58
|
|
|
56
|
-
/// Authenticate as
|
|
59
|
+
/// Authenticate as admin email / admin password. Idempotent — calling
|
|
57
60
|
/// twice refreshes the token.
|
|
58
61
|
Future<PocketBaseClient> authenticate(
|
|
59
62
|
String adminEmail,
|
|
60
|
-
String
|
|
63
|
+
String password,
|
|
61
64
|
) async {
|
|
62
65
|
final res = await _http.post(
|
|
63
|
-
Uri.parse('$_baseUrl/api/
|
|
66
|
+
Uri.parse('$_baseUrl/api/collections/_superusers/auth-with-password'),
|
|
64
67
|
headers: {'content-type': 'application/json'},
|
|
65
|
-
body: jsonEncode({'identity': adminEmail, 'password':
|
|
68
|
+
body: jsonEncode({'identity': adminEmail, 'password': password}),
|
|
66
69
|
);
|
|
67
70
|
if (res.statusCode != 200) {
|
|
68
71
|
throw PocketBaseException(
|
|
@@ -320,6 +323,587 @@ class PocketBaseClient {
|
|
|
320
323
|
}
|
|
321
324
|
}
|
|
322
325
|
|
|
326
|
+
/// Detailed health check returning the full JSON response.
|
|
327
|
+
Future<Map<String, dynamic>> healthDetailed() async {
|
|
328
|
+
final res = await _http.get(Uri.parse('$_baseUrl/api/health'));
|
|
329
|
+
if (res.statusCode != 200) {
|
|
330
|
+
throw PocketBaseException(
|
|
331
|
+
'health check failed: HTTP ${res.statusCode} ${res.body}',
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
return jsonDecode(res.body) as Map<String, dynamic>;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// ----- Backups -----
|
|
338
|
+
|
|
339
|
+
/// List available backups.
|
|
340
|
+
Future<List<PocketBaseBackup>> listBackups() async {
|
|
341
|
+
final res = await _get(Uri.parse('$_baseUrl/api/backups'));
|
|
342
|
+
if (res.statusCode != 200) {
|
|
343
|
+
throw PocketBaseException(
|
|
344
|
+
'listBackups failed: HTTP ${res.statusCode} ${res.body}',
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
final body = jsonDecode(res.body);
|
|
348
|
+
final items = body is List ? body : <dynamic>[];
|
|
349
|
+
return items
|
|
350
|
+
.map((j) => PocketBaseBackup.fromJson(j as Map<String, dynamic>))
|
|
351
|
+
.toList();
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/// Create a new backup. Optionally provide a [name].
|
|
355
|
+
Future<void> createBackup({String? name}) async {
|
|
356
|
+
final body = <String, dynamic>{};
|
|
357
|
+
if (name != null) body['name'] = name;
|
|
358
|
+
final res = await _post(
|
|
359
|
+
Uri.parse('$_baseUrl/api/backups'),
|
|
360
|
+
body: body.isEmpty ? null : body,
|
|
361
|
+
);
|
|
362
|
+
if (res.statusCode != 204 && res.statusCode != 200) {
|
|
363
|
+
throw PocketBaseException(
|
|
364
|
+
'createBackup failed: HTTP ${res.statusCode} ${res.body}',
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/// Delete a backup by key.
|
|
370
|
+
Future<void> deleteBackup(String key) async {
|
|
371
|
+
final res = await _delete(Uri.parse('$_baseUrl/api/backups/$key'));
|
|
372
|
+
if (res.statusCode != 204) {
|
|
373
|
+
throw PocketBaseException(
|
|
374
|
+
'deleteBackup($key) failed: HTTP ${res.statusCode} ${res.body}',
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/// Restore a backup by key (restarts PB).
|
|
380
|
+
Future<void> restoreBackup(String key) async {
|
|
381
|
+
final res = await _post(Uri.parse('$_baseUrl/api/backups/$key/restore'));
|
|
382
|
+
if (res.statusCode != 204 && res.statusCode != 200) {
|
|
383
|
+
throw PocketBaseException(
|
|
384
|
+
'restoreBackup($key) failed: HTTP ${res.statusCode} ${res.body}',
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/// Get the download URL for a backup file.
|
|
390
|
+
String backupDownloadUrl(String key, String token) {
|
|
391
|
+
return '$_baseUrl/api/backups/$key?token=$token';
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// ----- Admins (superusers collection in PB 0.40+) -----
|
|
395
|
+
|
|
396
|
+
/// List admin accounts via the _superusers collection.
|
|
397
|
+
Future<List<Map<String, dynamic>>> listAdmins() async {
|
|
398
|
+
final res = await _get(
|
|
399
|
+
Uri.parse('$_baseUrl/api/collections/_superusers/records?perPage=100'),
|
|
400
|
+
);
|
|
401
|
+
if (res.statusCode != 200) {
|
|
402
|
+
throw PocketBaseException(
|
|
403
|
+
'listAdmins failed: HTTP ${res.statusCode} ${res.body}',
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
final body = jsonDecode(res.body) as Map<String, dynamic>;
|
|
407
|
+
return (body['items'] as List? ?? []).cast<Map<String, dynamic>>();
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/// Create a new admin account via the _superusers collection.
|
|
411
|
+
Future<Map<String, dynamic>> createAdmin({
|
|
412
|
+
required String email,
|
|
413
|
+
required String password,
|
|
414
|
+
required String passwordConfirm,
|
|
415
|
+
}) async {
|
|
416
|
+
final headers = <String, String>{'content-type': 'application/json'};
|
|
417
|
+
if (_token.isNotEmpty) headers['authorization'] = _token;
|
|
418
|
+
final res = await _http.post(
|
|
419
|
+
Uri.parse('$_baseUrl/api/collections/_superusers/records'),
|
|
420
|
+
headers: headers,
|
|
421
|
+
body: jsonEncode({
|
|
422
|
+
'email': email,
|
|
423
|
+
'password': password,
|
|
424
|
+
'passwordConfirm': passwordConfirm,
|
|
425
|
+
}),
|
|
426
|
+
);
|
|
427
|
+
if (res.statusCode != 200 && res.statusCode != 201) {
|
|
428
|
+
throw PocketBaseException(
|
|
429
|
+
'createAdmin failed: HTTP ${res.statusCode} ${res.body}',
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
return jsonDecode(res.body) as Map<String, dynamic>;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/// Update an admin account by ID.
|
|
436
|
+
Future<Map<String, dynamic>> updateAdmin(
|
|
437
|
+
String id,
|
|
438
|
+
Map<String, dynamic> data,
|
|
439
|
+
) async {
|
|
440
|
+
final headers = <String, String>{'content-type': 'application/json'};
|
|
441
|
+
if (_token.isNotEmpty) headers['authorization'] = _token;
|
|
442
|
+
final res = await _http.patch(
|
|
443
|
+
Uri.parse('$_baseUrl/api/collections/_superusers/records/$id'),
|
|
444
|
+
headers: headers,
|
|
445
|
+
body: jsonEncode(data),
|
|
446
|
+
);
|
|
447
|
+
if (res.statusCode != 200) {
|
|
448
|
+
throw PocketBaseException(
|
|
449
|
+
'updateAdmin($id) failed: HTTP ${res.statusCode} ${res.body}',
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
return jsonDecode(res.body) as Map<String, dynamic>;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/// Delete an admin account by ID.
|
|
456
|
+
Future<void> deleteAdmin(String id) async {
|
|
457
|
+
final headers = <String, String>{};
|
|
458
|
+
if (_token.isNotEmpty) headers['authorization'] = _token;
|
|
459
|
+
final res = await _http.delete(
|
|
460
|
+
Uri.parse('$_baseUrl/api/collections/_superusers/records/$id'),
|
|
461
|
+
headers: headers,
|
|
462
|
+
);
|
|
463
|
+
if (res.statusCode != 204) {
|
|
464
|
+
throw PocketBaseException(
|
|
465
|
+
'deleteAdmin($id) failed: HTTP ${res.statusCode} ${res.body}',
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// ----- Collection export/import -----
|
|
471
|
+
|
|
472
|
+
/// Export all records from a collection as JSON.
|
|
473
|
+
Future<List<Map<String, dynamic>>> exportCollection(String collection) async {
|
|
474
|
+
// PB 0.22.x has no export-records endpoint; fetch all via listRecords.
|
|
475
|
+
final result = await listRecords<Map<String, dynamic>>(
|
|
476
|
+
collection,
|
|
477
|
+
(j) => Map<String, dynamic>.from(j),
|
|
478
|
+
perPage: 500,
|
|
479
|
+
);
|
|
480
|
+
return result.items;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/// Import records into a collection from a JSON list.
|
|
484
|
+
Future<void> importCollection(
|
|
485
|
+
String collection,
|
|
486
|
+
List<Map<String, dynamic>> records,
|
|
487
|
+
) async {
|
|
488
|
+
// PB 0.22.x has no import-records endpoint; create records individually.
|
|
489
|
+
for (final record in records) {
|
|
490
|
+
final body = Map<String, dynamic>.from(record)
|
|
491
|
+
..remove('id')
|
|
492
|
+
..remove('collectionId')
|
|
493
|
+
..remove('collectionName')
|
|
494
|
+
..remove('created')
|
|
495
|
+
..remove('updated');
|
|
496
|
+
await createRecord<Map<String, dynamic>>(collection, body, (j) => j);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// ----- Raw request (for query command) -----
|
|
501
|
+
|
|
502
|
+
/// Execute an arbitrary HTTP request against the PB API.
|
|
503
|
+
Future<http.Response> rawRequest(
|
|
504
|
+
String method,
|
|
505
|
+
String path, {
|
|
506
|
+
Map<String, dynamic>? body,
|
|
507
|
+
}) async {
|
|
508
|
+
await _ensureAuth();
|
|
509
|
+
final uri = Uri.parse('$_baseUrl$path');
|
|
510
|
+
switch (method.toUpperCase()) {
|
|
511
|
+
case 'GET':
|
|
512
|
+
return _get(uri);
|
|
513
|
+
case 'POST':
|
|
514
|
+
return _post(uri, body: body);
|
|
515
|
+
case 'PATCH':
|
|
516
|
+
return _patch(uri, body: body);
|
|
517
|
+
case 'DELETE':
|
|
518
|
+
return _delete(uri);
|
|
519
|
+
default:
|
|
520
|
+
throw PocketBaseException('Unsupported method: $method');
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/// Download a file by URL to bytes (unauthenticated).
|
|
525
|
+
Future<List<int>> downloadFileUnauth(String url) async {
|
|
526
|
+
final res = await _http.get(Uri.parse(url));
|
|
527
|
+
if (res.statusCode != 200) {
|
|
528
|
+
throw PocketBaseException(
|
|
529
|
+
'downloadFile($url) failed: HTTP ${res.statusCode}',
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
return res.bodyBytes;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// ----- Logs -----
|
|
536
|
+
|
|
537
|
+
/// List logs with optional filter and pagination.
|
|
538
|
+
Future<Map<String, dynamic>> listLogs({
|
|
539
|
+
String? filter,
|
|
540
|
+
String? sort,
|
|
541
|
+
int page = 1,
|
|
542
|
+
int perPage = 30,
|
|
543
|
+
}) async {
|
|
544
|
+
final qp = <String, String>{
|
|
545
|
+
'page': page.toString(),
|
|
546
|
+
'perPage': perPage.toString(),
|
|
547
|
+
};
|
|
548
|
+
if (filter != null && filter.isNotEmpty) qp['filter'] = filter;
|
|
549
|
+
if (sort != null && sort.isNotEmpty) qp['sort'] = sort;
|
|
550
|
+
final uri = Uri.parse('$_baseUrl/api/logs').replace(queryParameters: qp);
|
|
551
|
+
final res = await _get(uri);
|
|
552
|
+
if (res.statusCode != 200) {
|
|
553
|
+
throw PocketBaseException(
|
|
554
|
+
'listLogs failed: HTTP ${res.statusCode} ${res.body}',
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
return jsonDecode(res.body) as Map<String, dynamic>;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/// Get a single log by ID.
|
|
561
|
+
Future<Map<String, dynamic>> getLog(String id) async {
|
|
562
|
+
final res = await _get(Uri.parse('$_baseUrl/api/logs/$id'));
|
|
563
|
+
if (res.statusCode != 200) {
|
|
564
|
+
throw PocketBaseException(
|
|
565
|
+
'getLog($id) failed: HTTP ${res.statusCode} ${res.body}',
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
return jsonDecode(res.body) as Map<String, dynamic>;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/// Get log statistics.
|
|
572
|
+
Future<List<Map<String, dynamic>>> getLogStats({String? filter}) async {
|
|
573
|
+
final qp = <String, String>{};
|
|
574
|
+
if (filter != null && filter.isNotEmpty) qp['filter'] = filter;
|
|
575
|
+
final uri = Uri.parse('$_baseUrl/api/logs/stats')
|
|
576
|
+
.replace(queryParameters: qp);
|
|
577
|
+
final res = await _get(uri);
|
|
578
|
+
if (res.statusCode != 200) {
|
|
579
|
+
throw PocketBaseException(
|
|
580
|
+
'getLogStats failed: HTTP ${res.statusCode} ${res.body}',
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
final body = jsonDecode(res.body);
|
|
584
|
+
return body is List ? body.cast<Map<String, dynamic>>() : [];
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/// Truncate all logs.
|
|
588
|
+
Future<void> truncateLogs() async {
|
|
589
|
+
final res = await _delete(Uri.parse('$_baseUrl/api/logs'));
|
|
590
|
+
if (res.statusCode != 204) {
|
|
591
|
+
throw PocketBaseException(
|
|
592
|
+
'truncateLogs failed: HTTP ${res.statusCode} ${res.body}',
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// ----- Collections -----
|
|
598
|
+
|
|
599
|
+
/// List collections.
|
|
600
|
+
Future<List<Map<String, dynamic>>> listCollections() async {
|
|
601
|
+
final res = await _get(Uri.parse('$_baseUrl/api/collections'));
|
|
602
|
+
if (res.statusCode != 200) {
|
|
603
|
+
throw PocketBaseException(
|
|
604
|
+
'listCollections failed: HTTP ${res.statusCode} ${res.body}',
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
final body = jsonDecode(res.body) as Map<String, dynamic>;
|
|
608
|
+
return (body['items'] as List? ?? []).cast<Map<String, dynamic>>();
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/// Get a collection by name or ID.
|
|
612
|
+
Future<Map<String, dynamic>> getCollection(String nameOrId) async {
|
|
613
|
+
final res = await _get(Uri.parse('$_baseUrl/api/collections/$nameOrId'));
|
|
614
|
+
if (res.statusCode != 200) {
|
|
615
|
+
throw PocketBaseException(
|
|
616
|
+
'getCollection($nameOrId) failed: HTTP ${res.statusCode} ${res.body}',
|
|
617
|
+
);
|
|
618
|
+
}
|
|
619
|
+
return jsonDecode(res.body) as Map<String, dynamic>;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/// Create a collection.
|
|
623
|
+
Future<Map<String, dynamic>> createCollection(
|
|
624
|
+
Map<String, dynamic> body,
|
|
625
|
+
) async {
|
|
626
|
+
final res = await _post(Uri.parse('$_baseUrl/api/collections'), body: body);
|
|
627
|
+
if (res.statusCode != 200 && res.statusCode != 201) {
|
|
628
|
+
throw PocketBaseException(
|
|
629
|
+
'createCollection failed: HTTP ${res.statusCode} ${res.body}',
|
|
630
|
+
);
|
|
631
|
+
}
|
|
632
|
+
return jsonDecode(res.body) as Map<String, dynamic>;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/// Update a collection.
|
|
636
|
+
Future<Map<String, dynamic>> updateCollection(
|
|
637
|
+
String nameOrId,
|
|
638
|
+
Map<String, dynamic> body,
|
|
639
|
+
) async {
|
|
640
|
+
final res = await _patch(
|
|
641
|
+
Uri.parse('$_baseUrl/api/collections/$nameOrId'),
|
|
642
|
+
body: body,
|
|
643
|
+
);
|
|
644
|
+
if (res.statusCode != 200) {
|
|
645
|
+
throw PocketBaseException(
|
|
646
|
+
'updateCollection($nameOrId) failed: HTTP ${res.statusCode} ${res.body}',
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
return jsonDecode(res.body) as Map<String, dynamic>;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/// Delete a collection.
|
|
653
|
+
Future<void> deleteCollection(String nameOrId) async {
|
|
654
|
+
final res = await _delete(Uri.parse('$_baseUrl/api/collections/$nameOrId'));
|
|
655
|
+
if (res.statusCode != 204) {
|
|
656
|
+
throw PocketBaseException(
|
|
657
|
+
'deleteCollection($nameOrId) failed: HTTP ${res.statusCode} ${res.body}',
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/// Truncate a collection (delete all records).
|
|
663
|
+
Future<void> truncateCollection(String nameOrId) async {
|
|
664
|
+
final res = await _delete(
|
|
665
|
+
Uri.parse('$_baseUrl/api/collections/$nameOrId/truncate'),
|
|
666
|
+
);
|
|
667
|
+
if (res.statusCode != 204) {
|
|
668
|
+
throw PocketBaseException(
|
|
669
|
+
'truncateCollection($nameOrId) failed: HTTP ${res.statusCode} ${res.body}',
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/// Import collections configuration.
|
|
675
|
+
Future<void> importCollections(
|
|
676
|
+
List<Map<String, dynamic>> collections, {
|
|
677
|
+
bool deleteMissing = false,
|
|
678
|
+
}) async {
|
|
679
|
+
await _ensureAuth();
|
|
680
|
+
final res = await _http.put(
|
|
681
|
+
Uri.parse('$_baseUrl/api/collections/import'),
|
|
682
|
+
headers: {..._authHeaders, 'content-type': 'application/json'},
|
|
683
|
+
body: jsonEncode({
|
|
684
|
+
'collections': collections,
|
|
685
|
+
'deleteMissing': deleteMissing,
|
|
686
|
+
}),
|
|
687
|
+
);
|
|
688
|
+
if (res.statusCode != 204 && res.statusCode != 200) {
|
|
689
|
+
throw PocketBaseException(
|
|
690
|
+
'importCollections failed: HTTP ${res.statusCode} ${res.body}',
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// ----- Backup upload -----
|
|
696
|
+
|
|
697
|
+
/// Upload a backup zip file.
|
|
698
|
+
Future<void> uploadBackup(List<int> bytes, {String? name}) async {
|
|
699
|
+
await _ensureAuth();
|
|
700
|
+
final req = http.MultipartRequest(
|
|
701
|
+
'POST',
|
|
702
|
+
Uri.parse('$_baseUrl/api/backups/upload'),
|
|
703
|
+
);
|
|
704
|
+
req.headers['authorization'] = _token;
|
|
705
|
+
req.files.add(
|
|
706
|
+
http.MultipartFile.fromBytes(
|
|
707
|
+
'file',
|
|
708
|
+
bytes,
|
|
709
|
+
filename: name ?? 'backup.zip',
|
|
710
|
+
),
|
|
711
|
+
);
|
|
712
|
+
final streamed = await req.send();
|
|
713
|
+
final res = await http.Response.fromStream(streamed);
|
|
714
|
+
if (res.statusCode != 204 && res.statusCode != 200) {
|
|
715
|
+
throw PocketBaseException(
|
|
716
|
+
'uploadBackup failed: HTTP ${res.statusCode} ${res.body}',
|
|
717
|
+
);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// ----- Batch -----
|
|
722
|
+
|
|
723
|
+
/// Execute multiple record operations in a single transaction.
|
|
724
|
+
Future<List<Map<String, dynamic>>> batch(
|
|
725
|
+
List<Map<String, dynamic>> requests,
|
|
726
|
+
) async {
|
|
727
|
+
await _ensureAuth();
|
|
728
|
+
final res = await _http.post(
|
|
729
|
+
Uri.parse('$_baseUrl/api/batch'),
|
|
730
|
+
headers: _authHeaders,
|
|
731
|
+
body: jsonEncode({'requests': requests}),
|
|
732
|
+
);
|
|
733
|
+
if (res.statusCode != 200) {
|
|
734
|
+
throw PocketBaseException(
|
|
735
|
+
'batch failed: HTTP ${res.statusCode} ${res.body}',
|
|
736
|
+
);
|
|
737
|
+
}
|
|
738
|
+
return (jsonDecode(res.body) as List).cast<Map<String, dynamic>>();
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// ----- Settings -----
|
|
742
|
+
|
|
743
|
+
/// List all application settings.
|
|
744
|
+
Future<Map<String, dynamic>> listSettings() async {
|
|
745
|
+
await _ensureAuth();
|
|
746
|
+
final res = await _http.get(
|
|
747
|
+
Uri.parse('$_baseUrl/api/settings'),
|
|
748
|
+
headers: _authHeaders,
|
|
749
|
+
);
|
|
750
|
+
if (res.statusCode != 200) {
|
|
751
|
+
throw PocketBaseException(
|
|
752
|
+
'listSettings failed: HTTP ${res.statusCode} ${res.body}',
|
|
753
|
+
);
|
|
754
|
+
}
|
|
755
|
+
return jsonDecode(res.body) as Map<String, dynamic>;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/// Update application settings.
|
|
759
|
+
Future<Map<String, dynamic>> updateSettings(Map<String, dynamic> body) async {
|
|
760
|
+
await _ensureAuth();
|
|
761
|
+
final res = await _http.patch(
|
|
762
|
+
Uri.parse('$_baseUrl/api/settings'),
|
|
763
|
+
headers: {..._authHeaders, 'content-type': 'application/json'},
|
|
764
|
+
body: jsonEncode(body),
|
|
765
|
+
);
|
|
766
|
+
if (res.statusCode != 200) {
|
|
767
|
+
throw PocketBaseException(
|
|
768
|
+
'updateSettings failed: HTTP ${res.statusCode} ${res.body}',
|
|
769
|
+
);
|
|
770
|
+
}
|
|
771
|
+
return jsonDecode(res.body) as Map<String, dynamic>;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
/// Test S3 storage connection.
|
|
775
|
+
Future<void> testS3(String filesystem) async {
|
|
776
|
+
await _ensureAuth();
|
|
777
|
+
final res = await _http.post(
|
|
778
|
+
Uri.parse('$_baseUrl/api/settings/test/s3'),
|
|
779
|
+
headers: {..._authHeaders, 'content-type': 'application/json'},
|
|
780
|
+
body: jsonEncode({'filesystem': filesystem}),
|
|
781
|
+
);
|
|
782
|
+
if (res.statusCode != 204) {
|
|
783
|
+
throw PocketBaseException(
|
|
784
|
+
'testS3 failed: HTTP ${res.statusCode} ${res.body}',
|
|
785
|
+
);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/// Send a test email.
|
|
790
|
+
Future<void> testEmail({
|
|
791
|
+
required String email,
|
|
792
|
+
required String template,
|
|
793
|
+
String? collection,
|
|
794
|
+
}) async {
|
|
795
|
+
await _ensureAuth();
|
|
796
|
+
final body = <String, dynamic>{'email': email, 'template': template};
|
|
797
|
+
if (collection != null) body['collection'] = collection;
|
|
798
|
+
final res = await _http.post(
|
|
799
|
+
Uri.parse('$_baseUrl/api/settings/test/email'),
|
|
800
|
+
headers: {..._authHeaders, 'content-type': 'application/json'},
|
|
801
|
+
body: jsonEncode(body),
|
|
802
|
+
);
|
|
803
|
+
if (res.statusCode != 204) {
|
|
804
|
+
throw PocketBaseException(
|
|
805
|
+
'testEmail failed: HTTP ${res.statusCode} ${res.body}',
|
|
806
|
+
);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// ----- SQL -----
|
|
811
|
+
|
|
812
|
+
/// Execute a raw SQL query.
|
|
813
|
+
Future<Map<String, dynamic>> runSql(String query) async {
|
|
814
|
+
await _ensureAuth();
|
|
815
|
+
final res = await _http.post(
|
|
816
|
+
Uri.parse('$_baseUrl/api/sql'),
|
|
817
|
+
headers: {..._authHeaders, 'content-type': 'application/json'},
|
|
818
|
+
body: jsonEncode({'query': query}),
|
|
819
|
+
);
|
|
820
|
+
if (res.statusCode != 200) {
|
|
821
|
+
throw PocketBaseException(
|
|
822
|
+
'runSql failed: HTTP ${res.statusCode} ${res.body}',
|
|
823
|
+
);
|
|
824
|
+
}
|
|
825
|
+
return jsonDecode(res.body) as Map<String, dynamic>;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// ----- Crons -----
|
|
829
|
+
|
|
830
|
+
/// List all registered cron jobs.
|
|
831
|
+
Future<List<Map<String, dynamic>>> listCrons() async {
|
|
832
|
+
await _ensureAuth();
|
|
833
|
+
final res = await _http.get(
|
|
834
|
+
Uri.parse('$_baseUrl/api/crons'),
|
|
835
|
+
headers: _authHeaders,
|
|
836
|
+
);
|
|
837
|
+
if (res.statusCode != 200) {
|
|
838
|
+
throw PocketBaseException(
|
|
839
|
+
'listCrons failed: HTTP ${res.statusCode} ${res.body}',
|
|
840
|
+
);
|
|
841
|
+
}
|
|
842
|
+
return (jsonDecode(res.body) as List).cast<Map<String, dynamic>>();
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
/// Run a cron job by ID.
|
|
846
|
+
Future<void> runCron(String jobId) async {
|
|
847
|
+
await _ensureAuth();
|
|
848
|
+
final res = await _http.post(
|
|
849
|
+
Uri.parse('$_baseUrl/api/crons/$jobId'),
|
|
850
|
+
headers: _authHeaders,
|
|
851
|
+
);
|
|
852
|
+
if (res.statusCode != 204) {
|
|
853
|
+
throw PocketBaseException(
|
|
854
|
+
'runCron($jobId) failed: HTTP ${res.statusCode} ${res.body}',
|
|
855
|
+
);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// ----- Collections meta -----
|
|
860
|
+
|
|
861
|
+
/// Get collection scaffolds (default field templates).
|
|
862
|
+
Future<Map<String, dynamic>> getCollectionScaffolds() async {
|
|
863
|
+
await _ensureAuth();
|
|
864
|
+
final res = await _http.get(
|
|
865
|
+
Uri.parse('$_baseUrl/api/collections/meta/scaffolds'),
|
|
866
|
+
headers: _authHeaders,
|
|
867
|
+
);
|
|
868
|
+
if (res.statusCode != 200) {
|
|
869
|
+
throw PocketBaseException(
|
|
870
|
+
'getCollectionScaffolds failed: HTTP ${res.statusCode} ${res.body}',
|
|
871
|
+
);
|
|
872
|
+
}
|
|
873
|
+
return jsonDecode(res.body) as Map<String, dynamic>;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
/// List all configurable OAuth2 providers.
|
|
877
|
+
Future<List<Map<String, dynamic>>> listOAuth2Providers() async {
|
|
878
|
+
await _ensureAuth();
|
|
879
|
+
final res = await _http.get(
|
|
880
|
+
Uri.parse('$_baseUrl/api/collections/meta/oauth2-providers'),
|
|
881
|
+
headers: _authHeaders,
|
|
882
|
+
);
|
|
883
|
+
if (res.statusCode != 200) {
|
|
884
|
+
throw PocketBaseException(
|
|
885
|
+
'listOAuth2Providers failed: HTTP ${res.statusCode} ${res.body}',
|
|
886
|
+
);
|
|
887
|
+
}
|
|
888
|
+
return (jsonDecode(res.body) as List).cast<Map<String, dynamic>>();
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
/// Dry-run a view query and return sample records/fields.
|
|
892
|
+
Future<Map<String, dynamic>> dryRunViewQuery(String query) async {
|
|
893
|
+
await _ensureAuth();
|
|
894
|
+
final res = await _http.post(
|
|
895
|
+
Uri.parse('$_baseUrl/api/collections/meta/dry-run-view'),
|
|
896
|
+
headers: {..._authHeaders, 'content-type': 'application/json'},
|
|
897
|
+
body: jsonEncode({'query': query}),
|
|
898
|
+
);
|
|
899
|
+
if (res.statusCode != 200) {
|
|
900
|
+
throw PocketBaseException(
|
|
901
|
+
'dryRunViewQuery failed: HTTP ${res.statusCode} ${res.body}',
|
|
902
|
+
);
|
|
903
|
+
}
|
|
904
|
+
return jsonDecode(res.body) as Map<String, dynamic>;
|
|
905
|
+
}
|
|
906
|
+
|
|
323
907
|
void close() => _http.close();
|
|
324
908
|
}
|
|
325
909
|
|
|
@@ -347,3 +931,32 @@ class PocketBaseException implements Exception {
|
|
|
347
931
|
@override
|
|
348
932
|
String toString() => 'PocketBaseException: $message';
|
|
349
933
|
}
|
|
934
|
+
|
|
935
|
+
/// A PocketBase backup entry.
|
|
936
|
+
class PocketBaseBackup {
|
|
937
|
+
const PocketBaseBackup({
|
|
938
|
+
required this.key,
|
|
939
|
+
required this.modified,
|
|
940
|
+
required this.size,
|
|
941
|
+
});
|
|
942
|
+
|
|
943
|
+
factory PocketBaseBackup.fromJson(Map<String, dynamic> j) => PocketBaseBackup(
|
|
944
|
+
key: j['key'] as String? ?? '',
|
|
945
|
+
modified: j['modified'] as String? ?? '',
|
|
946
|
+
size: (j['size'] as num?)?.toInt() ?? 0,
|
|
947
|
+
);
|
|
948
|
+
|
|
949
|
+
final String key;
|
|
950
|
+
final String modified;
|
|
951
|
+
final int size;
|
|
952
|
+
|
|
953
|
+
String get sizeFormatted {
|
|
954
|
+
if (size >= 1024 * 1024) {
|
|
955
|
+
return '${(size / (1024 * 1024)).toStringAsFixed(1)} MB';
|
|
956
|
+
}
|
|
957
|
+
if (size >= 1024) {
|
|
958
|
+
return '${(size / 1024).toStringAsFixed(1)} KB';
|
|
959
|
+
}
|
|
960
|
+
return '$size B';
|
|
961
|
+
}
|
|
962
|
+
}
|