Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions UPGRADE.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,12 +139,12 @@
- [BREAKING] [#51](https://github.com/shopware/SwagMigrationAssistant/pull/51) - feat!: add migration validation of converted data
- [BREAKING] Added validation check of converted data to `convertData(...)` method in `SwagMigrationAssistant\Migration\Service\MigrationDataConverter`
- Added `hasValidMappingByEntityId(...)` to `SwagMigrationAssistant\Migration\Mapping\MappingService` and `SwagMigrationAssistant\Migration\Mapping\MappingServiceInterface` to check if a mapping exists and is valid for a given entity and source id
- Added new service `SwagMigrationAssistant\Migration\Validation\MigrationValidationService` to validate converted data against Shopware's data definitions
- Added new service `SwagMigrationAssistant\Migration\Validation\MigrationEntityValidationService` to validate converted data against Shopware's data definitions
- Added new events `SwagMigrationAssistant\Migration\Validation\Event\MigrationPreValidationEvent` and `SwagMigrationAssistant\Migration\Validation\Event\MigrationPostValidationEvent` to allow extensions to hook into the validation process
- Added new log classes `ValidationInvalidFieldValueLog`, `ValidationInvalidForeignKeyLog`, `ValidationMissingRequiredFieldLog` and `ValidationUnexpectedFieldLog` to log validation errors
- Added new context class `SwagMigrationAssistant\Migration\Validation\MigrationValidationContext` to pass validation related data
- Added new result class `SwagMigrationAssistant\Migration\Validation\MigrationValidationResult` to collect validation results
- Added new service `SwagMigrationAssistant\Migration\Validation\MigrationValidationService` to validate converted data against Shopware's data definitions in three steps:
- Added new service `SwagMigrationAssistant\Migration\Validation\MigrationEntityValidationService` to validate converted data against Shopware's data definitions in three steps:
- Entity structure: Check for unexpected fields
- Field Validation: Check for missing required fields and invalid field values
- Association Validation: Check for invalid foreign keys
Expand Down
16 changes: 9 additions & 7 deletions src/Controller/DataProviderController.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\Log\Package;
use Shopware\Core\Framework\Routing\ApiRouteScope;
use Shopware\Core\Framework\Routing\RoutingException;
use Shopware\Core\PlatformRequest;
use SwagMigrationAssistant\DataProvider\Provider\ProviderRegistryInterface;
use SwagMigrationAssistant\DataProvider\Service\EnvironmentServiceInterface;
use SwagMigrationAssistant\Exception\MigrationException;
Expand All @@ -32,7 +34,7 @@
use Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\Routing\Attribute\Route;

#[Route(defaults: ['_routeScope' => ['api']])]
#[Route(defaults: [PlatformRequest::ATTRIBUTE_ROUTE_SCOPE => [ApiRouteScope::ID]])]
#[Package('fundamentals@after-sales')]
class DataProviderController extends AbstractController
{
Expand All @@ -54,7 +56,7 @@ public function __construct(
#[Route(
path: '/api/_action/data-provider/get-environment',
name: 'api.admin.data-provider.get-environment',
defaults: ['_acl' => ['admin']],
defaults: [PlatformRequest::ATTRIBUTE_ACL => ['admin']],
methods: [Request::METHOD_GET]
)]
public function getEnvironment(Context $context): JsonResponse
Expand All @@ -67,7 +69,7 @@ public function getEnvironment(Context $context): JsonResponse
#[Route(
path: '/api/_action/data-provider/get-data',
name: 'api.admin.data-provider.get-data',
defaults: ['_acl' => ['admin']],
defaults: [PlatformRequest::ATTRIBUTE_ACL => ['admin']],
methods: [Request::METHOD_GET]
)]
public function getData(Request $request, Context $context): JsonResponse
Expand All @@ -89,7 +91,7 @@ public function getData(Request $request, Context $context): JsonResponse
#[Route(
path: '/api/_action/data-provider/get-total',
name: 'api.admin.data-provider.get-total',
defaults: ['_acl' => ['admin']],
defaults: [PlatformRequest::ATTRIBUTE_ACL => ['admin']],
methods: [Request::METHOD_GET]
)]
public function getTotal(Request $request, Context $context): JsonResponse
Expand All @@ -107,7 +109,7 @@ public function getTotal(Request $request, Context $context): JsonResponse
#[Route(
path: '/api/_action/data-provider/get-table',
name: 'api.admin.data-provider.get-table',
defaults: ['_acl' => ['admin']],
defaults: [PlatformRequest::ATTRIBUTE_ACL => ['admin']],
methods: [Request::METHOD_GET]
)]
public function getTable(Request $request, Context $context): JsonResponse
Expand All @@ -127,7 +129,7 @@ public function getTable(Request $request, Context $context): JsonResponse
#[Route(
path: '/api/_action/data-provider/generate-document',
name: 'api.admin.data-provider.generate-document',
defaults: ['_acl' => ['admin']],
defaults: [PlatformRequest::ATTRIBUTE_ACL => ['admin']],
methods: [Request::METHOD_GET]
)]
public function generateDocument(Request $request, Context $context): JsonResponse
Expand All @@ -154,7 +156,7 @@ public function generateDocument(Request $request, Context $context): JsonRespon
#[Route(
path: '/api/_action/data-provider/download-private-file/{file}',
name: 'api.admin.data-provider.download-private-file',
defaults: ['_acl' => ['admin']],
defaults: [PlatformRequest::ATTRIBUTE_ACL => ['admin']],
methods: [Request::METHOD_GET]
)]
public function downloadPrivateFile(Request $request, Context $context): StreamedResponse|RedirectResponse
Expand Down
150 changes: 150 additions & 0 deletions src/Controller/ErrorResolutionController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
<?php declare(strict_types=1);
/*
* (c) shopware AG <[email protected]>
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace SwagMigrationAssistant\Controller;

use Shopware\Core\Framework\Context;
use Shopware\Core\Framework\DataAbstractionLayer\DefinitionInstanceRegistry;
use Shopware\Core\Framework\Log\Package;
use Shopware\Core\Framework\Routing\ApiRouteScope;
use Shopware\Core\Framework\Validation\WriteConstraintViolationException;
use Shopware\Core\PlatformRequest;
use SwagMigrationAssistant\Exception\MigrationException;
use SwagMigrationAssistant\Migration\ErrorResolution\MigrationFieldExampleGenerator;
use SwagMigrationAssistant\Migration\Validation\Exception\MigrationValidationException;
use SwagMigrationAssistant\Migration\Validation\MigrationFieldValidationService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Attribute\Route;

#[Route(defaults: [PlatformRequest::ATTRIBUTE_ROUTE_SCOPE => [ApiRouteScope::ID]])]
#[Package('fundamentals@after-sales')]
class ErrorResolutionController extends AbstractController
{
/**
* @internal
*/
public function __construct(
private readonly DefinitionInstanceRegistry $definitionRegistry,
private readonly MigrationFieldValidationService $fieldValidationService,
) {
}

