@_nazmiforreal/flutter-ota 0.1.18 → 0.1.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/flutter-ota-linux-x64 +0 -0
- package/dart-src/packages/cli-tools/.dart_tool/package_config.json +2 -0
- package/dart-src/packages/cli-tools/.dart_tool/package_graph.json +8 -8
- package/dart-src/packages/cli-tools/lib/flutter_ota_kit_cli.dart +2 -2
- package/dart-src/packages/cli-tools/lib/src/commands/pocketbase.dart +1379 -11
- package/dart-src/packages/cli-tools/lib/src/pocketbase/installer.dart +1 -1
- package/dart-src/packages/cli-tools/lib/src/pocketbase/process_manager.dart +104 -9
- package/dart-src/packages/cli-tools/lib/src/pocketbase/schema_installer.dart +59 -5
- package/dart-src/packages/cli-tools/pubspec.lock +7 -7
- package/dart-src/packages/cli-tools/pubspec.yaml +8 -8
- package/dart-src/packages/cli-tools/test/mocks/mock_pocketbase_client.dart +149 -0
- package/dart-src/packages/cli-tools/test/pocketbase_schema_test.dart +14 -9
- package/dart-src/packages/core/CHANGELOG.md +6 -1
- package/dart-src/packages/core/lib/src/app_update_info.dart +2 -0
- package/dart-src/packages/core/pubspec.yaml +1 -1
- package/dart-src/plugins/aws/CHANGELOG.md +5 -1
- package/dart-src/plugins/aws/pubspec.yaml +3 -3
- package/dart-src/plugins/cloudflare/CHANGELOG.md +5 -1
- package/dart-src/plugins/cloudflare/pubspec.yaml +3 -3
- package/dart-src/plugins/plugin-core/CHANGELOG.md +5 -1
- package/dart-src/plugins/plugin-core/pubspec.yaml +2 -2
- package/dart-src/plugins/pocketbase/CHANGELOG.md +5 -1
- package/dart-src/plugins/pocketbase/lib/flutter_ota_kit_pocketbase.dart +2 -1
- package/dart-src/plugins/pocketbase/lib/src/pocketbase_bundle_mapper.dart +10 -2
- package/dart-src/plugins/pocketbase/lib/src/pocketbase_client.dart +614 -4
- package/dart-src/plugins/pocketbase/lib/src/pocketbase_database.dart +262 -42
- package/dart-src/plugins/pocketbase/lib/src/pocketbase_storage.dart +9 -2
- package/dart-src/plugins/pocketbase/pubspec.yaml +3 -3
- package/dart-src/plugins/postgres/CHANGELOG.md +5 -1
- package/dart-src/plugins/postgres/pubspec.yaml +3 -3
- package/dart-src/plugins/supabase/CHANGELOG.md +6 -1
- package/dart-src/plugins/supabase/lib/src/supabase_database.dart +4 -0
- package/dart-src/plugins/supabase/pubspec.yaml +3 -3
- package/package.json +2 -2
|
@@ -2,11 +2,15 @@
|
|
|
2
2
|
library;
|
|
3
3
|
|
|
4
4
|
import 'dart:async';
|
|
5
|
+
import 'dart:convert';
|
|
5
6
|
import 'dart:io';
|
|
6
7
|
|
|
8
|
+
import 'package:args/args.dart';
|
|
9
|
+
import 'package:flutter_ota_kit_pocketbase/flutter_ota_kit_pocketbase.dart';
|
|
7
10
|
import 'package:path/path.dart' as p;
|
|
8
11
|
|
|
9
12
|
import '../cli_base.dart';
|
|
13
|
+
import '../pack.dart';
|
|
10
14
|
import '../pocketbase/data_bootstrap.dart';
|
|
11
15
|
import '../pocketbase/installer.dart';
|
|
12
16
|
import '../pocketbase/process_manager.dart';
|
|
@@ -14,11 +18,25 @@ import '../pocketbase/schema_installer.dart';
|
|
|
14
18
|
import '../ui/ui.dart';
|
|
15
19
|
|
|
16
20
|
class PocketBaseCommand extends FlutterPatcherCommand {
|
|
17
|
-
|
|
21
|
+
PocketBaseCommand() {
|
|
18
22
|
addSubcommand(PocketBaseInstallCommand());
|
|
19
23
|
addSubcommand(PocketBaseServeCommand());
|
|
20
24
|
addSubcommand(PocketBaseStopCommand());
|
|
21
25
|
addSubcommand(PocketBaseStatusCommand());
|
|
26
|
+
addSubcommand(PocketBaseBackupCommand());
|
|
27
|
+
addSubcommand(PocketBaseExportCommand());
|
|
28
|
+
addSubcommand(PocketBaseImportCommand());
|
|
29
|
+
addSubcommand(PocketBaseAdminCommand());
|
|
30
|
+
addSubcommand(PocketBaseMigrateCommand());
|
|
31
|
+
addSubcommand(PocketBaseHealthCommand());
|
|
32
|
+
addSubcommand(PocketBaseLogsCommand());
|
|
33
|
+
addSubcommand(PocketBaseRecordsCommand());
|
|
34
|
+
addSubcommand(PocketBaseCollectionsCommand());
|
|
35
|
+
addSubcommand(PocketBaseSettingsCommand());
|
|
36
|
+
addSubcommand(PocketBaseSqlCommand());
|
|
37
|
+
addSubcommand(PocketBaseCronsCommand());
|
|
38
|
+
addSubcommand(PocketBaseQueryCommand());
|
|
39
|
+
addSubcommand(PocketBaseConfigCommand());
|
|
22
40
|
}
|
|
23
41
|
|
|
24
42
|
@override
|
|
@@ -26,7 +44,9 @@ class PocketBaseCommand extends FlutterPatcherCommand {
|
|
|
26
44
|
|
|
27
45
|
@override
|
|
28
46
|
String get description =>
|
|
29
|
-
'Manage a local PocketBase instance (install / serve / stop /
|
|
47
|
+
'Manage a local PocketBase instance (install / serve / stop / '
|
|
48
|
+
'status / backup / export / import / admin / migrate / health / '
|
|
49
|
+
'logs / records / collections / settings / sql / crons).';
|
|
30
50
|
}
|
|
31
51
|
|
|
32
52
|
class PocketBaseInstallCommand extends FlutterPatcherCommand {
|
|
@@ -136,6 +156,18 @@ class PocketBaseServeCommand extends FlutterPatcherCommand {
|
|
|
136
156
|
await manager.start(adminEmail: adminEmail, adminPassword: adminPassword);
|
|
137
157
|
step('PocketBase is running (pid ${manager.process!.pid}).');
|
|
138
158
|
|
|
159
|
+
// Wait for PB to be ready.
|
|
160
|
+
for (var i = 0; i < 20; i++) {
|
|
161
|
+
try {
|
|
162
|
+
final hc = PocketBaseClient('http://$host:$port');
|
|
163
|
+
final ok = await hc.health();
|
|
164
|
+
hc.close();
|
|
165
|
+
if (ok) break;
|
|
166
|
+
} catch (_) {
|
|
167
|
+
await Future<void>.delayed(const Duration(milliseconds: 500));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
139
171
|
if (adminEmail != null && adminPassword != null) {
|
|
140
172
|
step('Installing flutter-ota schema...');
|
|
141
173
|
final schemaInstaller = PocketBaseSchemaInstaller(
|
|
@@ -209,26 +241,1362 @@ class PocketBaseStopCommand extends FlutterPatcherCommand {
|
|
|
209
241
|
}
|
|
210
242
|
|
|
211
243
|
class PocketBaseStatusCommand extends FlutterPatcherCommand {
|
|
244
|
+
PocketBaseStatusCommand() {
|
|
245
|
+
argParser.addOption('port', help: 'HTTP port.', defaultsTo: '8090');
|
|
246
|
+
argParser.addOption('host', help: 'Bind address.', defaultsTo: '127.0.0.1');
|
|
247
|
+
}
|
|
248
|
+
|
|
212
249
|
@override
|
|
213
250
|
String get name => 'status';
|
|
214
251
|
|
|
215
252
|
@override
|
|
216
|
-
String get description => 'Show
|
|
253
|
+
String get description => 'Show installed version, PID, and health.';
|
|
217
254
|
|
|
218
255
|
@override
|
|
219
256
|
Future<int> run() => runGuarded(() async {
|
|
220
257
|
banner('pocketbase · status');
|
|
258
|
+
final r = argResults!;
|
|
259
|
+
final port = int.tryParse(r['port'] as String? ?? '8090') ?? 8090;
|
|
260
|
+
final host = r['host'] as String? ?? '127.0.0.1';
|
|
261
|
+
|
|
221
262
|
final paths = PocketBaseInstallPaths.resolve();
|
|
222
263
|
final exists = await paths.binaryPath.exists();
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
264
|
+
final manager = PocketBaseProcessManager(
|
|
265
|
+
binaryPath: paths.binaryPath,
|
|
266
|
+
dataDir: paths.installDir,
|
|
267
|
+
port: port,
|
|
268
|
+
host: host,
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
final pid = manager.readPid();
|
|
272
|
+
final pidAlive = pid > 0 ? await manager.isRunningByPidFile() : false;
|
|
273
|
+
|
|
274
|
+
var healthy = false;
|
|
275
|
+
try {
|
|
276
|
+
final client = PocketBaseClient('http://$host:$port');
|
|
277
|
+
healthy = await client.health();
|
|
278
|
+
client.close();
|
|
279
|
+
} catch (_) {}
|
|
280
|
+
|
|
281
|
+
box('status', [
|
|
282
|
+
kv('version', paths.version),
|
|
283
|
+
kv('binary', exists ? green('installed') : red('not installed')),
|
|
284
|
+
kv('pid', pidAlive ? green('$pid') : (pid > 0 ? yellow('$pid (dead)') : dim('-'))),
|
|
285
|
+
kv('health', healthy ? green('healthy') : red('unreachable')),
|
|
286
|
+
kv('url', 'http://$host:$port'),
|
|
287
|
+
]);
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ---------------------------------------------------------------------------
|
|
292
|
+
// Shared helpers
|
|
293
|
+
// ---------------------------------------------------------------------------
|
|
294
|
+
|
|
295
|
+
void _addBackendOptions(ArgParser parser) {
|
|
296
|
+
parser.addOption('url', help: 'PocketBase URL (auto-detected if running).');
|
|
297
|
+
parser.addOption('admin-email', help: 'Admin email.');
|
|
298
|
+
parser.addOption('admin-password', help: 'Admin password.');
|
|
299
|
+
parser.addOption('version', help: 'PocketBase version.');
|
|
300
|
+
parser.addOption('data-dir', help: 'PB data directory.');
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
(String url, String adminEmail, String adminPassword) _resolveBackend(ArgResults r) {
|
|
304
|
+
final url = r['url'] as String? ?? _detectRunningUrl(r);
|
|
305
|
+
if (url == null || url.isEmpty) {
|
|
306
|
+
throw const PackException(
|
|
307
|
+
'No PocketBase URL. Run `flutter-ota pocketbase serve` or pass --url.',
|
|
308
|
+
64,
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
final admin = r['admin-email'] as String? ??
|
|
312
|
+
Platform.environment['POCKETBASE_ADMIN_EMAIL'] ??
|
|
313
|
+
'';
|
|
314
|
+
final pass = r['admin-password'] as String? ??
|
|
315
|
+
Platform.environment['POCKETBASE_ADMIN_PASSWORD'] ??
|
|
316
|
+
'';
|
|
317
|
+
if (admin.isEmpty || pass.isEmpty) {
|
|
318
|
+
throw const PackException(
|
|
319
|
+
'Admin credentials required. Use --admin-email/--admin-password '
|
|
320
|
+
'or set POCKETBASE_ADMIN_EMAIL/POCKETBASE_ADMIN_PASSWORD env vars.',
|
|
321
|
+
64,
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
return (url, admin, pass);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
Future<PocketBaseClient> _getClient(ArgResults r) async {
|
|
328
|
+
final (url, admin, pass) = _resolveBackend(r);
|
|
329
|
+
final client = PocketBaseClient(url);
|
|
330
|
+
await client.authenticate(admin, pass);
|
|
331
|
+
return client;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
String? _detectRunningUrl(ArgResults r) {
|
|
335
|
+
final version = r['version'] as String? ?? kDefaultPocketBaseVersion;
|
|
336
|
+
final dataDirOverride = r['data-dir'] as String?;
|
|
337
|
+
final paths = PocketBaseInstallPaths.resolve(version: version);
|
|
338
|
+
final dataDir = dataDirOverride != null
|
|
339
|
+
? Directory(dataDirOverride)
|
|
340
|
+
: paths.installDir;
|
|
341
|
+
final config = PocketBaseProcessManager(
|
|
342
|
+
binaryPath: paths.binaryPath,
|
|
343
|
+
dataDir: dataDir,
|
|
344
|
+
).readConfig();
|
|
345
|
+
return config?['url'] as String?;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
Future<T> _pbStep<T>(
|
|
349
|
+
String label,
|
|
350
|
+
Future<T> Function(PocketBaseClient client) fn,
|
|
351
|
+
ArgResults r,
|
|
352
|
+
) async {
|
|
353
|
+
final steps = Steps('pocketbase');
|
|
354
|
+
try {
|
|
355
|
+
final client = await _getClient(r);
|
|
356
|
+
try {
|
|
357
|
+
return await steps.run(label, () => fn(client));
|
|
358
|
+
} finally {
|
|
359
|
+
client.close();
|
|
360
|
+
}
|
|
361
|
+
} on PocketBaseException catch (e) {
|
|
362
|
+
final match = RegExp(r'"message":"([^"]+)"').firstMatch(e.message);
|
|
363
|
+
final detail = match?.group(1) ?? e.message;
|
|
364
|
+
throw PackException('$label: $detail', 1);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// ---------------------------------------------------------------------------
|
|
369
|
+
// backup
|
|
370
|
+
// ---------------------------------------------------------------------------
|
|
371
|
+
|
|
372
|
+
class PocketBaseBackupCommand extends FlutterPatcherCommand {
|
|
373
|
+
PocketBaseBackupCommand() {
|
|
374
|
+
addSubcommand(PocketBaseBackupCreateCommand());
|
|
375
|
+
addSubcommand(PocketBaseBackupListCommand());
|
|
376
|
+
addSubcommand(PocketBaseBackupDeleteCommand());
|
|
377
|
+
addSubcommand(PocketBaseBackupRestoreCommand());
|
|
378
|
+
addSubcommand(PocketBaseBackupDownloadCommand());
|
|
379
|
+
addSubcommand(PocketBaseBackupUploadCommand());
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
@override
|
|
383
|
+
String get name => 'backup';
|
|
384
|
+
|
|
385
|
+
@override
|
|
386
|
+
String get description => 'Manage PocketBase backups.';
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
class PocketBaseBackupCreateCommand extends FlutterPatcherCommand {
|
|
390
|
+
PocketBaseBackupCreateCommand() {
|
|
391
|
+
_addBackendOptions(argParser);
|
|
392
|
+
argParser.addOption('name', help: 'Backup file name.');
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
@override
|
|
396
|
+
String get name => 'create';
|
|
397
|
+
|
|
398
|
+
@override
|
|
399
|
+
String get description => 'Create a new backup.';
|
|
400
|
+
|
|
401
|
+
@override
|
|
402
|
+
Future<int> run() => runGuarded(() async {
|
|
403
|
+
banner('pocketbase · backup · create');
|
|
404
|
+
await _pbStep(
|
|
405
|
+
'Creating backup',
|
|
406
|
+
(c) => c.createBackup(name: argResults!['name'] as String?),
|
|
407
|
+
argResults!,
|
|
408
|
+
);
|
|
409
|
+
step(green('Backup created'));
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
class PocketBaseBackupListCommand extends FlutterPatcherCommand {
|
|
414
|
+
PocketBaseBackupListCommand() {
|
|
415
|
+
_addBackendOptions(argParser);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
@override
|
|
419
|
+
String get name => 'list';
|
|
420
|
+
|
|
421
|
+
@override
|
|
422
|
+
String get description => 'List available backups.';
|
|
423
|
+
|
|
424
|
+
@override
|
|
425
|
+
Future<int> run() => runGuarded(() async {
|
|
426
|
+
banner('pocketbase · backup · list');
|
|
427
|
+
final backups = await _pbStep(
|
|
428
|
+
'Listing backups',
|
|
429
|
+
(c) => c.listBackups(),
|
|
430
|
+
argResults!,
|
|
431
|
+
);
|
|
432
|
+
if (backups.isEmpty) {
|
|
433
|
+
warn('No backups found.');
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
final rows = backups
|
|
437
|
+
.map((b) => [cyan(b.key), b.sizeFormatted, b.modified])
|
|
438
|
+
.toList();
|
|
439
|
+
table('${backups.length} backups', ['KEY', 'SIZE', 'MODIFIED'], rows);
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
class PocketBaseBackupDeleteCommand extends FlutterPatcherCommand {
|
|
444
|
+
PocketBaseBackupDeleteCommand() {
|
|
445
|
+
_addBackendOptions(argParser);
|
|
446
|
+
argParser.addOption('name', help: 'Backup key to delete.', mandatory: true);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
@override
|
|
450
|
+
String get name => 'delete';
|
|
451
|
+
|
|
452
|
+
@override
|
|
453
|
+
String get description => 'Delete a backup.';
|
|
454
|
+
|
|
455
|
+
@override
|
|
456
|
+
Future<int> run() => runGuarded(() async {
|
|
457
|
+
banner('pocketbase · backup · delete');
|
|
458
|
+
final key = argResults!['name'] as String;
|
|
459
|
+
await _pbStep('Deleting $key', (c) => c.deleteBackup(key), argResults!);
|
|
460
|
+
step(green('Deleted backup $key'));
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
class PocketBaseBackupRestoreCommand extends FlutterPatcherCommand {
|
|
465
|
+
PocketBaseBackupRestoreCommand() {
|
|
466
|
+
_addBackendOptions(argParser);
|
|
467
|
+
argParser.addOption('name', help: 'Backup key to restore.', mandatory: true);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
@override
|
|
471
|
+
String get name => 'restore';
|
|
472
|
+
|
|
473
|
+
@override
|
|
474
|
+
String get description => 'Restore a backup (restarts PB).';
|
|
475
|
+
|
|
476
|
+
@override
|
|
477
|
+
Future<int> run() => runGuarded(() async {
|
|
478
|
+
banner('pocketbase · backup · restore');
|
|
479
|
+
final key = argResults!['name'] as String;
|
|
480
|
+
await _pbStep('Restoring $key', (c) => c.restoreBackup(key), argResults!);
|
|
481
|
+
warn('PB will restart. Reconnect in a few seconds.');
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
class PocketBaseBackupDownloadCommand extends FlutterPatcherCommand {
|
|
486
|
+
PocketBaseBackupDownloadCommand() {
|
|
487
|
+
_addBackendOptions(argParser);
|
|
488
|
+
argParser.addOption('name', help: 'Backup key to download.', mandatory: true);
|
|
489
|
+
argParser.addOption('output', help: 'Output directory.', defaultsTo: '.');
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
@override
|
|
493
|
+
String get name => 'download';
|
|
494
|
+
|
|
495
|
+
@override
|
|
496
|
+
String get description => 'Download a backup file.';
|
|
497
|
+
|
|
498
|
+
@override
|
|
499
|
+
Future<int> run() => runGuarded(() async {
|
|
500
|
+
banner('pocketbase · backup · download');
|
|
501
|
+
final key = argResults!['name'] as String;
|
|
502
|
+
final output = argResults!['output'] as String;
|
|
503
|
+
final token = await _pbStep('Getting file token', (c) => c.getFileToken('', ''), argResults!);
|
|
504
|
+
final (url, _, _) = _resolveBackend(argResults!);
|
|
505
|
+
final dlClient = PocketBaseClient(url);
|
|
506
|
+
final url2 = dlClient.backupDownloadUrl(key, token);
|
|
507
|
+
final bytes = await dlClient.downloadFileUnauth(url2);
|
|
508
|
+
dlClient.close();
|
|
509
|
+
final out = File(p.join(output, key));
|
|
510
|
+
await out.parent.create(recursive: true);
|
|
511
|
+
await out.writeAsBytes(bytes);
|
|
512
|
+
step(green('Saved to ${out.path}'));
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
class PocketBaseBackupUploadCommand extends FlutterPatcherCommand {
|
|
517
|
+
PocketBaseBackupUploadCommand() {
|
|
518
|
+
_addBackendOptions(argParser);
|
|
519
|
+
argParser.addOption('file', help: 'Backup zip to upload.', mandatory: true);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
@override
|
|
523
|
+
String get name => 'upload';
|
|
524
|
+
|
|
525
|
+
@override
|
|
526
|
+
String get description => 'Upload a backup zip file.';
|
|
527
|
+
|
|
528
|
+
@override
|
|
529
|
+
Future<int> run() => runGuarded(() async {
|
|
530
|
+
banner('pocketbase · backup · upload');
|
|
531
|
+
final fp = argResults!['file'] as String;
|
|
532
|
+
final f = File(fp);
|
|
533
|
+
if (!await f.exists()) throw PackException('File not found: $fp', 64);
|
|
534
|
+
final bytes = await f.readAsBytes();
|
|
535
|
+
await _pbStep(
|
|
536
|
+
'Uploading ${p.basename(fp)}',
|
|
537
|
+
(c) => c.uploadBackup(bytes, name: p.basename(fp)),
|
|
538
|
+
argResults!,
|
|
539
|
+
);
|
|
540
|
+
step(green('Uploaded ${p.basename(fp)}'));
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// ---------------------------------------------------------------------------
|
|
545
|
+
// export / import
|
|
546
|
+
// ---------------------------------------------------------------------------
|
|
547
|
+
|
|
548
|
+
class PocketBaseExportCommand extends FlutterPatcherCommand {
|
|
549
|
+
PocketBaseExportCommand() {
|
|
550
|
+
_addBackendOptions(argParser);
|
|
551
|
+
argParser.addOption('collection', help: 'Collection to export.');
|
|
552
|
+
argParser.addOption('output', help: 'Output directory.', defaultsTo: '.');
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
@override
|
|
556
|
+
String get name => 'export';
|
|
557
|
+
|
|
558
|
+
@override
|
|
559
|
+
String get description =>
|
|
560
|
+
'Export collection(s) as JSON. Exports all if --collection omitted.';
|
|
561
|
+
|
|
562
|
+
@override
|
|
563
|
+
Future<int> run() => runGuarded(() async {
|
|
564
|
+
banner('pocketbase · export');
|
|
565
|
+
final r = argResults!;
|
|
566
|
+
final collection = r['collection'] as String?;
|
|
567
|
+
final output = r['output'] as String;
|
|
568
|
+
|
|
569
|
+
final client = PocketBaseClient(_resolveBackend(r).$1);
|
|
570
|
+
await client.authenticate(_resolveBackend(r).$2, _resolveBackend(r).$3);
|
|
571
|
+
|
|
572
|
+
final collections = <String>[];
|
|
573
|
+
if (collection != null && collection.isNotEmpty) {
|
|
574
|
+
collections.add(collection);
|
|
227
575
|
} else {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
576
|
+
final all = await client.listCollections();
|
|
577
|
+
for (final c in all) {
|
|
578
|
+
final name = c['name'] as String? ?? '';
|
|
579
|
+
if (name.isNotEmpty && !name.startsWith('_') && c['system'] != true) {
|
|
580
|
+
collections.add(name);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
if (collections.isEmpty) {
|
|
584
|
+
collections.addAll(['bundles', 'channels', 'audit_log', 'bundles_patches']);
|
|
585
|
+
}
|
|
231
586
|
}
|
|
232
|
-
|
|
587
|
+
|
|
588
|
+
for (final c in collections) {
|
|
589
|
+
try {
|
|
590
|
+
final records = await client.exportCollection(c);
|
|
591
|
+
final outFile = File(p.join(output, '$c.json'));
|
|
592
|
+
await outFile.parent.create(recursive: true);
|
|
593
|
+
await outFile.writeAsString(
|
|
594
|
+
const JsonEncoder.withIndent(' ').convert(records),
|
|
595
|
+
);
|
|
596
|
+
step('${records.length} records → ${outFile.path}');
|
|
597
|
+
} catch (e) {
|
|
598
|
+
warn('Failed to export $c: $e');
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
client.close();
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
class PocketBaseImportCommand extends FlutterPatcherCommand {
|
|
606
|
+
PocketBaseImportCommand() {
|
|
607
|
+
_addBackendOptions(argParser);
|
|
608
|
+
argParser.addOption('file', help: 'JSON file to import.', mandatory: true);
|
|
609
|
+
argParser.addOption('collection', help: 'Target collection name.');
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
@override
|
|
613
|
+
String get name => 'import';
|
|
614
|
+
|
|
615
|
+
@override
|
|
616
|
+
String get description => 'Import records from a JSON file into a collection.';
|
|
617
|
+
|
|
618
|
+
@override
|
|
619
|
+
Future<int> run() => runGuarded(() async {
|
|
620
|
+
banner('pocketbase · import');
|
|
621
|
+
final r = argResults!;
|
|
622
|
+
final fp = r['file'] as String;
|
|
623
|
+
final f = File(fp);
|
|
624
|
+
if (!await f.exists()) throw PackException('File not found: $fp', 64);
|
|
625
|
+
final json = jsonDecode(await f.readAsString()) as List<dynamic>;
|
|
626
|
+
final records = json.cast<Map<String, dynamic>>();
|
|
627
|
+
final target = r['collection'] as String? ?? p.basenameWithoutExtension(fp);
|
|
628
|
+
await _pbStep(
|
|
629
|
+
'Importing ${records.length} records into $target',
|
|
630
|
+
(c) => c.importCollection(target, records),
|
|
631
|
+
r,
|
|
632
|
+
);
|
|
633
|
+
step(green('${records.length} records imported into $target'));
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// ---------------------------------------------------------------------------
|
|
638
|
+
// admin
|
|
639
|
+
// ---------------------------------------------------------------------------
|
|
640
|
+
|
|
641
|
+
class PocketBaseAdminCommand extends FlutterPatcherCommand {
|
|
642
|
+
PocketBaseAdminCommand() {
|
|
643
|
+
addSubcommand(PocketBaseAdminCreateCommand());
|
|
644
|
+
addSubcommand(PocketBaseAdminListCommand());
|
|
645
|
+
addSubcommand(PocketBaseAdminUpdateCommand());
|
|
646
|
+
addSubcommand(PocketBaseAdminDeleteCommand());
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
@override
|
|
650
|
+
String get name => 'admin';
|
|
651
|
+
|
|
652
|
+
@override
|
|
653
|
+
String get description => 'Manage PocketBase admin accounts.';
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
class PocketBaseAdminCreateCommand extends FlutterPatcherCommand {
|
|
657
|
+
PocketBaseAdminCreateCommand() {
|
|
658
|
+
_addBackendOptions(argParser);
|
|
659
|
+
argParser.addOption('email', help: 'Admin email.', mandatory: true);
|
|
660
|
+
argParser.addOption('password', help: 'Admin password.', mandatory: true);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
@override
|
|
664
|
+
String get name => 'create';
|
|
665
|
+
|
|
666
|
+
@override
|
|
667
|
+
String get description => 'Create a new admin account.';
|
|
668
|
+
|
|
669
|
+
@override
|
|
670
|
+
Future<int> run() => runGuarded(() async {
|
|
671
|
+
banner('pocketbase · admin · create');
|
|
672
|
+
final r = argResults!;
|
|
673
|
+
final email = r['email'] as String;
|
|
674
|
+
final password = r['password'] as String;
|
|
675
|
+
final result = await _pbStep(
|
|
676
|
+
'Creating admin $email',
|
|
677
|
+
(c) => c.createAdmin(email: email, password: password, passwordConfirm: password),
|
|
678
|
+
r,
|
|
679
|
+
);
|
|
680
|
+
box('admin', [kv('id', cyan(result['id'] ?? '?')), kv('email', email)]);
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
class PocketBaseAdminListCommand extends FlutterPatcherCommand {
|
|
685
|
+
PocketBaseAdminListCommand() {
|
|
686
|
+
_addBackendOptions(argParser);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
@override
|
|
690
|
+
String get name => 'list';
|
|
691
|
+
|
|
692
|
+
@override
|
|
693
|
+
String get description => 'List admin accounts.';
|
|
694
|
+
|
|
695
|
+
@override
|
|
696
|
+
Future<int> run() => runGuarded(() async {
|
|
697
|
+
banner('pocketbase · admin · list');
|
|
698
|
+
final admins = await _pbStep('Listing admins', (c) => c.listAdmins(), argResults!);
|
|
699
|
+
if (admins.isEmpty) {
|
|
700
|
+
warn('No admins found.');
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
final rows = admins
|
|
704
|
+
.map((a) => [cyan(a['id']?.toString() ?? '?'), a['email']?.toString() ?? '?'])
|
|
705
|
+
.toList();
|
|
706
|
+
table('${admins.length} admins', ['ID', 'EMAIL'], rows);
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
class PocketBaseAdminUpdateCommand extends FlutterPatcherCommand {
|
|
711
|
+
PocketBaseAdminUpdateCommand() {
|
|
712
|
+
_addBackendOptions(argParser);
|
|
713
|
+
argParser.addOption('id', help: 'Admin ID to update.', mandatory: true);
|
|
714
|
+
argParser.addOption('body', help: 'JSON fields to update.', mandatory: true);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
@override
|
|
718
|
+
String get name => 'update';
|
|
719
|
+
|
|
720
|
+
@override
|
|
721
|
+
String get description => 'Update an admin account.';
|
|
722
|
+
|
|
723
|
+
@override
|
|
724
|
+
Future<int> run() => runGuarded(() async {
|
|
725
|
+
banner('pocketbase · admin · update');
|
|
726
|
+
final r = argResults!;
|
|
727
|
+
final id = r['id'] as String;
|
|
728
|
+
final body = jsonDecode(r['body'] as String) as Map<String, dynamic>;
|
|
729
|
+
await _pbStep('Updating admin $id', (c) => c.updateAdmin(id, body), r);
|
|
730
|
+
step(green('Updated admin $id'));
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
class PocketBaseAdminDeleteCommand extends FlutterPatcherCommand {
|
|
735
|
+
PocketBaseAdminDeleteCommand() {
|
|
736
|
+
_addBackendOptions(argParser);
|
|
737
|
+
argParser.addOption('id', help: 'Admin ID to delete.', mandatory: true);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
@override
|
|
741
|
+
String get name => 'delete';
|
|
742
|
+
|
|
743
|
+
@override
|
|
744
|
+
String get description => 'Delete an admin account.';
|
|
745
|
+
|
|
746
|
+
@override
|
|
747
|
+
Future<int> run() => runGuarded(() async {
|
|
748
|
+
banner('pocketbase · admin · delete');
|
|
749
|
+
final id = argResults!['id'] as String;
|
|
750
|
+
await _pbStep('Deleting admin $id', (c) => c.deleteAdmin(id), argResults!);
|
|
751
|
+
step(green('Deleted admin $id'));
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// ---------------------------------------------------------------------------
|
|
756
|
+
// migrate
|
|
757
|
+
// ---------------------------------------------------------------------------
|
|
758
|
+
|
|
759
|
+
class PocketBaseMigrateCommand extends FlutterPatcherCommand {
|
|
760
|
+
PocketBaseMigrateCommand() {
|
|
761
|
+
_addBackendOptions(argParser);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
@override
|
|
765
|
+
String get name => 'migrate';
|
|
766
|
+
|
|
767
|
+
@override
|
|
768
|
+
String get description => 'Re-run schema migration on a running PocketBase.';
|
|
769
|
+
|
|
770
|
+
@override
|
|
771
|
+
Future<int> run() => runGuarded(() async {
|
|
772
|
+
banner('pocketbase · migrate');
|
|
773
|
+
final (url, admin, pass) = _resolveBackend(argResults!);
|
|
774
|
+
final installer = PocketBaseSchemaInstaller(
|
|
775
|
+
url: url,
|
|
776
|
+
adminEmail: admin,
|
|
777
|
+
adminPassword: pass,
|
|
778
|
+
);
|
|
779
|
+
final res = await installer.install();
|
|
780
|
+
if (res.created.isNotEmpty) {
|
|
781
|
+
step(green('Created: ${res.created.join(', ')}'));
|
|
782
|
+
}
|
|
783
|
+
if (res.skipped.isNotEmpty) {
|
|
784
|
+
step('Already present: ${res.skipped.join(', ')}');
|
|
785
|
+
}
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// ---------------------------------------------------------------------------
|
|
790
|
+
// health
|
|
791
|
+
// ---------------------------------------------------------------------------
|
|
792
|
+
|
|
793
|
+
class PocketBaseHealthCommand extends FlutterPatcherCommand {
|
|
794
|
+
PocketBaseHealthCommand() {
|
|
795
|
+
_addBackendOptions(argParser);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
@override
|
|
799
|
+
String get name => 'health';
|
|
800
|
+
|
|
801
|
+
@override
|
|
802
|
+
String get description => 'Check PocketBase health.';
|
|
803
|
+
|
|
804
|
+
@override
|
|
805
|
+
Future<int> run() => runGuarded(() async {
|
|
806
|
+
banner('pocketbase · health');
|
|
807
|
+
final health = await _pbStep('health', (c) => c.healthDetailed(), argResults!);
|
|
808
|
+
final code = health['code'] as int? ?? 0;
|
|
809
|
+
final message = health['message'] as String? ?? '';
|
|
810
|
+
final data = health['data'] as Map<String, dynamic>? ?? {};
|
|
811
|
+
box('health', [
|
|
812
|
+
kv('status', code == 200 ? green('healthy') : red('unhealthy')),
|
|
813
|
+
kv('message', message),
|
|
814
|
+
if (data.isNotEmpty) kv('canBackup', '${data['canBackup'] ?? false}'),
|
|
815
|
+
]);
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// ---------------------------------------------------------------------------
|
|
820
|
+
// logs
|
|
821
|
+
// ---------------------------------------------------------------------------
|
|
822
|
+
|
|
823
|
+
class PocketBaseLogsCommand extends FlutterPatcherCommand {
|
|
824
|
+
PocketBaseLogsCommand() {
|
|
825
|
+
addSubcommand(PocketBaseLogsListCommand());
|
|
826
|
+
addSubcommand(PocketBaseLogsStatsCommand());
|
|
827
|
+
addSubcommand(PocketBaseLogsTruncateCommand());
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
@override
|
|
831
|
+
String get name => 'logs';
|
|
832
|
+
|
|
833
|
+
@override
|
|
834
|
+
String get description => 'Manage PocketBase logs.';
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
class PocketBaseLogsListCommand extends FlutterPatcherCommand {
|
|
838
|
+
PocketBaseLogsListCommand() {
|
|
839
|
+
_addBackendOptions(argParser);
|
|
840
|
+
argParser.addOption('filter', help: 'PB filter expression.');
|
|
841
|
+
argParser.addOption('sort', help: 'Sort field.', defaultsTo: '-created');
|
|
842
|
+
argParser.addOption('page', help: 'Page number.', defaultsTo: '1');
|
|
843
|
+
argParser.addOption('per-page', help: 'Items per page.', defaultsTo: '30');
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
@override
|
|
847
|
+
String get name => 'list';
|
|
848
|
+
|
|
849
|
+
@override
|
|
850
|
+
String get description => 'List request logs.';
|
|
851
|
+
|
|
852
|
+
@override
|
|
853
|
+
Future<int> run() => runGuarded(() async {
|
|
854
|
+
banner('pocketbase · logs · list');
|
|
855
|
+
final r = argResults!;
|
|
856
|
+
final data = await _pbStep(
|
|
857
|
+
'Fetching logs',
|
|
858
|
+
(c) => c.listLogs(
|
|
859
|
+
filter: r['filter'] as String?,
|
|
860
|
+
sort: r['sort'] as String?,
|
|
861
|
+
page: int.tryParse(r['page'] as String? ?? '1') ?? 1,
|
|
862
|
+
perPage: int.tryParse(r['per-page'] as String? ?? '30') ?? 30,
|
|
863
|
+
),
|
|
864
|
+
r,
|
|
865
|
+
);
|
|
866
|
+
final items = (data['items'] as List? ?? []).cast<Map<String, dynamic>>();
|
|
867
|
+
final total = data['totalItems'] as int? ?? 0;
|
|
868
|
+
if (items.isEmpty) {
|
|
869
|
+
warn('No logs found.');
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
final rows = items.map((log) {
|
|
873
|
+
final level = log['level'] as int? ?? 0;
|
|
874
|
+
final lvl = level == 0 ? green('INFO') : level == 1 ? yellow('WARN') : red('ERROR');
|
|
875
|
+
final msg = (log['message'] as String? ?? '').length > 60
|
|
876
|
+
? '${(log['message'] as String).substring(0, 57)}...'
|
|
877
|
+
: log['message'] as String? ?? '';
|
|
878
|
+
return [lvl, log['created']?.toString().substring(0, 19) ?? '', msg];
|
|
879
|
+
}).toList();
|
|
880
|
+
table('$total logs', ['LEVEL', 'TIME', 'MESSAGE'], rows);
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
class PocketBaseLogsStatsCommand extends FlutterPatcherCommand {
|
|
885
|
+
PocketBaseLogsStatsCommand() {
|
|
886
|
+
_addBackendOptions(argParser);
|
|
887
|
+
argParser.addOption('filter', help: 'PB filter expression.');
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
@override
|
|
891
|
+
String get name => 'stats';
|
|
892
|
+
|
|
893
|
+
@override
|
|
894
|
+
String get description => 'Show hourly log statistics.';
|
|
895
|
+
|
|
896
|
+
@override
|
|
897
|
+
Future<int> run() => runGuarded(() async {
|
|
898
|
+
banner('pocketbase · logs · stats');
|
|
899
|
+
final stats = await _pbStep(
|
|
900
|
+
'Fetching log stats',
|
|
901
|
+
(c) => c.getLogStats(filter: argResults!['filter'] as String?),
|
|
902
|
+
argResults!,
|
|
903
|
+
);
|
|
904
|
+
if (stats.isEmpty) {
|
|
905
|
+
warn('No stats available.');
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
final rows = stats.map((s) => [
|
|
909
|
+
s['date']?.toString().substring(0, 16) ?? '',
|
|
910
|
+
'${s['total'] ?? 0}',
|
|
911
|
+
]).toList();
|
|
912
|
+
table('${stats.length} entries', ['TIME', 'COUNT'], rows);
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
class PocketBaseLogsTruncateCommand extends FlutterPatcherCommand {
|
|
917
|
+
PocketBaseLogsTruncateCommand() {
|
|
918
|
+
_addBackendOptions(argParser);
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
@override
|
|
922
|
+
String get name => 'truncate';
|
|
923
|
+
|
|
924
|
+
@override
|
|
925
|
+
String get description => 'Delete all logs.';
|
|
926
|
+
|
|
927
|
+
@override
|
|
928
|
+
Future<int> run() => runGuarded(() async {
|
|
929
|
+
banner('pocketbase · logs · truncate');
|
|
930
|
+
await _pbStep('Truncating logs', (c) => c.truncateLogs(), argResults!);
|
|
931
|
+
step(green('All logs deleted'));
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
// ---------------------------------------------------------------------------
|
|
936
|
+
// records
|
|
937
|
+
// ---------------------------------------------------------------------------
|
|
938
|
+
|
|
939
|
+
class PocketBaseRecordsCommand extends FlutterPatcherCommand {
|
|
940
|
+
PocketBaseRecordsCommand() {
|
|
941
|
+
addSubcommand(PocketBaseRecordsListCommand());
|
|
942
|
+
addSubcommand(PocketBaseRecordsGetCommand());
|
|
943
|
+
addSubcommand(PocketBaseRecordsCreateCommand());
|
|
944
|
+
addSubcommand(PocketBaseRecordsUpdateCommand());
|
|
945
|
+
addSubcommand(PocketBaseRecordsDeleteCommand());
|
|
946
|
+
addSubcommand(PocketBaseRecordsBatchCommand());
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
@override
|
|
950
|
+
String get name => 'records';
|
|
951
|
+
|
|
952
|
+
@override
|
|
953
|
+
String get description => 'CRUD operations on collection records.';
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
class PocketBaseRecordsListCommand extends FlutterPatcherCommand {
|
|
957
|
+
PocketBaseRecordsListCommand() {
|
|
958
|
+
_addBackendOptions(argParser);
|
|
959
|
+
argParser.addOption('collection', help: 'Collection name.', mandatory: true);
|
|
960
|
+
argParser.addOption('filter', help: 'PB filter expression.');
|
|
961
|
+
argParser.addOption('sort', help: 'Sort field.');
|
|
962
|
+
argParser.addOption('page', help: 'Page number.', defaultsTo: '1');
|
|
963
|
+
argParser.addOption('per-page', help: 'Items per page.', defaultsTo: '30');
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
@override
|
|
967
|
+
String get name => 'list';
|
|
968
|
+
|
|
969
|
+
@override
|
|
970
|
+
String get description => 'List records in a collection.';
|
|
971
|
+
|
|
972
|
+
@override
|
|
973
|
+
Future<int> run() => runGuarded(() async {
|
|
974
|
+
banner('pocketbase · records · list');
|
|
975
|
+
final r = argResults!;
|
|
976
|
+
final data = await _pbStep(
|
|
977
|
+
'Listing ${r['collection']}',
|
|
978
|
+
(c) => c.listRecords<dynamic>(
|
|
979
|
+
r['collection'] as String,
|
|
980
|
+
(j) => j,
|
|
981
|
+
filter: r['filter'] as String?,
|
|
982
|
+
sort: r['sort'] as String?,
|
|
983
|
+
page: int.tryParse(r['page'] as String? ?? '1') ?? 1,
|
|
984
|
+
perPage: int.tryParse(r['per-page'] as String? ?? '30') ?? 30,
|
|
985
|
+
),
|
|
986
|
+
r,
|
|
987
|
+
);
|
|
988
|
+
if (data.items.isEmpty) {
|
|
989
|
+
warn('No records found.');
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
final rows = data.items.map((r) {
|
|
993
|
+
final m = r as Map<String, dynamic>;
|
|
994
|
+
return [cyan(m['id']?.toString() ?? '?'), m.values.first?.toString() ?? ''];
|
|
995
|
+
}).toList();
|
|
996
|
+
table('${data.totalItems} records', ['ID', 'FIRST FIELD'], rows);
|
|
997
|
+
});
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
class PocketBaseRecordsGetCommand extends FlutterPatcherCommand {
|
|
1001
|
+
PocketBaseRecordsGetCommand() {
|
|
1002
|
+
_addBackendOptions(argParser);
|
|
1003
|
+
argParser.addOption('collection', help: 'Collection name.', mandatory: true);
|
|
1004
|
+
argParser.addOption('id', help: 'Record ID.', mandatory: true);
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
@override
|
|
1008
|
+
String get name => 'get';
|
|
1009
|
+
|
|
1010
|
+
@override
|
|
1011
|
+
String get description => 'Get a single record.';
|
|
1012
|
+
|
|
1013
|
+
@override
|
|
1014
|
+
Future<int> run() => runGuarded(() async {
|
|
1015
|
+
banner('pocketbase · records · get');
|
|
1016
|
+
final r = argResults!;
|
|
1017
|
+
final record = await _pbStep(
|
|
1018
|
+
'Getting ${r['collection']}/${r['id']}',
|
|
1019
|
+
(c) => c.getRecord<dynamic>(r['collection'] as String, r['id'] as String, (j) => j),
|
|
1020
|
+
r,
|
|
1021
|
+
);
|
|
1022
|
+
if (record == null) {
|
|
1023
|
+
warn('Record not found.');
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
1026
|
+
stdout.writeln(const JsonEncoder.withIndent(' ').convert(record));
|
|
1027
|
+
});
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
class PocketBaseRecordsCreateCommand extends FlutterPatcherCommand {
|
|
1031
|
+
PocketBaseRecordsCreateCommand() {
|
|
1032
|
+
_addBackendOptions(argParser);
|
|
1033
|
+
argParser.addOption('collection', help: 'Collection name.', mandatory: true);
|
|
1034
|
+
argParser.addOption('body', help: 'JSON body.', mandatory: true);
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
@override
|
|
1038
|
+
String get name => 'create';
|
|
1039
|
+
|
|
1040
|
+
@override
|
|
1041
|
+
String get description => 'Create a record.';
|
|
1042
|
+
|
|
1043
|
+
@override
|
|
1044
|
+
Future<int> run() => runGuarded(() async {
|
|
1045
|
+
banner('pocketbase · records · create');
|
|
1046
|
+
final r = argResults!;
|
|
1047
|
+
final body = jsonDecode(r['body'] as String) as Map<String, dynamic>;
|
|
1048
|
+
final record = await _pbStep(
|
|
1049
|
+
'Creating record in ${r['collection']}',
|
|
1050
|
+
(c) => c.createRecord<dynamic>(r['collection'] as String, body, (j) => j),
|
|
1051
|
+
r,
|
|
1052
|
+
);
|
|
1053
|
+
box('record', [kv('id', cyan((record as Map)['id']?.toString() ?? '?'))]);
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
class PocketBaseRecordsUpdateCommand extends FlutterPatcherCommand {
|
|
1058
|
+
PocketBaseRecordsUpdateCommand() {
|
|
1059
|
+
_addBackendOptions(argParser);
|
|
1060
|
+
argParser.addOption('collection', help: 'Collection name.', mandatory: true);
|
|
1061
|
+
argParser.addOption('id', help: 'Record ID.', mandatory: true);
|
|
1062
|
+
argParser.addOption('body', help: 'JSON body.', mandatory: true);
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
@override
|
|
1066
|
+
String get name => 'update';
|
|
1067
|
+
|
|
1068
|
+
@override
|
|
1069
|
+
String get description => 'Update a record.';
|
|
1070
|
+
|
|
1071
|
+
@override
|
|
1072
|
+
Future<int> run() => runGuarded(() async {
|
|
1073
|
+
banner('pocketbase · records · update');
|
|
1074
|
+
final r = argResults!;
|
|
1075
|
+
final body = jsonDecode(r['body'] as String) as Map<String, dynamic>;
|
|
1076
|
+
await _pbStep(
|
|
1077
|
+
'Updating ${r['collection']}/${r['id']}',
|
|
1078
|
+
(c) => c.updateRecord<dynamic>(r['collection'] as String, r['id'] as String, body, (j) => j),
|
|
1079
|
+
r,
|
|
1080
|
+
);
|
|
1081
|
+
step(green('Updated ${r['collection']}/${r['id']}'));
|
|
1082
|
+
});
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
class PocketBaseRecordsDeleteCommand extends FlutterPatcherCommand {
|
|
1086
|
+
PocketBaseRecordsDeleteCommand() {
|
|
1087
|
+
_addBackendOptions(argParser);
|
|
1088
|
+
argParser.addOption('collection', help: 'Collection name.', mandatory: true);
|
|
1089
|
+
argParser.addOption('id', help: 'Record ID.', mandatory: true);
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
@override
|
|
1093
|
+
String get name => 'delete';
|
|
1094
|
+
|
|
1095
|
+
@override
|
|
1096
|
+
String get description => 'Delete a record.';
|
|
1097
|
+
|
|
1098
|
+
@override
|
|
1099
|
+
Future<int> run() => runGuarded(() async {
|
|
1100
|
+
banner('pocketbase · records · delete');
|
|
1101
|
+
final r = argResults!;
|
|
1102
|
+
await _pbStep(
|
|
1103
|
+
'Deleting ${r['collection']}/${r['id']}',
|
|
1104
|
+
(c) => c.deleteRecord(r['collection'] as String, r['id'] as String),
|
|
1105
|
+
r,
|
|
1106
|
+
);
|
|
1107
|
+
step(green('Deleted ${r['collection']}/${r['id']}'));
|
|
1108
|
+
});
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
class PocketBaseRecordsBatchCommand extends FlutterPatcherCommand {
|
|
1112
|
+
PocketBaseRecordsBatchCommand() {
|
|
1113
|
+
_addBackendOptions(argParser);
|
|
1114
|
+
argParser.addOption('requests', help: 'JSON array of request objects.', mandatory: true);
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
@override
|
|
1118
|
+
String get name => 'batch';
|
|
1119
|
+
|
|
1120
|
+
@override
|
|
1121
|
+
String get description => 'Execute multiple record operations in a single transaction.';
|
|
1122
|
+
|
|
1123
|
+
@override
|
|
1124
|
+
Future<int> run() => runGuarded(() async {
|
|
1125
|
+
banner('pocketbase · records · batch');
|
|
1126
|
+
final requests = jsonDecode(argResults!['requests'] as String) as List<dynamic>;
|
|
1127
|
+
final body = requests.cast<Map<String, dynamic>>();
|
|
1128
|
+
final results = await _pbStep(
|
|
1129
|
+
'Executing ${body.length} batch requests',
|
|
1130
|
+
(c) => c.batch(body),
|
|
1131
|
+
argResults!,
|
|
1132
|
+
);
|
|
1133
|
+
for (final (i, r) in results.indexed) {
|
|
1134
|
+
final status = r['status'] as int? ?? 0;
|
|
1135
|
+
final ok = status >= 200 && status < 300;
|
|
1136
|
+
step('${ok ? green('ok') : red('fail')} [${i + 1}] HTTP $status');
|
|
1137
|
+
}
|
|
1138
|
+
});
|
|
1139
|
+
}
|
|
1140
|
+
// ---------------------------------------------------------------------------
|
|
1141
|
+
|
|
1142
|
+
class PocketBaseCollectionsCommand extends FlutterPatcherCommand {
|
|
1143
|
+
PocketBaseCollectionsCommand() {
|
|
1144
|
+
addSubcommand(PocketBaseCollectionsListCommand());
|
|
1145
|
+
addSubcommand(PocketBaseCollectionsViewCommand());
|
|
1146
|
+
addSubcommand(PocketBaseCollectionsCreateCommand());
|
|
1147
|
+
addSubcommand(PocketBaseCollectionsUpdateCommand());
|
|
1148
|
+
addSubcommand(PocketBaseCollectionsDeleteCommand());
|
|
1149
|
+
addSubcommand(PocketBaseCollectionsTruncateCommand());
|
|
1150
|
+
addSubcommand(PocketBaseCollectionsImportCommand());
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
@override
|
|
1154
|
+
String get name => 'collections';
|
|
1155
|
+
|
|
1156
|
+
@override
|
|
1157
|
+
String get description => 'Manage PocketBase collections.';
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
class PocketBaseCollectionsListCommand extends FlutterPatcherCommand {
|
|
1161
|
+
PocketBaseCollectionsListCommand() {
|
|
1162
|
+
_addBackendOptions(argParser);
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
@override
|
|
1166
|
+
String get name => 'list';
|
|
1167
|
+
|
|
1168
|
+
@override
|
|
1169
|
+
String get description => 'List all collections.';
|
|
1170
|
+
|
|
1171
|
+
@override
|
|
1172
|
+
Future<int> run() => runGuarded(() async {
|
|
1173
|
+
banner('pocketbase · collections · list');
|
|
1174
|
+
final cols = await _pbStep('Listing collections', (c) => c.listCollections(), argResults!);
|
|
1175
|
+
if (cols.isEmpty) {
|
|
1176
|
+
warn('No collections found.');
|
|
1177
|
+
return;
|
|
1178
|
+
}
|
|
1179
|
+
final rows = cols.map((c) => [
|
|
1180
|
+
cyan(c['name']?.toString() ?? '?'),
|
|
1181
|
+
c['type']?.toString() ?? '?',
|
|
1182
|
+
'${(c['fields'] as List?)?.length ?? 0} fields',
|
|
1183
|
+
]).toList();
|
|
1184
|
+
table('${cols.length} collections', ['NAME', 'TYPE', 'FIELDS'], rows);
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
class PocketBaseCollectionsViewCommand extends FlutterPatcherCommand {
|
|
1189
|
+
PocketBaseCollectionsViewCommand() {
|
|
1190
|
+
_addBackendOptions(argParser);
|
|
1191
|
+
argParser.addOption('name', help: 'Collection name or ID.', mandatory: true);
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
@override
|
|
1195
|
+
String get name => 'view';
|
|
1196
|
+
|
|
1197
|
+
@override
|
|
1198
|
+
String get description => 'View a collection schema.';
|
|
1199
|
+
|
|
1200
|
+
@override
|
|
1201
|
+
Future<int> run() => runGuarded(() async {
|
|
1202
|
+
banner('pocketbase · collections · view');
|
|
1203
|
+
final name = argResults!['name'] as String;
|
|
1204
|
+
final col = await _pbStep('Viewing $name', (c) => c.getCollection(name), argResults!);
|
|
1205
|
+
stdout.writeln(const JsonEncoder.withIndent(' ').convert(col));
|
|
1206
|
+
});
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
class PocketBaseCollectionsCreateCommand extends FlutterPatcherCommand {
|
|
1210
|
+
PocketBaseCollectionsCreateCommand() {
|
|
1211
|
+
_addBackendOptions(argParser);
|
|
1212
|
+
argParser.addOption('body', help: 'JSON collection definition.', mandatory: true);
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
@override
|
|
1216
|
+
String get name => 'create';
|
|
1217
|
+
|
|
1218
|
+
@override
|
|
1219
|
+
String get description => 'Create a new collection.';
|
|
1220
|
+
|
|
1221
|
+
@override
|
|
1222
|
+
Future<int> run() => runGuarded(() async {
|
|
1223
|
+
banner('pocketbase · collections · create');
|
|
1224
|
+
final body = jsonDecode(argResults!['body'] as String) as Map<String, dynamic>;
|
|
1225
|
+
await _pbStep('Creating ${body['name'] ?? 'collection'}',
|
|
1226
|
+
(c) => c.createCollection(body), argResults!);
|
|
1227
|
+
step(green('Created ${body['name'] ?? 'collection'}'));
|
|
1228
|
+
});
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
class PocketBaseCollectionsUpdateCommand extends FlutterPatcherCommand {
|
|
1232
|
+
PocketBaseCollectionsUpdateCommand() {
|
|
1233
|
+
_addBackendOptions(argParser);
|
|
1234
|
+
argParser.addOption('name', help: 'Collection name or ID.', mandatory: true);
|
|
1235
|
+
argParser.addOption('body', help: 'JSON fields to update.', mandatory: true);
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
@override
|
|
1239
|
+
String get name => 'update';
|
|
1240
|
+
|
|
1241
|
+
@override
|
|
1242
|
+
String get description => 'Update a collection.';
|
|
1243
|
+
|
|
1244
|
+
@override
|
|
1245
|
+
Future<int> run() => runGuarded(() async {
|
|
1246
|
+
banner('pocketbase · collections · update');
|
|
1247
|
+
final r = argResults!;
|
|
1248
|
+
final name = r['name'] as String;
|
|
1249
|
+
final body = jsonDecode(r['body'] as String) as Map<String, dynamic>;
|
|
1250
|
+
await _pbStep('Updating $name', (c) => c.updateCollection(name, body), r);
|
|
1251
|
+
step(green('Updated $name'));
|
|
1252
|
+
});
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
class PocketBaseCollectionsDeleteCommand extends FlutterPatcherCommand {
|
|
1256
|
+
PocketBaseCollectionsDeleteCommand() {
|
|
1257
|
+
_addBackendOptions(argParser);
|
|
1258
|
+
argParser.addOption('name', help: 'Collection name or ID.', mandatory: true);
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
@override
|
|
1262
|
+
String get name => 'delete';
|
|
1263
|
+
|
|
1264
|
+
@override
|
|
1265
|
+
String get description => 'Delete a collection.';
|
|
1266
|
+
|
|
1267
|
+
@override
|
|
1268
|
+
Future<int> run() => runGuarded(() async {
|
|
1269
|
+
banner('pocketbase · collections · delete');
|
|
1270
|
+
final name = argResults!['name'] as String;
|
|
1271
|
+
await _pbStep('Deleting $name', (c) => c.deleteCollection(name), argResults!);
|
|
1272
|
+
step(green('Deleted collection $name'));
|
|
1273
|
+
});
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
class PocketBaseCollectionsTruncateCommand extends FlutterPatcherCommand {
|
|
1277
|
+
PocketBaseCollectionsTruncateCommand() {
|
|
1278
|
+
_addBackendOptions(argParser);
|
|
1279
|
+
argParser.addOption('name', help: 'Collection name or ID.', mandatory: true);
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
@override
|
|
1283
|
+
String get name => 'truncate';
|
|
1284
|
+
|
|
1285
|
+
@override
|
|
1286
|
+
String get description => 'Delete all records in a collection.';
|
|
1287
|
+
|
|
1288
|
+
@override
|
|
1289
|
+
Future<int> run() => runGuarded(() async {
|
|
1290
|
+
banner('pocketbase · collections · truncate');
|
|
1291
|
+
final name = argResults!['name'] as String;
|
|
1292
|
+
await _pbStep('Truncating $name', (c) => c.truncateCollection(name), argResults!);
|
|
1293
|
+
step(green('Truncated $name'));
|
|
1294
|
+
});
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
class PocketBaseCollectionsImportCommand extends FlutterPatcherCommand {
|
|
1298
|
+
PocketBaseCollectionsImportCommand() {
|
|
1299
|
+
_addBackendOptions(argParser);
|
|
1300
|
+
argParser.addOption('file', help: 'JSON file with collections config.', mandatory: true);
|
|
1301
|
+
argParser.addFlag('delete-missing', help: 'Delete collections not in file.', defaultsTo: false);
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
@override
|
|
1305
|
+
String get name => 'import';
|
|
1306
|
+
|
|
1307
|
+
@override
|
|
1308
|
+
String get description => 'Import collections from a JSON file.';
|
|
1309
|
+
|
|
1310
|
+
@override
|
|
1311
|
+
Future<int> run() => runGuarded(() async {
|
|
1312
|
+
banner('pocketbase · collections · import');
|
|
1313
|
+
final r = argResults!;
|
|
1314
|
+
final fp = r['file'] as String;
|
|
1315
|
+
final f = File(fp);
|
|
1316
|
+
if (!await f.exists()) throw PackException('File not found: $fp', 64);
|
|
1317
|
+
final json = jsonDecode(await f.readAsString()) as List<dynamic>;
|
|
1318
|
+
final cols = json.cast<Map<String, dynamic>>();
|
|
1319
|
+
await _pbStep(
|
|
1320
|
+
'Importing ${cols.length} collections',
|
|
1321
|
+
(c) => c.importCollections(cols, deleteMissing: r['delete-missing'] as bool),
|
|
1322
|
+
r,
|
|
1323
|
+
);
|
|
1324
|
+
step(green('Imported ${cols.length} collections'));
|
|
1325
|
+
});
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
// ---------------------------------------------------------------------------
|
|
1329
|
+
// settings
|
|
1330
|
+
// ---------------------------------------------------------------------------
|
|
1331
|
+
|
|
1332
|
+
class PocketBaseSettingsCommand extends FlutterPatcherCommand {
|
|
1333
|
+
PocketBaseSettingsCommand() {
|
|
1334
|
+
addSubcommand(PocketBaseSettingsListCommand());
|
|
1335
|
+
addSubcommand(PocketBaseSettingsUpdateCommand());
|
|
1336
|
+
addSubcommand(PocketBaseSettingsTestS3Command());
|
|
1337
|
+
addSubcommand(PocketBaseSettingsTestEmailCommand());
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
@override
|
|
1341
|
+
String get name => 'settings';
|
|
1342
|
+
|
|
1343
|
+
@override
|
|
1344
|
+
String get description => 'Manage PocketBase application settings.';
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
class PocketBaseSettingsListCommand extends FlutterPatcherCommand {
|
|
1348
|
+
PocketBaseSettingsListCommand() {
|
|
1349
|
+
_addBackendOptions(argParser);
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
@override
|
|
1353
|
+
String get name => 'list';
|
|
1354
|
+
|
|
1355
|
+
@override
|
|
1356
|
+
String get description => 'List all application settings.';
|
|
1357
|
+
|
|
1358
|
+
@override
|
|
1359
|
+
Future<int> run() => runGuarded(() async {
|
|
1360
|
+
banner('pocketbase · settings · list');
|
|
1361
|
+
final settings = await _pbStep('Fetching settings', (c) => c.listSettings(), argResults!);
|
|
1362
|
+
stdout.writeln(const JsonEncoder.withIndent(' ').convert(settings));
|
|
1363
|
+
});
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
class PocketBaseSettingsUpdateCommand extends FlutterPatcherCommand {
|
|
1367
|
+
PocketBaseSettingsUpdateCommand() {
|
|
1368
|
+
_addBackendOptions(argParser);
|
|
1369
|
+
argParser.addOption('body', help: 'JSON object with settings to update.', mandatory: true);
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
@override
|
|
1373
|
+
String get name => 'update';
|
|
1374
|
+
|
|
1375
|
+
@override
|
|
1376
|
+
String get description => 'Update application settings.';
|
|
1377
|
+
|
|
1378
|
+
@override
|
|
1379
|
+
Future<int> run() => runGuarded(() async {
|
|
1380
|
+
banner('pocketbase · settings · update');
|
|
1381
|
+
final body = jsonDecode(argResults!['body'] as String) as Map<String, dynamic>;
|
|
1382
|
+
await _pbStep('Updating settings', (c) => c.updateSettings(body), argResults!);
|
|
1383
|
+
step(green('Settings updated'));
|
|
1384
|
+
});
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
class PocketBaseSettingsTestS3Command extends FlutterPatcherCommand {
|
|
1388
|
+
PocketBaseSettingsTestS3Command() {
|
|
1389
|
+
_addBackendOptions(argParser);
|
|
1390
|
+
argParser.addOption('filesystem', help: 'Storage filesystem to test.', defaultsTo: 'storage');
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
@override
|
|
1394
|
+
String get name => 'test-s3';
|
|
1395
|
+
|
|
1396
|
+
@override
|
|
1397
|
+
String get description => 'Test S3 storage connection.';
|
|
1398
|
+
|
|
1399
|
+
@override
|
|
1400
|
+
Future<int> run() => runGuarded(() async {
|
|
1401
|
+
banner('pocketbase · settings · test-s3');
|
|
1402
|
+
final fs = argResults!['filesystem'] as String;
|
|
1403
|
+
await _pbStep('Testing S3 ($fs)', (c) => c.testS3(fs), argResults!);
|
|
1404
|
+
step(green('S3 connection OK'));
|
|
1405
|
+
});
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
class PocketBaseSettingsTestEmailCommand extends FlutterPatcherCommand {
|
|
1409
|
+
PocketBaseSettingsTestEmailCommand() {
|
|
1410
|
+
_addBackendOptions(argParser);
|
|
1411
|
+
argParser.addOption('email', help: 'Recipient email.', mandatory: true);
|
|
1412
|
+
argParser.addOption('template', help: 'Email template.', mandatory: true);
|
|
1413
|
+
argParser.addOption('collection', help: 'Auth collection name or ID.');
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
@override
|
|
1417
|
+
String get name => 'test-email';
|
|
1418
|
+
|
|
1419
|
+
@override
|
|
1420
|
+
String get description => 'Send a test email.';
|
|
1421
|
+
|
|
1422
|
+
@override
|
|
1423
|
+
Future<int> run() => runGuarded(() async {
|
|
1424
|
+
banner('pocketbase · settings · test-email');
|
|
1425
|
+
final r = argResults!;
|
|
1426
|
+
await _pbStep(
|
|
1427
|
+
'Sending test email to ${r['email']}',
|
|
1428
|
+
(c) => c.testEmail(
|
|
1429
|
+
email: r['email'] as String,
|
|
1430
|
+
template: r['template'] as String,
|
|
1431
|
+
collection: r['collection'] as String?,
|
|
1432
|
+
),
|
|
1433
|
+
r,
|
|
1434
|
+
);
|
|
1435
|
+
step(green('Test email sent'));
|
|
1436
|
+
});
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
// ---------------------------------------------------------------------------
|
|
1440
|
+
// sql
|
|
1441
|
+
// ---------------------------------------------------------------------------
|
|
1442
|
+
|
|
1443
|
+
class PocketBaseSqlCommand extends FlutterPatcherCommand {
|
|
1444
|
+
PocketBaseSqlCommand() {
|
|
1445
|
+
_addBackendOptions(argParser);
|
|
1446
|
+
argParser.addOption('query', help: 'SQL query to execute.', mandatory: true);
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
@override
|
|
1450
|
+
String get name => 'sql';
|
|
1451
|
+
|
|
1452
|
+
@override
|
|
1453
|
+
String get description => 'Execute a raw SQL query.';
|
|
1454
|
+
|
|
1455
|
+
@override
|
|
1456
|
+
Future<int> run() => runGuarded(() async {
|
|
1457
|
+
banner('pocketbase · sql');
|
|
1458
|
+
final query = argResults!['query'] as String;
|
|
1459
|
+
final result = await _pbStep('Executing query', (c) => c.runSql(query), argResults!);
|
|
1460
|
+
final columns = (result['columns'] as List? ?? []).cast<Map<String, dynamic>>();
|
|
1461
|
+
final rows = (result['rows'] as List? ?? []).cast<List<dynamic>>();
|
|
1462
|
+
if (rows.isNotEmpty) {
|
|
1463
|
+
final colNames = columns.map((c) => '${c['name']}').toList();
|
|
1464
|
+
final tableRows = rows.map((r) => r.map((v) => '$v').toList()).toList();
|
|
1465
|
+
table('${rows.length} rows', colNames, tableRows);
|
|
1466
|
+
} else {
|
|
1467
|
+
step('Query executed. ${result['affectedRows'] ?? 0} rows affected.');
|
|
1468
|
+
}
|
|
1469
|
+
});
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
// ---------------------------------------------------------------------------
|
|
1473
|
+
// crons
|
|
1474
|
+
// ---------------------------------------------------------------------------
|
|
1475
|
+
|
|
1476
|
+
class PocketBaseCronsCommand extends FlutterPatcherCommand {
|
|
1477
|
+
PocketBaseCronsCommand() {
|
|
1478
|
+
addSubcommand(PocketBaseCronsListCommand());
|
|
1479
|
+
addSubcommand(PocketBaseCronsRunCommand());
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
@override
|
|
1483
|
+
String get name => 'crons';
|
|
1484
|
+
|
|
1485
|
+
@override
|
|
1486
|
+
String get description => 'Manage PocketBase cron jobs.';
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
class PocketBaseCronsListCommand extends FlutterPatcherCommand {
|
|
1490
|
+
PocketBaseCronsListCommand() {
|
|
1491
|
+
_addBackendOptions(argParser);
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
@override
|
|
1495
|
+
String get name => 'list';
|
|
1496
|
+
|
|
1497
|
+
@override
|
|
1498
|
+
String get description => 'List all registered cron jobs.';
|
|
1499
|
+
|
|
1500
|
+
@override
|
|
1501
|
+
Future<int> run() => runGuarded(() async {
|
|
1502
|
+
banner('pocketbase · crons · list');
|
|
1503
|
+
final crons = await _pbStep('Fetching cron jobs', (c) => c.listCrons(), argResults!);
|
|
1504
|
+
if (crons.isEmpty) {
|
|
1505
|
+
warn('No cron jobs registered.');
|
|
1506
|
+
return;
|
|
1507
|
+
}
|
|
1508
|
+
final rows = crons.map((c) => [
|
|
1509
|
+
cyan(c['id']?.toString() ?? '?'),
|
|
1510
|
+
c['expression']?.toString() ?? '?',
|
|
1511
|
+
]).toList();
|
|
1512
|
+
table('${crons.length} cron jobs', ['ID', 'EXPRESSION'], rows);
|
|
1513
|
+
});
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
class PocketBaseCronsRunCommand extends FlutterPatcherCommand {
|
|
1517
|
+
PocketBaseCronsRunCommand() {
|
|
1518
|
+
_addBackendOptions(argParser);
|
|
1519
|
+
argParser.addOption('job-id', help: 'Cron job ID to run.', mandatory: true);
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
@override
|
|
1523
|
+
String get name => 'run';
|
|
1524
|
+
|
|
1525
|
+
@override
|
|
1526
|
+
String get description => 'Trigger a cron job by ID.';
|
|
1527
|
+
|
|
1528
|
+
@override
|
|
1529
|
+
Future<int> run() => runGuarded(() async {
|
|
1530
|
+
banner('pocketbase · crons · run');
|
|
1531
|
+
final jobId = argResults!['job-id'] as String;
|
|
1532
|
+
await _pbStep('Running $jobId', (c) => c.runCron(jobId), argResults!);
|
|
1533
|
+
step(green('Cron $jobId triggered'));
|
|
1534
|
+
});
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
// ---------------------------------------------------------------------------
|
|
1538
|
+
// query
|
|
1539
|
+
// ---------------------------------------------------------------------------
|
|
1540
|
+
|
|
1541
|
+
class PocketBaseQueryCommand extends FlutterPatcherCommand {
|
|
1542
|
+
PocketBaseQueryCommand() {
|
|
1543
|
+
_addBackendOptions(argParser);
|
|
1544
|
+
argParser.addOption('method', help: 'HTTP method.', defaultsTo: 'GET');
|
|
1545
|
+
argParser.addOption('path', help: 'API path.', mandatory: true);
|
|
1546
|
+
argParser.addOption('body', help: 'JSON body for POST/PATCH.');
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
@override
|
|
1550
|
+
String get name => 'query';
|
|
1551
|
+
|
|
1552
|
+
@override
|
|
1553
|
+
String get description => 'Execute an arbitrary API request.';
|
|
1554
|
+
|
|
1555
|
+
@override
|
|
1556
|
+
Future<int> run() => runGuarded(() async {
|
|
1557
|
+
banner('pocketbase · query');
|
|
1558
|
+
final r = argResults!;
|
|
1559
|
+
final method = r['method'] as String;
|
|
1560
|
+
final path = r['path'] as String;
|
|
1561
|
+
final bodyStr = r['body'] as String?;
|
|
1562
|
+
Map<String, dynamic>? body;
|
|
1563
|
+
if (bodyStr != null && bodyStr.isNotEmpty) {
|
|
1564
|
+
body = jsonDecode(bodyStr) as Map<String, dynamic>;
|
|
1565
|
+
}
|
|
1566
|
+
final client = PocketBaseClient(_resolveBackend(r).$1);
|
|
1567
|
+
await client.authenticate(_resolveBackend(r).$2, _resolveBackend(r).$3);
|
|
1568
|
+
final res = await client.rawRequest(method, path, body: body);
|
|
1569
|
+
client.close();
|
|
1570
|
+
stdout.writeln(const JsonEncoder.withIndent(' ').convert(jsonDecode(res.body)));
|
|
1571
|
+
});
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
// ---------------------------------------------------------------------------
|
|
1575
|
+
// config
|
|
1576
|
+
// ---------------------------------------------------------------------------
|
|
1577
|
+
|
|
1578
|
+
class PocketBaseConfigCommand extends FlutterPatcherCommand {
|
|
1579
|
+
PocketBaseConfigCommand() {
|
|
1580
|
+
_addBackendOptions(argParser);
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
@override
|
|
1584
|
+
String get name => 'config';
|
|
1585
|
+
|
|
1586
|
+
@override
|
|
1587
|
+
String get description => 'Show current PocketBase configuration.';
|
|
1588
|
+
|
|
1589
|
+
@override
|
|
1590
|
+
Future<int> run() => runGuarded(() async {
|
|
1591
|
+
banner('pocketbase · config');
|
|
1592
|
+
final (url, admin, _) = _resolveBackend(argResults!);
|
|
1593
|
+
final paths = PocketBaseInstallPaths.resolve();
|
|
1594
|
+
final exists = await paths.binaryPath.exists();
|
|
1595
|
+
box('config', [
|
|
1596
|
+
kv('url', cyan(url)),
|
|
1597
|
+
kv('admin', admin),
|
|
1598
|
+
kv('version', paths.version),
|
|
1599
|
+
kv('binary', exists ? green(paths.binaryPath.path) : red('not installed')),
|
|
1600
|
+
]);
|
|
233
1601
|
});
|
|
234
1602
|
}
|