From 691f1381b13cf80b63f9a203fc95fb7d9e8a4a73 Mon Sep 17 00:00:00 2001 From: Valerii Naida Date: Wed, 23 Oct 2019 14:33:52 -0500 Subject: [PATCH 1/2] Asynchronous Import base descriptions -- Intro -- REST API -- Modularity (API / Extension points) --- .../asycnhronous-import-service.md | 16 - .../asycnhronous-import.md | 43 ++ .../asynchronous-import/base-extension.md | 374 ------------------ .../asynchronous-import/import-ui.md | 2 - .../modularity/advanced-pricing.md | 4 + .../modularity/data-converting.md | 146 +++++++ .../modularity/data-exchanging.md | 211 ++++++++++ .../modularity/import-csv.md | 174 ++++++++ .../modularity/source-data-retrieving.md | 153 +++++++ .../asynchronous-import/rest-api.md.md | 85 ++++ 10 files changed, 816 insertions(+), 392 deletions(-) delete mode 100644 design-documents/asynchronous-import/asycnhronous-import-service.md create mode 100644 design-documents/asynchronous-import/asycnhronous-import.md delete mode 100644 design-documents/asynchronous-import/base-extension.md delete mode 100644 design-documents/asynchronous-import/import-ui.md create mode 100644 design-documents/asynchronous-import/modularity/advanced-pricing.md create mode 100644 design-documents/asynchronous-import/modularity/data-converting.md create mode 100644 design-documents/asynchronous-import/modularity/data-exchanging.md create mode 100644 design-documents/asynchronous-import/modularity/import-csv.md create mode 100644 design-documents/asynchronous-import/modularity/source-data-retrieving.md create mode 100644 design-documents/asynchronous-import/rest-api.md.md diff --git a/design-documents/asynchronous-import/asycnhronous-import-service.md b/design-documents/asynchronous-import/asycnhronous-import-service.md deleted file mode 100644 index ca638f019..000000000 --- a/design-documents/asynchronous-import/asycnhronous-import-service.md +++ /dev/null @@ -1,16 +0,0 @@ - -# Asynchronous Import as separate service -## Context - -Main idea of extension is to replace current Import module with new implementation that will allow to users import objects in Magento by using Asynchronous approach. - -Basic idea: -- User upload *.csv (or any other) file format -- Extension receive file, validate it and return to user File UUID -- By using this File UUID user can start import with customer parameters (if applicable) -- Module will parse file, split it on single messages and sends to Asynchronous API. -- Later on user can request back status of import and resubmit objects which were failed during the import - -Proposed to split on several phases -- [Phase 1](base-extension.md) -- [Phase 2](import-ui.md) \ No newline at end of file diff --git a/design-documents/asynchronous-import/asycnhronous-import.md b/design-documents/asynchronous-import/asycnhronous-import.md new file mode 100644 index 000000000..a24544eb2 --- /dev/null +++ b/design-documents/asynchronous-import/asycnhronous-import.md @@ -0,0 +1,43 @@ +# Asynchronous Import as separate service + +Main idea of extension is to replace current Import module with new implementation that will allow to users import objects in Magento by using Asynchronous approach. + +## Workflow + +- Upload source data to instance storage (optional step); +- Retrieve source data from Source (partial reading); +- Parse source data; +- Apply data converting rule; +- Exchange data with Magento instance via Bulk API Service contracts / Bulk API REST (partial processing); +- Later an user can request balk status of import and resubmit objects which were failed during the import; + +## REST API + +- [REST API](rest-api.md) + +## Modularity (API / Extension points) + +- [Start Import based on CSV data (main entry point)](modularity/import-csv.md) +- [Source data retrieving](modularity/source-data-retrieving.md) +- [Data converting before import](modularity/data-converting.md) +- [Data exchanging with Magento instance](modularity/data-exchanging.md) +- [Import advanced pricing](modularity/advanced-pricing.md) + +## MVP + +[MVP Board](https://app.zenhub.com/workspaces/async-import-5b5f349bd6768d6255917727/reports/burndown?milestoneId=4625823) + +- Sources data retrieving: *HTTP, HTTPS, base64*; +- CSV reader; +- Data Converting Rules; +- Import Data Exchanging; +- Import configuration; +- Product import; +- Stock status import; +- Advanced pricing import; +- Documentation; + +## TODO +- Design of `Import configuration`; +- Design of `Restart failed operations`; +- Design of `Get Import status`; diff --git a/design-documents/asynchronous-import/base-extension.md b/design-documents/asynchronous-import/base-extension.md deleted file mode 100644 index e3e166973..000000000 --- a/design-documents/asynchronous-import/base-extension.md +++ /dev/null @@ -1,374 +0,0 @@ -# Phase 1 - -With phase 1 we are planning to develop next functionality: -- Endpoint to receive file -- Endpoint to start processing -- Endpoint to receive status - -## File Upload Endpoint -### Source Upload endpoint - -So import will start from uploading import Source file. Currently we will support "csv" files format - -POST `/V1/import/source/csv` - -This request can accept files from different sources: -- Local file path -- Direct link to the file -- Base64 encoded file content - -#### Local file path - -Path is relative from Magento Root folder - -``` -{ - "source": { - "import_data": "var/catalog_product.csv", - "import_type": "local_path", - "uuid": "UUID", - "format": { - "csv_separator": "string", - "csv_enclosure": "string", - "csv_delimiter": "string", - "multiple_value_separator": "string" - } - } -} -``` - -#### Direct link to the file - -``` -{ - "source": { - "import_data": "http://some.domain/file.csv", - "import_type": "external", - "uuid": "UUID", - "format": { - "csv_separator": "string", - "csv_enclosure": "string", - "csv_delimiter": "string", - "multiple_value_separator": "string" - } - } -} -``` - -#### Base64 encoded file content - -``` -{ - "source": { - "import_data": "c2t1LHN0b3JlX3ZpZXdfY29kZSxhdHRyaWJ1dGVfc2V0X2NvZGUscHJvZHVjdF90eXBlLGNhdGVnb3JpZXMscHJvZHVjdF93ZWJzaXRlcyxuYW1lLGRlc2NyaXB0aW9uLHNob3J0X2Rlc2NyaXB0aW9uLHdlaWdodCxwcm9kdWN0X29ubGluZSx0YXhfY2xhc3NfbmFtZSx2aXNpYmlsaXR5LHBya......", - "import_type": "base64_encoded_data", - "uuid": "UUID", - "format": { - "csv_separator": "string", - "csv_enclosure": "string", - "csv_delimiter": "string", - "multiple_value_separator": "string" - } - } -} -``` - -Import of big file also can be divided in several parts. -For this case we have separate endpoint - -POST `/V1/import/source/csv/partial/` - -Input request will looks like: - -``` -{ - "source": { - "import_data": "c2t1LHN0b3JlX3ZpZXdfY29kZSxhdHRyaWJ1dGVfc2V0X2NvZGUscHJvZHVjdF90eXBlLGNhdGVnb3JpZXMscHJvZHVjdF93ZWJzaXRlcyxuYW1lLGRlc2NyaXB0aW9uLHNob3J0X2Rlc2NyaXB0aW9uLHdlaWdodCxwcm9kdWN0X29ubGluZSx0YXhfY2xhc3NfbmFtZSx2aXNpYmlsaXR5LHBya...", - "data_hash" : "sha256 encoded data of the full 'import_data' value" - "pieces_count": "5" - "piece_number": "1", - "import_type": "base64_encoded_data", - "uuid": "UUID", - "format": { - "csv_separator": "string", - "csv_enclosure": "string", - "csv_delimiter": "string", - "multiple_value_separator": "string" - } - } -} -``` -where *import_data* is a 1/N part of the whole content, and *data_hash* contains sha256 hash of full import_data body. - -`pieces_count` - its an amount of pieces that will be transferred for 1 file. We need it to be sure that import is completed and then we could detect if it was successfully finished or failed - -`piece_number` - its a number that detects which part of file currently transferred. This is required to have to support Asynchronous File import when we dont need to send parts in correct sequence - -Those parts could be send asynchronously. They will be merged together after all data are transferred. - -### Return values - -As return user will receive: - -| Key | Value | -| --- | --- | -| uuid | Imported File ID | -| status | Status of this file. Possible values: completed, uploaded, failed | -| error | Error message if exists | - -Example: - -``` -{ - "uuid": null, - "status": null, - "error": null, - "source": { - // Source object is coming here - } -} -``` - -### Update Imported Source Format - -Its possible also to Update Format - -PUT `/V1/import/source/csv/:uuid` - -``` -{ - "source": { - "uuid": "uuid", - "format": { - "csv_separator": "string", - "csv_enclosure": "string", - "csv_delimiter": "string", - "multiple_value_separator": "string" - } - } -} -``` - -### Delete Imported Source Format - -DELETE `/V1/import/source/:uuid` - -### Get List of sources - -GET `/V1/import/sources/?searchCriteria` - -Will return list of Source that was uploaded before. - -``` -{ - "sources": [ - { - "uuid": "uuid", - "format": { - "csv_separator": "string", - "csv_enclosure": "string", - "csv_delimiter": "string", - "multiple_value_separator": "string" - } - }, - { - "uuid": "uuid", - "format": { - "csv_separator": "string", - "csv_enclosure": "string", - "csv_delimiter": "string", - "multiple_value_separator": "string" - } - } - ..... - ], - "search_criteria": { - "filter_groups": [ - { - "filters": [ - { - "field": "string", - "value": "string", - "condition_type": "string" - } - ] - } - ], - "sort_orders": [ - { - "field": "string", - "direction": "string" - } - ], - "page_size": 0, - "current_page": 0 - }, - "total_count": 0 -} -``` - -## Start File Import Endpoint -### Main endpoint - -Current Endpoint starts import process based on FileID. Where Module will read file, split it into message and send to Async API - -POST `/V1/import/start/{uuid}` - -`type` - its an import type like: catalog_product, catalog_category, customer, order ... etc... - -Start File Import - -``` -{ - "importConfig": { - "import_type": "catalog_product", - "import_strategy": "add_update, delete, replace", - "validation_strategy": "string", - "allowed_error_count": 0, - "import_image_archive": "string", - "import_images_file_dir": "string", - "mapping": [ - { - "name": "string", - "source_path": "string", - "target_path": "string", - "target_value": "mixed", - "processing_rules": [ - { - "sort": int, - "function": "string", - "args": "array" - } - ] - } - ] - } -} -``` - -| Key | Value | -| --- | --- | -| uuid | UUID that was returned by source upload call | -| behaviour | Import behaviour (add_update, delete, update, add, replace) | -| import_image_archive | Relative path to product images archive file | -| import_images_file_dir | Relative path to product images files | -| validation_strategy | Moved from main standard Import, not sure if we will use if | -| allowed_error_count | How many errors allowed to be during the import | -| mapping | Data format mapping | - - -#### Return - -``` -{ - "uuid": string - "status": "proccessing", - "error": "string" -} -``` - -### Get import status - -Receive information about imported file -GET `/V1/import/:uuid` - -#### Return - -Will be returned list of objects that we tried to import - -``` -{ - "status": "string", - "error": "string", - "uuid": 0, - "entity_type": "catalog_product, customers ....", - "user_id": "User ID who created this request", - "user_type": "User Type who created this request", - "items": [ - { - "uuid": 0, - "status": "", - "serialized_data": "", - "result_serialized_data": "", - "error_code":"", - "result_message":"" - }] -} -``` - -| Key | Value | -| --- | --- | -| uuid | Imported File ID | -| status | Status of this file. Possible values: completed, not_completed, error | -| error | Error message if exists | -| entity_type | Import type: eg. customers, products, etc ... | -| items | List of items that were imported. As an array | - -### Get single import operation status - -Receive information about imported file -GET `/V1/import/operation/:uuid` - -#### Return - -Will be returned specific object that we tried to import - -``` -{ - "status": "string", - "error": "string", - "uuid": 0, - "entity_type": "catalog_product, customers ....", - "user_id": "User ID who created this request", - "user_type": "User Type who created this request", - "items": [ - { - "uuid": 0, - "status": "", - "serialized_data": "", - "result_serialized_data": "", - "error_code":"", - "result_message":"" - }] -} -``` - -| Key | Value | -| --- | --- | -| uuid | Imported File ID | -| status | Status of this file. Possible values: completed, not_completed, error | -| error | Error message if exists | -| entity_type | Import type: eg. customers, products, etc ... | -| items | Item that were requested. For do not change interfaces it will be still as an array, but always one item | - -##### And Item object will contain - -This values are based on magento "magento_operation" table - -| Key | Value | -| --- | --- | -| uuid | Entity UUID - defined by customer OR auto-generated by system | -| status | Import status. "pending, failed, processing, completed" | -| serialized_data | Data that was send to Magento, via Async endpoint | -| result_serialized_data | Data that Magento returned for this object after import | -| error_code | Error code | -| result_message | Result message of operation execution | - - -## Repeatable call endpoint - -Main idea, is in case some operation import failure, that user could align input data and repeat import for this particular item - -PUT `/V1/import/operation-id/{uuid}` - -``` -{ - "serialized_data":"" -} -``` - -### Return - -``` -[] -``` diff --git a/design-documents/asynchronous-import/import-ui.md b/design-documents/asynchronous-import/import-ui.md deleted file mode 100644 index a31fab418..000000000 --- a/design-documents/asynchronous-import/import-ui.md +++ /dev/null @@ -1,2 +0,0 @@ -# Phase 2 -In a second phase planned to build UI for Asynchronous Import. It will be separate Magento module that will use API to communicate with Asynchronous import module. diff --git a/design-documents/asynchronous-import/modularity/advanced-pricing.md b/design-documents/asynchronous-import/modularity/advanced-pricing.md new file mode 100644 index 000000000..1611bec02 --- /dev/null +++ b/design-documents/asynchronous-import/modularity/advanced-pricing.md @@ -0,0 +1,4 @@ +# Import advanced pricing + +## Modules +- `AsynchronousImportAdvancedPricing` diff --git a/design-documents/asynchronous-import/modularity/data-converting.md b/design-documents/asynchronous-import/modularity/data-converting.md new file mode 100644 index 000000000..33be34e93 --- /dev/null +++ b/design-documents/asynchronous-import/modularity/data-converting.md @@ -0,0 +1,146 @@ +# Data converting before import + +## Modules + +- `AsynchronousImportDataConvertingApi` +- `AsynchronousImportDataConverting` + +## Implementation + +### API + +```php +namespace Magento\AsynchronousImportDataConvertingApi\Api; + +use Magento\AsynchronousImportDataConvertingApi\Api\Data\ConvertingRuleInterface; +use Magento\Framework\Validation\ValidationException; + +/** + * Apply converting rules to import data operation. Uses differect strategies for rules applying + * Responsible for data changing before import + * + * @api + */ +interface ApplyConvertingRulesInterface +{ + /** + * Apply converting rules to import data operation. Uses differect strategies for rules applying + * + * @param array $importData + * @param ConvertingRuleInterface[] $convertingRules + * @return array + * @throws ValidationException + * @throws ApplyConvertingRulesException + */ + public function execute( + array $importData, + array $convertingRules + ): array; +} +``` + +```php +namespace Magento\AsynchronousImportDataConvertingApi\Api\Data; + +/** + * Describes how to change data before import + * + * @api + */ +interface ConvertingRuleInterface +{ + /** + * Get rule identifier + * + * @return string + */ + public function getIdentifier(): string; + + /** + * Get rule parameters + * + * @return string[]|null Null value is needed fro SOAP parser + */ + public function getParameters(): array; + + /** + * Get sort + * + * @return int|null + */ + public function getSort(): ?int; + + /** + * Get apply to + * + * @return string[]|null Null value is needed fro SOAP parser + */ + public function getApplyTo(): array; +} +``` + +### Extension points + +```php +namespace Magento\AsynchronousImportDataConvertingApi\Model; + +use Magento\AsynchronousImportDataConvertingApi\Api\Data\ConvertingRuleInterface; + +/** + * Extension point for adding converting rule applying algorithms + * Represents concrete strategy + * + * @api + */ +interface ApplyConvertingRuleStrategyInterface +{ + /** + * Converting rule applying strategy + * + * @param array $importData + * @param ConvertingRuleInterface $convertingRule + * @return array + */ + public function execute( + array $importData, + ConvertingRuleInterface $convertingRule + ): array; +} +``` + +```php +namespace Magento\AsynchronousImportDataConvertingApi\Model; + +/** + * Extension point for adding converting rule validators via DI configuration + * + * @api + */ +class ConvertingRuleValidatorChain implements ConvertingRuleValidatorInterface +{ + ... +} +``` + +```php +namespace Magento\AsynchronousImportDataConvertingApi\Model; + +use Magento\AsynchronousImportDataConvertingApi\Api\Data\ConvertingRuleInterface; +use Magento\Framework\Validation\ValidationResult; + +/** + * Extension point for adding converting rule validators + * + * @api + */ +interface ConvertingRuleValidatorInterface +{ + /** + * Validate converting rule + * + * @param ConvertingRuleInterface $convertingRule + * @return ValidationResult + */ + public function validate(ConvertingRuleInterface $convertingRule): ValidationResult; +} +``` diff --git a/design-documents/asynchronous-import/modularity/data-exchanging.md b/design-documents/asynchronous-import/modularity/data-exchanging.md new file mode 100644 index 000000000..44307aea4 --- /dev/null +++ b/design-documents/asynchronous-import/modularity/data-exchanging.md @@ -0,0 +1,211 @@ +# Data exchanging with Magento instance + +## Modules + +- `AsynchronousImportDataExchangingApi` +- `AsynchronousImportDataExchanging` + +## Implementation + +### API + +```php +namespace Magento\AsynchronousImportDataExchangingApi\Api; + +use Magento\AsynchronousImportDataExchangingApi\Api\Data\ImportInterface; +use Magento\Framework\Validation\ValidationException; + +/** + * Operation for exchanging import data with destination instance. Uses differect strategies for data import + * + * @api + */ +interface ExchangeImportDataInterface +{ + /** + * Operation for exchanging import data with destination instance + * + * @param ImportInterface $import + * @param array $importData + * @return void + * @throws ValidationException + * @throws ImportDataExchangeException + */ + public function execute(ImportInterface $import, array $importData): void; +} +``` + +```php +namespace Magento\AsynchronousImportDataExchangingApi\Api\Data; + +use Magento\Framework\Api\ExtensibleDataInterface; + +/** + * Describes how to import data + * + * @api + */ +interface ImportInterface extends ExtensibleDataInterface +{ + /** + * Get import uuid + * + * @return string|null + */ + public function getUuid(): ?string; + + /** + * Get import type + * + * @return string + */ + public function getImportType(): string; + + /** + * Get import behaviour + * + * @return string + */ + public function getImportBehaviour(): string; + + /** + * Get existing extension attributes object + * + * Used fully qualified namespaces in annotations for proper work of extension interface/class code generation + * + * @return \Magento\AsynchronousImportDataExchangingApi\Api\Data\ImportExtensionInterface|null + */ + public function getExtensionAttributes(): ?ImportExtensionInterface; +} +``` + +```php +namespace Magento\AsynchronousImportDataExchangingApi\Api; + +use Magento\AsynchronousImportDataExchangingApi\Api\Data\ImportStatusInterface; +use Magento\Framework\Exception\NotFoundException; + +/** + * Get import status operation + * + * @api + */ +interface GetImportStatusInterface +{ + /** + * Get import status operation + * + * @param string $uuid + * @return ImportStatusInterface + * @throws NotFoundException + */ + public function execute(string $uuid): ImportStatusInterface; +} +``` + +```php +namespace Magento\AsynchronousImportDataExchangingApi\Api\Data; + +/** + * Represents import status + * + * @api + */ +interface ImportStatusInterface +{ + public const STATUS_RUNNING = 'running'; + public const STATUS_COMPLETED = 'completed'; + public const STATUS_FAIL = 'fail'; + + /** + * Get status + * + * @return string One of const STATUS_* + */ + public function getStatus(): string; + + /** + * Get Errors + * + * @return array + */ + public function getErrors(): array; + + /** + * Get created at + * + * @return string|null + */ + public function getCreatedAt(): ?string; + + /** + * Get finished at + * + * @return string|null + */ + public function getFinishedAt(): ?string; +} +``` + +### Extension points + +```php +namespace Magento\AsynchronousImportDataExchangingApi\Model; + +use Magento\AsynchronousImportDataExchangingApi\Api\Data\ImportInterface; + +/** + * Extension point for adding data import algorithms + * Represents concrete strategy + * + * @api + */ +interface ExchangeDataStrategyInterface +{ + /** + * Data import strategy + * + * @param ImportInterface $import + * @param array $importData + * @return void + */ + public function execute(ImportInterface $import, array $importData): void; +} +``` + +```php +namespace Magento\AsynchronousImportDataExchangingApi\Model; + +/** + * Extension point for adding import request validators via DI configuration + * + * @api + */ +class ImportValidatorChain implements ImportValidatorInterface +{ + ... +} +``` + +```php +namespace Magento\AsynchronousImportDataExchangingApi\Model; + +use Magento\AsynchronousImportDataExchangingApi\Api\Data\ImportInterface; +use Magento\Framework\Validation\ValidationResult; + +/** + * Extension point for adding import request validators + * + * @api + */ +interface ImportValidatorInterface +{ + /** + * Import validation. Extension point for base validation + * + * @param ImportInterface $import + * @return ValidationResult + */ + public function validate(ImportInterface $import): ValidationResult; +} +``` diff --git a/design-documents/asynchronous-import/modularity/import-csv.md b/design-documents/asynchronous-import/modularity/import-csv.md new file mode 100644 index 000000000..cdf59d090 --- /dev/null +++ b/design-documents/asynchronous-import/modularity/import-csv.md @@ -0,0 +1,174 @@ +# Start Import based on CSV data (main entry point) + +## Modules + +- `AsynchronousImportCsvApi` +- `AsynchronousImportCsv` + +## Implementation + +### API + +```php +namespace Magento\AsynchronousImportCsvApi\Api; + +use Magento\AsynchronousImportDataConvertingApi\Api\ApplyConvertingRulesException; +use Magento\AsynchronousImportCsvApi\Api\Data\CsvFormatInterface; +use Magento\AsynchronousImportDataConvertingApi\Api\Data\ConvertingRuleInterface; +use Magento\AsynchronousImportDataExchangingApi\Api\Data\ImportInterface; +use Magento\AsynchronousImportDataExchangingApi\Api\ImportDataExchangeException; +use Magento\AsynchronousImportSourceDataRetrievingApi\Api\Data\SourceInterface; +use Magento\AsynchronousImportSourceDataRetrievingApi\Api\SourceDataRetrievingException; +use Magento\Framework\Validation\ValidationException; + +/** + * Start import operation + * + * @api + */ +interface StartImportInterface +{ + /** + * Start import operation + * + * @param SourceInterface $source Describes how to retrieve data from data source + * @param ImportInterface $import Describes how to import data + * @param CsvFormatInterface|null $format Describes how to parse data + * @param ConvertingRuleInterface[] $convertingRules Describes how to change data before import + * @return string + * @throws ValidationException + * @throws SourceDataRetrievingException + * @throws ApplyConvertingRulesException + * @throws ImportDataExchangeException + */ + public function execute( + SourceInterface $source, + ImportInterface $import, + CsvFormatInterface $format = null, + array $convertingRules = [] + ): string; +} +``` + +```php +namespace Magento\AsynchronousImportCsvApi\Api\Data; + +use Magento\Framework\Api\ExtensibleDataInterface; + +/** + * Describes how to parse data + * + * @api + */ +interface CsvFormatInterface extends ExtensibleDataInterface +{ + /** + * Get CSV Escape + * + * @return string|null + */ + public function getEscape(): ?string; + + /** + * Get CSV Enclosure + * + * @return string|null + */ + public function getEnclosure(): ?string; + + /** + * Get CSV Delimiter + * + * @return string|null + */ + public function getDelimiter(): ?string; + + /** + * Get Multiple Value Separator + * + * @return string|null + */ + public function getMultipleValueSeparator(): ?string; + + /** + * Get existing extension attributes object + * + * Used fully qualified namespaces in annotations for proper work of extension interface/class code generation + * + * @return \Magento\AsynchronousImportCsvApi\Api\Data\CsvFormatExtensionInterface|null + */ + public function getExtensionAttributes(): ?CsvFormatExtensionInterface; +} +``` + +```php +/** + * Describes how to parse data + * + * @api + */ +interface CsvFormatInterface extends ExtensibleDataInterface +{ + /** + * Get CSV Escape + * + * @return string|null + */ + public function getEscape(): ?string; + + /** + * Get CSV Enclosure + * + * @return string|null + */ + public function getEnclosure(): ?string; + + /** + * Get CSV Delimiter + * + * @return string|null + */ + public function getDelimiter(): ?string; + + /** + * Get Multiple Value Separator + * + * @return string|null + */ + public function getMultipleValueSeparator(): ?string; + + /** + * Get existing extension attributes object + * + * Used fully qualified namespaces in annotations for proper work of extension interface/class code generation + * + * @return \Magento\AsynchronousImportCsvApi\Api\Data\CsvFormatExtensionInterface|null + */ + public function getExtensionAttributes(): ?CsvFormatExtensionInterface; +} +``` + +### Extension points + +```php +namespace Magento\AsynchronousImportCsvApi\Model; + +use Magento\AsynchronousImportCsvApi\Api\Data\CsvFormatInterface; + +/** + * Extension point for data parsing (based on passed CSV format) + * + * @api + */ +interface DataParserInterface +{ + /** + * Extension point for data parsing (based on passed CSV format) + * + * @param array $data + * @param CsvFormatInterface|null $csvFormat + * @return array + */ + public function execute(array $data, CsvFormatInterface $csvFormat = null): array; +} +``` diff --git a/design-documents/asynchronous-import/modularity/source-data-retrieving.md b/design-documents/asynchronous-import/modularity/source-data-retrieving.md new file mode 100644 index 000000000..791dae9f3 --- /dev/null +++ b/design-documents/asynchronous-import/modularity/source-data-retrieving.md @@ -0,0 +1,153 @@ +# Source data retrieving + +## Info + +- Responsible for retrieving source data; +- Provide ability to easily add new adapters for different data sources; +- Provide ability to use streaming in different adapters (do not load whole data into memory); + +## Modules + +- `AsynchronousImportSourceDataRetrievingApi` +- `AsynchronousImportSourceDataRetrieving` + +## Implementation + +### API + +```php +namespace Magento\AsynchronousImportSourceDataRetrievingApi\Api; + +use Magento\AsynchronousImportSourceDataRetrievingApi\Api\Data\SourceInterface; +use Magento\AsynchronousImportSourceDataRetrievingApi\Api\Data\SourceDataInterface; +use Magento\Framework\Validation\ValidationException; + +/** + * Retrieve source data operation. Uses differect strategies for source data retrieving + * + * @api + */ +interface RetrieveSourceDataInterface +{ + /** + * Retrieve source data operation. Uses differect strategies for source data retrieving + * + * @param SourceInterface $source + * @return SourceDataInterface + * @throws ValidationException + * @throws SourceDataRetrievingException + */ + public function execute(SourceInterface $source): SourceDataInterface; +} +``` + +```php +namespace Magento\AsynchronousImportSourceDataRetrievingApi\Api\Data; + +/** + * Describes how to retrieve data from data source + * + * @api + */ +interface SourceInterface +{ + /** + * Get source type + * + * @return string + */ + public function getSourceType(): string; + + /** + * Get source definition + * + * @return string + */ + public function getSourceDefinition(): string; + + /** + * Get source data format + * + * @return string + */ + public function getSourceDataFormat(): string; +} +``` + +```php +namespace Magento\AsynchronousImportSourceDataRetrievingApi\Api\Data; + +/** + * Represents retrieved source data (result of retrieving operation) + * + * @api + */ +interface SourceDataInterface extends \IteratorAggregate +{ + public const ITERATOR = 'iterator'; +} +``` + +### Extension points + +```php +namespace Magento\AsynchronousImportSourceDataRetrievingApi\Model; + +use Magento\AsynchronousImportSourceDataRetrievingApi\Api\Data\SourceInterface; +use Magento\AsynchronousImportSourceDataRetrievingApi\Api\SourceDataRetrievingException; + +/** + * Extension point for adding source data retrieving algorithms + * Represents concrete strategy + * + * @api + */ +interface RetrieveSourceDataStrategyInterface +{ + /** + * Source data retrieving strategy + * + * @param SourceInterface $source + * @return \Traversable + * @throws SourceDataRetrievingException + */ + public function execute(SourceInterface $source): \Traversable; +} +``` + +```php +namespace Magento\AsynchronousImportSourceDataRetrievingApi\Model; + +/** + * Extension point for adding source validators via DI configuration + * + * @api + */ +class SourceValidatorChain implements SourceValidatorInterface +{ + ... +} +``` + +```php +namespace Magento\AsynchronousImportSourceDataRetrievingApi\Model; + +use Magento\AsynchronousImportSourceDataRetrievingApi\Api\Data\SourceInterface; +use Magento\Framework\Validation\ValidationResult; + +/** + * Extension point for adding source validators + * + * @api + */ +interface SourceValidatorInterface +{ + /** + * Validate source + * + * @param SourceInterface $source + * @return ValidationResult + */ + public function validate(SourceInterface $source): ValidationResult; +} +``` diff --git a/design-documents/asynchronous-import/rest-api.md.md b/design-documents/asynchronous-import/rest-api.md.md new file mode 100644 index 000000000..76af74365 --- /dev/null +++ b/design-documents/asynchronous-import/rest-api.md.md @@ -0,0 +1,85 @@ +# REST API + +## 1. Source Data Upload Endpoint (optional step) + +The entry point for start uploading Source data to instance storage (Currently supported only local instance) + +**Request:** + +`POST /V1/import/source` + +``` +{ + "source": { + "sourceType": "http", + "sourceDefinition": "http://some.domain/file.csv", + "sourceDataFormat": "CSV" + } +} +``` + +**Response:** + +``` +{ + "filename": "var/import/filename.csv" +} +``` + + +## 2. Start Import Endpoint + +Single entry point for **partial** reading and import of data. + +`POST /V1/import/csv` + +``` +{ + "source": { + "sourceType": "http" + "sourceDefinition": "http://some.domain/file.csv" + "sourceDataFormat": "CSV" + } + "format': { + "escape": "|" + "enclosure" : "|" + "delimiter" => "|" + "multipleValueSeparator" : "|" + "extensionAttributes": [] + } + "convertingRules": [ + { + "identifier": "string_replace" + "applyTo": ["sku", "name"] + "parameters": { + "search": "_" + "replace": "-" + } + } + ] + "import": { + "importType": "advanced_pricing" + "importBehaviour": "add" + "extensionAttributes": [] + } +} +``` + +Where is: +- `source` is **required**. Describes how to retrieve data from Source + - `sourceType` is **required**. Example: `HTTP|HTTPS|local_file|base_64`. Could contains adiditonal information as `encoded string` if needed. + - `sourceDefinition` is **required**. Depends on `sourceType`. Example: `http://some.domain/file.csv|var/import/filename.csv` + - `sourceDataFormat` is **optional**. Needed for preliminary validation before data retrieving. +- `format` is **optional**. Describes how to parse data. +- `convertingRules[]` is **optional**. Describes how to change data before Import process. Additional data could be placed in `parameters`. +- `import` is **required**. Describes how to import data. + - `importType` is **required**. Example: `stock|advanced_pricing|product`. + - `importBehaviour` is **required**. Example: `add|delete` + +**Result:** + +``` +{ + "UUID": "uuid_string" +} +``` From f91e29cfb1969616470656db3237a129f280cfd2 Mon Sep 17 00:00:00 2001 From: Lyzun Oleksandr Date: Wed, 20 Nov 2019 15:18:23 +0100 Subject: [PATCH 2/2] Update in respect to comments --- .../asynchronous-import/AsyncImportUML.png | Bin 0 -> 74162 bytes .../asycnhronous-import.md | 15 ++++-- .../modularity/data-converting.md | 8 +++ .../modularity/import-csv.md | 47 ------------------ .../asynchronous-import/rest-api.md.md | 8 +-- 5 files changed, 22 insertions(+), 56 deletions(-) create mode 100644 design-documents/asynchronous-import/AsyncImportUML.png diff --git a/design-documents/asynchronous-import/AsyncImportUML.png b/design-documents/asynchronous-import/AsyncImportUML.png new file mode 100644 index 0000000000000000000000000000000000000000..eb2e0e9f0765ec52df0be7e6fe69230c20a1038b GIT binary patch literal 74162 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 Aq5uE@ literal 0 HcmV?d00001 diff --git a/design-documents/asynchronous-import/asycnhronous-import.md b/design-documents/asynchronous-import/asycnhronous-import.md index a24544eb2..f3eb2800e 100644 --- a/design-documents/asynchronous-import/asycnhronous-import.md +++ b/design-documents/asynchronous-import/asycnhronous-import.md @@ -31,13 +31,18 @@ Main idea of extension is to replace current Import module with new implementati - CSV reader; - Data Converting Rules; - Import Data Exchanging; -- Import configuration; +- Import Configuration; - Product import; -- Stock status import; +- Stock import; - Advanced pricing import; - Documentation; +## UML + +![AsyncImportUML@2x](AsyncImportUML.png) + ## TODO -- Design of `Import configuration`; -- Design of `Restart failed operations`; -- Design of `Get Import status`; +- Design of Import configuration; +- Design of Get Import status; +- Design of Import processing - Sync / Async +- Design of Restart failed operations diff --git a/design-documents/asynchronous-import/modularity/data-converting.md b/design-documents/asynchronous-import/modularity/data-converting.md index 33be34e93..b00880244 100644 --- a/design-documents/asynchronous-import/modularity/data-converting.md +++ b/design-documents/asynchronous-import/modularity/data-converting.md @@ -79,6 +79,14 @@ interface ConvertingRuleInterface } ``` +Converting rules are transforming incoming data in a way, that Magento API can correctly understand them. + +#### Example: +Magento default Advanced Pricing import *.csv file contains Magento Website assignment as a string value: "All Websites [ALL]". + +Magento API cannot recognise correct website base on this string representation, so this means we have to create converting rule that will define correct transformation from provided text to Magento Website ID. + + ### Extension points ```php diff --git a/design-documents/asynchronous-import/modularity/import-csv.md b/design-documents/asynchronous-import/modularity/import-csv.md index cdf59d090..4841799c7 100644 --- a/design-documents/asynchronous-import/modularity/import-csv.md +++ b/design-documents/asynchronous-import/modularity/import-csv.md @@ -101,53 +101,6 @@ interface CsvFormatInterface extends ExtensibleDataInterface } ``` -```php -/** - * Describes how to parse data - * - * @api - */ -interface CsvFormatInterface extends ExtensibleDataInterface -{ - /** - * Get CSV Escape - * - * @return string|null - */ - public function getEscape(): ?string; - - /** - * Get CSV Enclosure - * - * @return string|null - */ - public function getEnclosure(): ?string; - - /** - * Get CSV Delimiter - * - * @return string|null - */ - public function getDelimiter(): ?string; - - /** - * Get Multiple Value Separator - * - * @return string|null - */ - public function getMultipleValueSeparator(): ?string; - - /** - * Get existing extension attributes object - * - * Used fully qualified namespaces in annotations for proper work of extension interface/class code generation - * - * @return \Magento\AsynchronousImportCsvApi\Api\Data\CsvFormatExtensionInterface|null - */ - public function getExtensionAttributes(): ?CsvFormatExtensionInterface; -} -``` - ### Extension points ```php diff --git a/design-documents/asynchronous-import/rest-api.md.md b/design-documents/asynchronous-import/rest-api.md.md index 76af74365..2ee536fab 100644 --- a/design-documents/asynchronous-import/rest-api.md.md +++ b/design-documents/asynchronous-import/rest-api.md.md @@ -42,9 +42,9 @@ Single entry point for **partial** reading and import of data. } "format': { "escape": "|" - "enclosure" : "|" - "delimiter" => "|" - "multipleValueSeparator" : "|" + "enclosure": "|" + "delimiter": "|" + "multipleValueSeparator": "|" "extensionAttributes": [] } "convertingRules": [ @@ -80,6 +80,6 @@ Where is: ``` { - "UUID": "uuid_string" + "uuid": "uuid_string" } ```