#[Route(
path: '/api/_action/migration/error-resolution/validate',
name: 'api.admin.migration.error-resolution.validate',
defaults: [PlatformRequest::ATTRIBUTE_ACL => ['swag_migration.viewer']],
methods: [Request::METHOD_POST]
)]
public function validateResolution(Request $request, Context $context): JsonResponse
{
$entityName = (string) $request->request->get('entityName');
$fieldName = (string) $request->request->get('fieldName');

if ($entityName === '') {
throw MigrationException::missingRequestParameter('entityName');
}

if ($fieldName === '') {
throw MigrationException::missingRequestParameter('fieldName');
}

$fieldValue = $this->decodeFieldValue($request->request->all()['fieldValue'] ?? null);

if ($fieldValue === null) {
throw MigrationException::missingRequestParameter('fieldValue');
}

try {
$this->fieldValidationService->validateField(
$entityName,
$fieldName,
$fieldValue,
$context,
);
} catch (MigrationValidationException $exception) {
$previous = $exception->getPrevious();

if ($previous instanceof WriteConstraintViolationException) {
return new JsonResponse([
'valid' => false,
'violations' => $previous->toArray(),
]);
}

return new JsonResponse([
'valid' => false,
'violations' => [['message' => $exception->getMessage()]],
]);
} catch (\Exception $exception) {
return new JsonResponse([
'valid' => false,
'violations' => [['message' => $exception->getMessage()]],
]);
}

return new JsonResponse([
'valid' => true,
'violations' => [],
]);
}

