From 8c81ef2a8de55ce8499da5d6420e18b4062c2158 Mon Sep 17 00:00:00 2001 From: Abrar Ahmad Date: Tue, 8 Nov 2022 14:49:01 +0500 Subject: [PATCH] wip --- src/Contracts/SyncMaster.php | 2 +- src/Database/Models/TenantPivot.php | 3 +- src/Listeners/UpdateSyncedResource.php | 10 +- ...1_000002_create_tenant_resources_table.php | 25 ++ ..._11_000001_test_create_companies_table.php | 30 ++ tests/ResourceSyncingPolymorphicTest.php | 365 ++++++++++++++++++ 6 files changed, 428 insertions(+), 7 deletions(-) create mode 100644 tests/Etc/synced_resource_migrations/2020_05_11_000002_create_tenant_resources_table.php create mode 100644 tests/Etc/synced_resource_migrations/companies/2020_05_11_000001_test_create_companies_table.php create mode 100644 tests/ResourceSyncingPolymorphicTest.php diff --git a/src/Contracts/SyncMaster.php b/src/Contracts/SyncMaster.php index 28fafa91..3036fbe8 100644 --- a/src/Contracts/SyncMaster.php +++ b/src/Contracts/SyncMaster.php @@ -14,7 +14,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany; */ interface SyncMaster extends Syncable { - public function tenants(): BelongsToMany; + //public function tenants(): BelongsToMany; public function getTenantModelName(): string; } diff --git a/src/Database/Models/TenantPivot.php b/src/Database/Models/TenantPivot.php index 3cc614a9..429992d0 100644 --- a/src/Database/Models/TenantPivot.php +++ b/src/Database/Models/TenantPivot.php @@ -4,10 +4,11 @@ declare(strict_types=1); namespace Stancl\Tenancy\Database\Models; +use Illuminate\Database\Eloquent\Relations\MorphPivot; use Illuminate\Database\Eloquent\Relations\Pivot; use Stancl\Tenancy\Contracts\Syncable; -class TenantPivot extends Pivot +class TenantPivot extends MorphPivot { public static function booted(): void { diff --git a/src/Listeners/UpdateSyncedResource.php b/src/Listeners/UpdateSyncedResource.php index 39391eac..117d3b59 100644 --- a/src/Listeners/UpdateSyncedResource.php +++ b/src/Listeners/UpdateSyncedResource.php @@ -46,10 +46,10 @@ class UpdateSyncedResource extends QueueableListener // Since this model is "dirty" (taken by reference from the event), it might have the tenants // relationship already loaded and cached. For this reason, we refresh the relationship. - $centralModel->load('tenants'); + $centralModel->load('resources'); /** @var TenantCollection $tenants */ - $tenants = $centralModel->tenants; + $tenants = $centralModel->resources; return $tenants; } @@ -80,7 +80,7 @@ class UpdateSyncedResource extends QueueableListener return ((string) $model->pivot->tenant_id) === ((string) $tenant->getTenantKey()); }; - $mappingExists = $centralModel->tenants->contains($currentTenantMapping); + $mappingExists = $centralModel->resources->contains($currentTenantMapping); if (! $mappingExists) { // Here we should call TenantPivot, but we call general Pivot, so that this works @@ -89,12 +89,12 @@ class UpdateSyncedResource extends QueueableListener /** @var Tenant */ $tenant = $event->tenant; - $centralModel->tenants()->attach($tenant->getTenantKey()); + $centralModel->resources()->attach($tenant->getTenantKey()); }); } /** @var TenantCollection $tenants */ - $tenants = $centralModel->tenants->filter(function ($model) use ($currentTenantMapping) { + $tenants = $centralModel->resources->filter(function ($model) use ($currentTenantMapping) { // Remove the mapping for the current tenant. return ! $currentTenantMapping($model); }); diff --git a/tests/Etc/synced_resource_migrations/2020_05_11_000002_create_tenant_resources_table.php b/tests/Etc/synced_resource_migrations/2020_05_11_000002_create_tenant_resources_table.php new file mode 100644 index 00000000..83b722fa --- /dev/null +++ b/tests/Etc/synced_resource_migrations/2020_05_11_000002_create_tenant_resources_table.php @@ -0,0 +1,25 @@ +increments('id'); + $table->string('tenant_id'); + $table->string('resource_global_id'); + $table->string('tenant_resources_type'); + }); + } + + public function down() + { + Schema::dropIfExists('tenant_resources'); + } +} diff --git a/tests/Etc/synced_resource_migrations/companies/2020_05_11_000001_test_create_companies_table.php b/tests/Etc/synced_resource_migrations/companies/2020_05_11_000001_test_create_companies_table.php new file mode 100644 index 00000000..2d61a45d --- /dev/null +++ b/tests/Etc/synced_resource_migrations/companies/2020_05_11_000001_test_create_companies_table.php @@ -0,0 +1,30 @@ +increments('id'); + $table->string('global_id')->unique(); + $table->string('name'); + $table->string('email'); + }); + } + + public function down() + { + Schema::dropIfExists('companies'); + } +} diff --git a/tests/ResourceSyncingPolymorphicTest.php b/tests/ResourceSyncingPolymorphicTest.php new file mode 100644 index 00000000..085fbfe3 --- /dev/null +++ b/tests/ResourceSyncingPolymorphicTest.php @@ -0,0 +1,365 @@ + [ + DatabaseTenancyBootstrapper::class, + ]]); + + Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) { + return $event->tenant; + })->toListener()); + + DatabaseConfig::generateDatabaseNamesUsing(function () { + return 'db' . Str::random(16); + }); + + Event::listen(TenancyInitialized::class, BootstrapTenancy::class); + Event::listen(TenancyEnded::class, RevertToCentralContext::class); + + UpdateSyncedResource::$shouldQueue = false; // Global state cleanup + Event::listen(SyncedResourceSaved::class, UpdateSyncedResource::class); + + // Run migrations on central connection + pest()->artisan('migrate', [ + '--path' => [ + __DIR__ . '/Etc/synced_resource_migrations', + __DIR__ . '/Etc/synced_resource_migrations/users', + ], + '--realpath' => true, + ])->assertExitCode(0); +}); + +test('polymorphic relationship works for every model when syncing resources from central to tenant', function (){ + $tenant1 = ResourceTenantForPolymorphic::create(['id' => 't1']); + migrateUsersTableForPloymorphicTenants(); + + // Assert User resource is synced + $centralUser = CentralUserForPolymorphic::create([ + 'global_id' => 'acme', + 'name' => 'John Doe', + 'email' => 'john@localhost', + 'password' => 'password', + 'role' => 'commenter', + ]); + + $tenant1->run(function () { + expect(ResourceUserForPolymorphic::all())->toHaveCount(0); + }); + + // When central model provides nothing/null, the resource model will be created as a 1:1 copy of central model + $centralUser->resources()->attach('t1'); + + expect($centralUser->getSyncedCreationAttributes())->toBeNull(); + $tenant1->run(function () use ($centralUser) { + $resourceUser = ResourceUserForPolymorphic::first(); + expect($resourceUser)->not()->toBeNull(); + $resourceUser = $resourceUser->toArray(); + $centralUser = $centralUser->withoutRelations()->toArray(); + + // remove id from comparison, because we don't copy id and let target model handle it + unset($resourceUser['id']); + unset($centralUser['id']); + + expect($resourceUser)->toBe($centralUser); + }); + + $tenant2 = ResourceTenantForPolymorphic::create(['id' => 't2']); + migrateCompaniesTableForTenants(); + + // Assert Company resource is synced + $centralCompany = CentralCompanyForPolymorphic::create([ + 'global_id' => 'acme', + 'name' => 'ArchTech', + 'email' => 'archtech@localhost', + ]); + + $tenant2->run(function () { + expect(ResourceCompanyForPolymorphic::all())->toHaveCount(0); + }); + + // When central model provides nothing/null, the resource model will be created as a 1:1 copy of central model + $centralCompany->resources()->attach('t2'); + + expect($centralCompany->getSyncedCreationAttributes())->toBeNull(); + $tenant2->run(function () use ($centralCompany) { + $resourceCompany = ResourceCompanyForPolymorphic::first(); + expect($resourceCompany)->not()->toBeNull(); + $resourceCompany = $resourceCompany->toArray(); + $centralCompany = $centralCompany->withoutRelations()->toArray(); + + // remove id from comparison, because we don't copy id and let target model handle it + unset($resourceCompany['id']); + unset($centralCompany['id']); + + expect($resourceCompany)->toBe($centralCompany); + }); +}); + +test('polymorphic relationship works for multiple models when syncing resources from tenant to central', function () { + $tenant1 = ResourceTenantForPolymorphic::create(['id' => 't1']); + migrateUsersTableForPloymorphicTenants(); + + tenancy()->initialize($tenant1); + + // Assert User resource is synced + $resourceUser = ResourceUserForPolymorphic::create([ + 'name' => 'John Doe', + 'email' => 'john@localhost', + 'password' => 'password', + 'role' => 'commenter', + ]); + + tenancy()->end(); + + $centralUser = CentralUserForPolymorphic::first()->only(['name', 'email', 'password', 'role']); + $resourceUser = $resourceUser->only(['name', 'email', 'password', 'role']); + + expect($resourceUser)->toBe($centralUser); + + // Assert Company resource is synced + $tenant2 = ResourceTenantForPolymorphic::create(['id' => 't2']); + migrateCompaniesTableForTenants(); + + tenancy()->initialize($tenant2); + + // Assert User resource is synced + $resourceCompany = ResourceCompanyForPolymorphic::create([ + 'global_id' => 'acme', + 'name' => 'tenant comp', + 'email' => 'company@localhost', + ]); + + tenancy()->end(); + + $centralCompany = CentralCompanyForPolymorphic::first()->only(['name', 'email']); + $resourceCompany = $resourceCompany->only(['name', 'email']); + expect($resourceCompany)->toBe($centralCompany); +}); + +function migrateUsersTableForPloymorphicTenants(): void +{ + pest()->artisan('tenants:migrate', [ + '--path' => __DIR__ . '/Etc/synced_resource_migrations/users', + '--realpath' => true, + ])->assertExitCode(0); +} + +function migrateCompaniesTableForTenants(): void +{ + // Run migrations on central connection + pest()->artisan('migrate', [ + '--path' => [ + __DIR__ . '/Etc/synced_resource_migrations/companies', + ], + '--realpath' => true, + ])->assertExitCode(0); + + pest()->artisan('tenants:migrate', [ + '--path' => __DIR__ . '/Etc/synced_resource_migrations/companies', + '--realpath' => true, + ])->assertExitCode(0); +} + +class ResourceTenantForPolymorphic extends Tenant +{ + public function users() + { + return $this->morphedByMany(CentralUserForPolymorphic::class, 'tenant_resources', 'tenant_resources', 'tenant_id', 'resource_global_id', 'id', 'global_id') + ->using(TenantPivot::class); + } + + public function companies() + { + return $this->morphedByMany(CentralCompanyForPolymorphic::class, 'tenant_resources', 'tenant_resources', 'tenant_id', 'resource_global_id', 'id', 'global_id') + ->using(TenantPivot::class); + } +} + +class CentralUserForPolymorphic extends Model implements SyncMaster +{ + use ResourceSyncing, CentralConnection; + + protected $guarded = []; + + public $timestamps = false; + + public $table = 'users'; + + public function resources(): MorphToMany + { + return $this->morphToMany(ResourceTenantForPolymorphic::class, 'tenant_resources', 'tenant_resources', 'resource_global_id', 'tenant_id', 'global_id') + ->using(TenantPivot::class); + } + + public function getTenantModelName(): string + { + return ResourceUserForPolymorphic::class; + } + + public function getGlobalIdentifierKey(): string|int + { + return $this->getAttribute($this->getGlobalIdentifierKeyName()); + } + + public function getGlobalIdentifierKeyName(): string + { + return 'global_id'; + } + + public function getCentralModelName(): string + { + return static::class; + } + + public function getSyncedAttributeNames(): array + { + return [ + 'global_id', + 'name', + 'password', + 'email', + ]; + } +} + +class ResourceUserForPolymorphic extends Model implements Syncable +{ + use ResourceSyncing; + + protected $table = 'users'; + + protected $guarded = []; + + public $timestamps = false; + + public function getGlobalIdentifierKey(): string|int + { + return $this->getAttribute($this->getGlobalIdentifierKeyName()); + } + + public function getGlobalIdentifierKeyName(): string + { + return 'global_id'; + } + + public function getCentralModelName(): string + { + return CentralUserForPolymorphic::class; + } + + public function getSyncedAttributeNames(): array + { + return [ + 'global_id', + 'name', + 'password', + 'email', + ]; + } +} + +class CentralCompanyForPolymorphic extends Model implements SyncMaster +{ + use ResourceSyncing, CentralConnection; + + protected $guarded = []; + + public $timestamps = false; + + public $table = 'companies'; + + public function resources(): MorphToMany + { + return $this->morphToMany(ResourceTenantForPolymorphic::class, 'tenant_resources', 'tenant_resources', 'resource_global_id', 'tenant_id', 'global_id') + ->using(TenantPivot::class); + } + + public function getTenantModelName(): string + { + return ResourceCompanyForPolymorphic::class; + } + + public function getGlobalIdentifierKey(): string|int + { + return $this->getAttribute($this->getGlobalIdentifierKeyName()); + } + + public function getGlobalIdentifierKeyName(): string + { + return 'global_id'; + } + + public function getCentralModelName(): string + { + return static::class; + } + + public function getSyncedAttributeNames(): array + { + return [ + 'global_id', + 'name', + 'email', + ]; + } +} + +class ResourceCompanyForPolymorphic extends Model implements Syncable +{ + use ResourceSyncing; + + protected $table = 'companies'; + + protected $guarded = []; + + public $timestamps = false; + + public function getGlobalIdentifierKey(): string|int + { + return $this->getAttribute($this->getGlobalIdentifierKeyName()); + } + + public function getGlobalIdentifierKeyName(): string + { + return 'global_id'; + } + + public function getCentralModelName(): string + { + return CentralCompanyForPolymorphic::class; + } + + public function getSyncedAttributeNames(): array + { + return [ + 'global_id', + 'name', + 'email', + ]; + } +} +