diff --git a/composer.json b/composer.json index bb31a26..0a301a8 100644 --- a/composer.json +++ b/composer.json @@ -22,7 +22,7 @@ "repositories": { "slotdb-client": { "type": "vcs", - "url": "../slotdb-client-php" + "url": "git@dev.noccylabs.info:slotdb/slotdb-client-php.git" } }, "bin": [ diff --git a/composer.lock b/composer.lock index 3da48ab..11d4c4b 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "9dc3a3ca30d0b0d1698b2e4e26577e67", + "content-hash": "8140c4572894cc393ef74de8ad3fe56c", "packages": [ { "name": "guzzlehttp/guzzle", @@ -593,8 +593,8 @@ "version": "dev-master", "source": { "type": "git", - "url": "../slotdb-client-php", - "reference": "4734ca5fe4ed02da5bc3e0df3c9ecbabc3474af3" + "url": "git@dev.noccylabs.info:slotdb/slotdb-client-php.git", + "reference": "e6e1e980eaa9500fc38bfd22dfdc23ca13258ad3" }, "require": { "psr/http-client": "^1.0" @@ -620,7 +620,7 @@ } ], "description": "SlotDB PHP Client", - "time": "2025-03-14T14:34:28+00:00" + "time": "2025-03-15T13:25:21+00:00" }, { "name": "symfony/console", diff --git a/src/Application.php b/src/Application.php index f10f183..00ad090 100644 --- a/src/Application.php +++ b/src/Application.php @@ -16,8 +16,8 @@ class Application extends \Symfony\Component\Console\Application $psr = new Client(); $client = new SlotDbClient($psr, $baseUrl); - $this->add(new Command\ImportCommand()); - $this->add(new Command\ExportCommand()); + $this->add(new Command\ImportCommand($client)); + $this->add(new Command\ExportCommand($client)); $this->add(new Command\Slot\SlotCreateCommand($client)); $this->add(new Command\Slot\SlotQueryCommand($client)); diff --git a/src/Command/ExportCommand.php b/src/Command/ExportCommand.php index e28da68..047cacb 100644 --- a/src/Command/ExportCommand.php +++ b/src/Command/ExportCommand.php @@ -2,10 +2,73 @@ namespace SlotDb\Cli\Command; +use SlotDb\Client\SlotDbClient; use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; #[AsCommand(name:"export", description:"Export the database to a file")] class ExportCommand extends BaseCommand { + public function __construct( + private readonly SlotDbClient $client + ) + { + parent::__construct(); + } + + protected function configure() + { + $this->addOption("all-props", "A", InputOption::VALUE_NONE, "Include all slot properties. If not specified, properties that match the group property will be stripped."); + $this->addOption("output", "o", InputOption::VALUE_REQUIRED, "Write to file instead of standard output"); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + // TODO implement paging + + $groups = $this->client->findGroups(); + $indexedGroups = array_combine( + array_map(fn($group) => $group['_group'], $groups), + array_values($groups) + ); + + $slots = $this->client->findSlots(); + if (!$input->getOption("all-props")) { + foreach ($slots as &$slot) { + if ($slot['_group'] && isset($indexedGroups[$slot['_group']])) { + $group = $indexedGroups[$slot['_group']]; + foreach ($slot as $k=>$v) { + if (str_starts_with($k,"_")) continue; + if (isset($group[$k]) && $group[$k] == $v) { + unset($slot[$k]); + } + } + } + } + } + + $export = json_encode([ + 'comment' => "This is an exported SlotDB database.", + 'timestamp' => date('Y-m-d H:i:s P'), + 'generator' => "SlotCLI v".APP_VERSION." (".APP_BUILT.")", + 'export' => [ + 'schemas' => [], + 'hooks' => [], + 'groups' => $groups, + 'slots' => $slots, + ], + ], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); + + if ($input->getOption("output")) { + $file = $input->getOption("output"); + file_put_contents($file, $export."\n"); + } else { + $output->writeln($export); + } + + return self::SUCCESS; + } } diff --git a/src/Command/Group/GroupFindCommand.php b/src/Command/Group/GroupFindCommand.php index 92d3120..46dad70 100644 --- a/src/Command/Group/GroupFindCommand.php +++ b/src/Command/Group/GroupFindCommand.php @@ -42,8 +42,14 @@ class GroupFindCommand extends Command foreach ($groups as $group) { $row = []; foreach ($keys as $key) { - $row[] = isset($group[$key]) ? json_encode($group[$key]) : null; + $row[] = isset($group[$key]) ? $group[$key] : null; } + $row = array_map(fn($v) => match(true) { + is_bool($v) => "".($v?"true":"false")."", + is_int($v) => "{$v}", + is_string($v) => "{$v}", + default => $v, + }, $row); $table->addRow($row); } diff --git a/src/Command/Group/GroupQueryCommand.php b/src/Command/Group/GroupQueryCommand.php index 9716ff0..f57116a 100644 --- a/src/Command/Group/GroupQueryCommand.php +++ b/src/Command/Group/GroupQueryCommand.php @@ -32,7 +32,7 @@ class GroupQueryCommand extends Command try { $group = $this->client->queryGroup($groupId); } catch (\RuntimeException $e) { - $output->writeln("Invalid group id: {$slotId}"); + $output->writeln("Invalid group id: {$groupId}"); return self::INVALID; } diff --git a/src/Command/ImportCommand.php b/src/Command/ImportCommand.php index 9ee9283..5835220 100644 --- a/src/Command/ImportCommand.php +++ b/src/Command/ImportCommand.php @@ -2,10 +2,68 @@ namespace SlotDb\Cli\Command; +use SlotDb\Client\SlotDbClient; use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; #[AsCommand(name:"import", description:"Import database from file")] class ImportCommand extends BaseCommand { + public function __construct( + private readonly SlotDbClient $client + ) + { + parent::__construct(); + } + + protected function configure() + { + $this->addOption("hard-reset", "R", InputOption::VALUE_NONE, "Completely wipe the database before importing the new data."); + $this->addArgument("filename", InputArgument::OPTIONAL, "Filename to read from, if omitted read standard input."); + $this->addOption("assume-yes", "y", InputOption::VALUE_NONE, "Assume yes on all questions."); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + if ($input->getArgument("filename")) { + $file = $input->getArgument("filename"); + $json = file_get_contents($file); + } else { + $json = stream_get_contents(STDIN); + } + + $export = json_decode($json); + + $output->writeln("File was generated {$export->timestamp}"); + $output->writeln("Generated using {$export->generator}"); + $output->writeln(""); + if (!$input->getOption("assume-yes")) { + $confirm = (new SymfonyStyle($input, $output))->confirm("Do you want to continue", false); + if (!$confirm) { + return self::FAILURE; + } + $output->writeln(""); + } + + $output->writeln("+ Importing schemas..."); + + $output->writeln("+ Importing hooks..."); + + $output->writeln("+ Importing groups..."); + foreach ($export->export->groups as $group) { + $output->writeln(" - ".$group->_group.""); + } + + $output->writeln("+ Importing slots..."); + foreach ($export->export->slots as $slot) { + $output->writeln(" - ".$slot->_slot.""); + } + + return self::SUCCESS; + } } diff --git a/src/Command/Slot/SlotCreateCommand.php b/src/Command/Slot/SlotCreateCommand.php index 8bebab5..3f1de0e 100644 --- a/src/Command/Slot/SlotCreateCommand.php +++ b/src/Command/Slot/SlotCreateCommand.php @@ -23,7 +23,7 @@ class SlotCreateCommand extends Command protected function configure() { $this - ->addOption("name", "n", InputOption::VALUE_REQUIRED, "Set slot name") + ->addOption("name", "N", InputOption::VALUE_REQUIRED, "Set slot name") ->addArgument("slot", InputArgument::REQUIRED, "Slot ID") ->addArgument("properties", InputArgument::IS_ARRAY, "Properties to assign to the slot") ; @@ -32,8 +32,25 @@ class SlotCreateCommand extends Command protected function execute(InputInterface $input, OutputInterface $output): int { - // $slotId = $input->getArgument("slot"); - // $slot = $this->client->querySlot($slotId); + $slotId = $input->getArgument("slot"); + $slotName = $input->getOption("name") ?? $slotId; + + $props = []; + foreach ($input->getArgument("properties") as $prop) { + if (!str_contains($prop, '=')) { + $output->writeln("Invalid property \"{$prop}\", should be in the form of key=value."); + return self::INVALID; + } + [$k, $v] = explode("=", $prop, 2); + $props[$k] = match ($v) { + 'true','false','null' => json_decode($v), + default => json_decode($v) ?? $v, + }; + } + + $slot = $this->client->createSlot($slotId, $slotName, $props); + + var_dump($slot); return self::SUCCESS; } diff --git a/src/Command/Slot/SlotFindCommand.php b/src/Command/Slot/SlotFindCommand.php index 84ae00e..06e715f 100644 --- a/src/Command/Slot/SlotFindCommand.php +++ b/src/Command/Slot/SlotFindCommand.php @@ -55,8 +55,14 @@ class SlotFindCommand extends Command foreach ($slots as $slot) { $row = []; foreach ($keys as $key) { - $row[] = isset($slot[$key]) ? json_encode($slot[$key]) : null; + $row[] = isset($slot[$key]) ? $slot[$key] : null; } + $row = array_map(fn($v) => match(true) { + is_bool($v) => "".($v?"true":"false")."", + is_int($v) => "{$v}", + is_string($v) => "{$v}", + default => $v, + }, $row); $table->addRow($row); } diff --git a/src/bootstrap.php b/src/bootstrap.php index 214d3b1..7d6dbac 100644 --- a/src/bootstrap.php +++ b/src/bootstrap.php @@ -2,6 +2,12 @@ require_once __DIR__."/../vendor/autoload.php"; +$buildInfo = (file_exists(__DIR__."/build.php")) + ? include __DIR__."/build.php" + : [ 'version' => '0.0.0', 'builddate' => 'never' ]; +define("APP_VERSION", $buildInfo['version']); +define("APP_BUILT", $buildInfo['builddate']); + $app = new SlotDb\Cli\Application(); $app->run();