#[Route(
path: '/api/_action/migration/error-resolution/example-field-structure',
name: 'api.admin.migration.error-resolution.example-field-structure',
defaults: [PlatformRequest::ATTRIBUTE_ACL => ['swag_migration.viewer']],
methods: [Request::METHOD_POST]
)]
public function getExampleFieldStructure(Request $request): JsonResponse
{
$entityName = (string) $request->request->get('entityName');
$fieldName = (string) $request->request->get('fieldName');

if ($entityName === '') {
throw MigrationException::missingRequestParameter('entityName');
}

if ($fieldName === '') {
throw MigrationException::missingRequestParameter('fieldName');
}

$entityDefinition = $this->definitionRegistry->getByEntityName($entityName);
$fields = $entityDefinition->getFields();

if (!$fields->has($fieldName)) {
throw MigrationValidationException::entityFieldNotFound($entityName, $fieldName);
}

$field = $fields->get($fieldName);

$response = [
'fieldType' => MigrationFieldExampleGenerator::getFieldType($field),
'example' => MigrationFieldExampleGenerator::generateExample($field),
];

return new JsonResponse($response);
}

/**
* @return array<array-key, mixed>|bool|float|int|string|null
*/
private function decodeFieldValue(mixed $value): array|bool|float|int|string|null
{
if ($value === null || $value === '' || $value === []) {
return null;
}

if (!\is_string($value)) {
return $value;
}

$decoded = \json_decode($value, true);

return \json_last_error() === \JSON_ERROR_NONE ? $decoded : $value;
}
}
40 changes: 31 additions & 9 deletions src/Controller/HistoryController.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@

use Shopware\Core\Framework\Context;
use Shopware\Core\Framework\Log\Package;
use Shopware\Core\Framework\Routing\ApiRouteScope;
use Shopware\Core\Framework\Routing\RoutingException;
use Shopware\Core\PlatformRequest;
use SwagMigrationAssistant\Exception\MigrationException;
use SwagMigrationAssistant\Migration\History\HistoryServiceInterface;
use SwagMigrationAssistant\Migration\History\LogGroupingService;
Expand All @@ -22,7 +24,7 @@
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;

#[Route(defaults: ['_routeScope' => ['api']])]
#[Route(defaults: [PlatformRequest::ATTRIBUTE_ROUTE_SCOPE => [ApiRouteScope::ID]])]
#[Package('fundamentals@after-sales')]
class HistoryController extends AbstractController
{
Expand All @@ -35,7 +37,12 @@ public function __construct(
) {
}

#[Route(path: '/api/_action/migration/get-grouped-logs-of-run', name: 'api.admin.migration.get-grouped-logs-of-run', methods: ['GET'], defaults: ['_acl' => ['swag_migration.viewer']])]
#[Route(
path: '/api/_action/migration/get-grouped-logs-of-run',
name: 'api.admin.migration.get-grouped-logs-of-run',
defaults: [PlatformRequest::ATTRIBUTE_ACL => ['swag_migration.viewer']],
methods: [Request::METHOD_GET],
)]
public function getGroupedLogsOfRun(Request $request, Context $context): JsonResponse
{
$runUuid = $request->query->getAlnum('runUuid');
Expand All @@ -60,7 +67,12 @@ public function getGroupedLogsOfRun(Request $request, Context $context): JsonRes
]);
}

#[Route(path: '/api/_action/migration/download-logs-of-run', name: 'api.admin.migration.download-logs-of-run', methods: ['POST'], defaults: ['auth_required' => false, '_acl' => ['swag_migration.viewer']])]
#[Route(
path: '/api/_action/migration/download-logs-of-run',
name: 'api.admin.migration.download-logs-of-run',
defaults: ['auth_required' => false, PlatformRequest::ATTRIBUTE_ACL => ['swag_migration.viewer']],
methods: [Request::METHOD_POST],
)]
public function downloadLogsOfRun(Request $request, Context $context): StreamedResponse
{
$runUuid = $request->request->getAlnum('runUuid');
Expand All @@ -86,7 +98,12 @@ public function downloadLogsOfRun(Request $request, Context $context): StreamedR
return $response;
}

#[Route(path: '/api/_action/migration/clear-data-of-run', name: 'api.admin.migration.clear-data-of-run', methods: ['POST'], defaults: ['_acl' => ['swag_migration.deleter']])]
#[Route(
path: '/api/_action/migration/clear-data-of-run',
name: 'api.admin.migration.clear-data-of-run',
defaults: [PlatformRequest::ATTRIBUTE_ACL => ['swag_migration.deleter']],
methods: [Request::METHOD_POST],
)]
public function clearDataOfRun(Request $request, Context $context): Response
{
$runUuid = $request->request->getAlnum('runUuid');
Expand All @@ -104,7 +121,12 @@ public function clearDataOfRun(Request $request, Context $context): Response
return new Response();
}

