diff --git a/src/Updater.php b/src/Updater.php index 15a81f9..63c6d22 100644 --- a/src/Updater.php +++ b/src/Updater.php @@ -2,29 +2,56 @@ namespace nullthoughts\LaravelDataSync; +use Illuminate\Support\Collection; +use Illuminate\Support\Facades\File; +use Illuminate\Support\Facades\Storage; +use Illuminate\Support\Str; use nullthoughts\LaravelDataSync\Exceptions\ErrorUpdatingModelException; use nullthoughts\LaravelDataSync\Exceptions\FileDirectoryNotFoundException; use nullthoughts\LaravelDataSync\Exceptions\NoCriteriaException; use nullthoughts\LaravelDataSync\Exceptions\NoRecordsInvalidJSONException; -use Illuminate\Support\Collection; -use Illuminate\Support\Facades\File; -use Illuminate\Support\Str; use stdClass; class Updater { + /** + * @var string + */ + private $directory; + + /** + * @var \Illuminate\Support\Collection + */ + private $files; + + /** + * @var bool + */ + private $remote; + + /** + * @var string + */ + private $disk; + /** * Get files in sync directory. * - * @param string|null $path - * @param string|null $model + * @param string|null $path + * @param string|null $model + * + * @param bool $remote + * @param string $disk * * @throws \nullthoughts\LaravelDataSync\Exceptions\FileDirectoryNotFoundException */ - public function __construct($path = null, $model = null) + public function __construct($path = null, $model = null, $remote = false, $disk = 's3') { - $directory = $this->getDirectory($path); - $this->files = $this->getFiles($directory, $model); + $this->remote = $remote; + $this->disk = $disk; + + $this->directory = $this->getDirectory($path); + $this->files = $this->getFiles($this->directory, $model); } /** @@ -50,11 +77,11 @@ class Updater /** * Parse each record for criteria/values and update/create model. * - * @param string $file - * - * @throws \nullthoughts\LaravelDataSync\Exceptions\NoRecordsInvalidJSONException + * @param string $file * * @return \Illuminate\Support\Collection + * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException + * @throws \nullthoughts\LaravelDataSync\Exceptions\NoRecordsInvalidJSONException */ protected function syncModel(string $file) { @@ -89,7 +116,7 @@ class Updater { $directory = $path ?? config('data-sync.path', base_path('sync')); - if (!file_exists($directory)) { + if ($this->directoryMissingLocally($directory) || $this->directoryMissingRemotely($directory)) { throw new FileDirectoryNotFoundException(); } @@ -110,10 +137,17 @@ class Updater return Collection::wrap($directory.'/'.$model.'.json'); } - return collect(File::files($directory)) + $files = ($this->remote) ? Storage::disk($this->disk)->files($directory) : File::files($directory); + + return collect($files) ->filter(function ($file) { return pathinfo($file, PATHINFO_EXTENSION) == 'json'; })->map(function ($path) { + + if (is_string($path)) { + return $path; + } + return $path->getPathname(); }); } @@ -204,15 +238,17 @@ class Updater /** * Parses JSON from file and returns collection. * - * @param string $file - * - * @throws \nullthoughts\LaravelDataSync\Exceptions\NoRecordsInvalidJSONException + * @param string $file * * @return \Illuminate\Support\Collection + * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException + * @throws \nullthoughts\LaravelDataSync\Exceptions\NoRecordsInvalidJSONException */ protected function getRecords(string $file) { - $records = collect(json_decode(File::get($file))); + $fetchedFile = ($this->remote) ? Storage::disk($this->disk)->get($file) : File::get($file); + + $records = collect(json_decode($fetchedFile)); if ($records->isEmpty()) { throw new NoRecordsInvalidJSONException($file); @@ -273,4 +309,24 @@ class Updater return [$key => $value]; })->toArray(); } + + /** + * @param \Illuminate\Config\Repository $directory + * + * @return bool + */ + protected function directoryMissingLocally($directory) + { + return !$this->remote && !file_exists($directory); + } + + /** + * @param \Illuminate\Config\Repository $directory + * + * @return bool + */ + protected function directoryMissingRemotely($directory) + { + return $this->remote && !Storage::disk($this->disk)->exists($directory); + } } diff --git a/tests/Unit/UpdaterRemoteTest.php b/tests/Unit/UpdaterRemoteTest.php new file mode 100644 index 0000000..a6c9186 --- /dev/null +++ b/tests/Unit/UpdaterRemoteTest.php @@ -0,0 +1,160 @@ +put('test-data/roles.json', File::get(__DIR__.'/../test-data/roles.json')); + foreach (File::directories(__DIR__.'/../test-data/') as $directory) { + $files = File::files($directory); + + foreach ($files as $file) { + Storage::disk('s3')->put('test-data/'.basename($directory).'/'.$file->getRelativePathname(), File::get($file->getPathname())); + } + } + } + + /** @test */ + public function it_adds_roles_to_the_database_in_remote() + { + $updater = new UpdaterFake('test-data', 'roles', true, 's3'); + + $updater->run(); + + $this->assertDatabaseHas('roles', ['slug' => 'update-student-records']); + $this->assertDatabaseHas('roles', ['slug' => 'borrow-ferrari']); + $this->assertDatabaseHas('roles', ['slug' => 'destroy-ferrari']); + } + + /** @test */ + public function it_can_default_to_configuration_in_remote() + { + config()->set('data-sync.path', 'test-data'); + + $updater = new UpdaterFake(null, null, true, 's3'); + + $updater->run(); + + $this->assertDatabaseHas('roles', ['slug' => 'update-student-records']); + $this->assertDatabaseHas('roles', ['slug' => 'borrow-ferrari']); + $this->assertDatabaseHas('roles', ['slug' => 'destroy-ferrari']); + } + + /** @test */ + public function it_can_update_an_existing_record_in_remote() + { + config()->set('data-sync.path', 'test-data'); + (new UpdaterFake(null, null, true, 's3'))->run(); + + config()->set('data-sync.path', 'test-data/valid'); + (new UpdaterFake(null, null, true, 's3'))->run(); + + $this->assertDatabaseHas('roles', ['category' => 'changed']); + $this->assertDatabaseHas('roles', ['category' => 'changed']); + $this->assertDatabaseHas('roles', ['category' => 'changed']); + } + + /** @test */ + public function it_can_update_the_relationship_in_remote() + { + $supervisor = Supervisor::create([ + 'name' => 'CEO', + ]); + + config()->set('data-sync.path', 'test-data/relationship'); + (new UpdaterFake(null, null, true, 's3'))->run(); + + $this->assertEquals($supervisor->id, Roles::first()->supervisor_id); + $this->assertTrue($supervisor->is(Roles::first()->supervisor)); + } + + /** + * @test + * @group current + */ + public function exception_is_thrown_if_the_directory_does_not_exists() + { + try { + new UpdaterFake(null, null, true, 's3'); + + $this->fail('exception was thrown'); + } catch (Exception $e) { + $this->assertEquals('Specified sync file directory does not exist', $e->getMessage()); + } + } + + /** @test */ + public function invalid_json_throws_an_exception_in_remote() + { + try { + $updater = new UpdaterFake('test-data/invalid-json', null, true, 's3'); + $updater->run(); + + $this->fail('exception was thrown'); + } catch (Exception $e) { + $this->assertStringContainsString('No records or invalid JSON for', $e->getMessage()); + } + } + + /** @test */ + public function the_json_must_contain_a_key_with_an_underscore_in_remote() + { + try { + $updater = new UpdaterFake('test-data/no-criteria', null, true, 's3'); + $updater->run(); + + $this->fail('exception was thrown'); + } catch (Exception $e) { + $this->assertEquals('No criteria/attributes detected', $e->getMessage()); + } + } + + /** @test */ + public function order_of_imports_can_be_defined_in_config_in_remote() + { + config()->set('data-sync.order', [ + 'Supervisor', + 'Roles', + ]); + + $updater = new UpdaterFake('test-data/ordered', null, true, 's3'); + $updater->run(); + + $this->assertDatabaseHas('roles', ['slug' => 'update-student-records']); + $this->assertDatabaseHas('supervisors', ['name' => 'CEO']); + } + + /** @test */ + public function exception_is_thrown_if_imports_are_in_incorrect_order_in_remote() + { + config()->set('data-sync.order', [ + 'Roles', + 'Supervisor', + ]); + + $this->expectException(ErrorUpdatingModelException::class); + + $updater = new UpdaterFake('test-data/ordered', null, true, 's3'); + $updater->run(); + } + + /** @test */ + public function it_ignores_non_json_files_in_remote() + { + $updater = new UpdaterFake('test-data/not-json', null, true, 's3'); + $updater->run(); + + $this->assertDatabaseMissing('roles', ['slug' => 'update-student-records']); + } +}