#[Route(path: '/api/_action/migration/is-media-processing', name: 'api.admin.migration.is-media-processing', methods: ['GET'], defaults: ['_acl' => ['swag_migration_history:read']])]
#[Route(
path: '/api/_action/migration/is-media-processing',
name: 'api.admin.migration.is-media-processing',
defaults: [PlatformRequest::ATTRIBUTE_ACL => ['swag_migration_history:read']],
methods: [Request::METHOD_GET],
)]
public function isMediaProcessing(): JsonResponse
{
$result = $this->historyService->isMediaProcessing();
Expand All @@ -115,8 +137,8 @@ public function isMediaProcessing(): JsonResponse
#[Route(
path: '/api/_action/migration/get-log-groups',
name: 'api.admin.migration.get-log-groups',
methods: ['GET'],
defaults: ['_acl' => ['swag_migration.viewer']]
defaults: [PlatformRequest::ATTRIBUTE_ACL => ['swag_migration.viewer']],
methods: [Request::METHOD_GET],
)]
public function getLogGroups(Request $request, Context $context): JsonResponse
{
Expand Down Expand Up @@ -166,8 +188,8 @@ public function getLogGroups(Request $request, Context $context): JsonResponse
#[Route(
path: '/api/_action/migration/get-all-log-ids',
name: 'api.admin.migration.get-all-log-ids',
methods: ['POST'],
defaults: ['_acl' => ['swag_migration.viewer']]
defaults: [PlatformRequest::ATTRIBUTE_ACL => ['swag_migration.viewer']],
methods: [Request::METHOD_POST],
)]
public function getAllLogIds(Request $request): JsonResponse
{
Expand Down
18 changes: 15 additions & 3 deletions src/Controller/PremappingController.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@

use Shopware\Core\Framework\Context;
use Shopware\Core\Framework\Log\Package;
use Shopware\Core\Framework\Routing\ApiRouteScope;
use Shopware\Core\Framework\Routing\RoutingException;
use Shopware\Core\PlatformRequest;
use SwagMigrationAssistant\Migration\MigrationContextFactoryInterface;
use SwagMigrationAssistant\Migration\Service\PremappingServiceInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
Expand All @@ -18,7 +20,7 @@
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;

#[Route(defaults: ['_routeScope' => ['api']])]
#[Route(defaults: [PlatformRequest::ATTRIBUTE_ROUTE_SCOPE => [ApiRouteScope::ID]])]
#[Package('fundamentals@after-sales')]
class PremappingController extends AbstractController
{
Expand All @@ -31,7 +33,12 @@ public function __construct(
) {
}

#[Route(path: '/api/_action/migration/generate-premapping', name: 'api.admin.migration.generate-premapping', methods: ['POST'], defaults: ['_acl' => ['swag_migration.editor']])]
#[Route(
path: '/api/_action/migration/generate-premapping',
name: 'api.admin.migration.generate-premapping',
defaults: [PlatformRequest::ATTRIBUTE_ACL => ['swag_migration.editor']],
methods: [Request::METHOD_POST],
)]
public function generatePremapping(Request $request, Context $context): JsonResponse
{
$dataSelectionIds = $request->request->all('dataSelectionIds');
Expand All @@ -44,7 +51,12 @@ public function generatePremapping(Request $request, Context $context): JsonResp
return new JsonResponse($this->premappingService->generatePremapping($context, $migrationContext, $dataSelectionIds));
}

#[Route(path: '/api/_action/migration/write-premapping', name: 'api.admin.migration.write-premapping', methods: ['POST'], defaults: ['_acl' => ['swag_migration.editor']])]
#[Route(
path: '/api/_action/migration/write-premapping',
name: 'api.admin.migration.write-premapping',
defaults: [PlatformRequest::ATTRIBUTE_ACL => ['swag_migration.editor']],
methods: [Request::METHOD_POST],
)]
public function writePremapping(Request $request, Context $context): Response
{
$premapping = $request->request->all('premapping');
Expand Down
Loading
Loading