From b4c15571eb550a177abbb871205c6cd65aa54191 Mon Sep 17 00:00:00 2001 From: Nathaniel Hammond Date: Fri, 16 Oct 2026 10:31:06 +0100 Subject: [PATCH 1/4] Fix images, variants and metafields syncing in reverse order --- CHANGELOG.md | 5 +++ src/Plugin.php | 2 +- src/jobs/ProcessBulkOperationData.php | 1 + src/migrations/Install.php | 1 + ...1016_090000_add_position_to_data_table.php | 33 +++++++++++++++++++ src/records/ShopifyData.php | 1 + src/services/Api.php | 2 +- .../jobs/ProcessBulkOperationDataTest.php | 24 ++++++++++++++ 8 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 src/migrations/m261016_090000_add_position_to_data_table.php diff --git a/CHANGELOG.md b/CHANGELOG.md index e9e24c5..d1fdfae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Release Notes for Shopify +## Unreleased + +- Added `craft\shopify\records\ShopifyData::$position`. +- Fixed a bug where product images, variants, and metafields were synced in reverse order. ([#161](https://github.com/craftcms/shopify/issues/161)) + ## 8.2.0 - 2026-10-08 - Improved the memory usage of bulk operation processing. diff --git a/src/Plugin.php b/src/Plugin.php index b5b79fa..ec7b20b 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -79,7 +79,7 @@ class Plugin extends BasePlugin /** * @var string */ - public string $schemaVersion = '8.1.0'; + public string $schemaVersion = '8.2.1.0'; /** * @inheritdoc diff --git a/src/jobs/ProcessBulkOperationData.php b/src/jobs/ProcessBulkOperationData.php index 3e7c4e5..fb3c55b 100644 --- a/src/jobs/ProcessBulkOperationData.php +++ b/src/jobs/ProcessBulkOperationData.php @@ -134,6 +134,7 @@ protected function processItem(mixed $item): void $record->type = $type; $record->data = $item; $record->parentId = $item['__parentId'] ?? null; + $record->position = $this->objectCount > 0 ? $this->objectCount - 1 - $this->itemOffset : null; $record->save(); // Process the data based on the type diff --git a/src/migrations/Install.php b/src/migrations/Install.php index 4f178df..4b89472 100644 --- a/src/migrations/Install.php +++ b/src/migrations/Install.php @@ -61,6 +61,7 @@ public function createTables(): void 'type' => $this->string(), 'data' => $this->json(), 'parentId' => $this->string(), + 'position' => $this->integer(), 'dateCreated' => $this->dateTime()->notNull(), 'dateUpdated' => $this->dateTime()->notNull(), 'uid' => $this->uid(), diff --git a/src/migrations/m261016_090000_add_position_to_data_table.php b/src/migrations/m261016_090000_add_position_to_data_table.php new file mode 100644 index 0000000..e6cfa80 --- /dev/null +++ b/src/migrations/m261016_090000_add_position_to_data_table.php @@ -0,0 +1,33 @@ +db->columnExists(Table::DATA, 'position')) { + $this->addColumn(Table::DATA, 'position', $this->integer()->after('parentId')); + } + + return true; + } + + /** + * @inheritdoc + */ + public function safeDown(): bool + { + echo "m261016_090000_add_position_to_data_table cannot be reverted.\n"; + return false; + } +} diff --git a/src/records/ShopifyData.php b/src/records/ShopifyData.php index 75da938..73a2549 100644 --- a/src/records/ShopifyData.php +++ b/src/records/ShopifyData.php @@ -22,6 +22,7 @@ * @property string $type * @property string|array $data * @property string $parentId + * @property int|null $position The line index of the item in its bulk operation file (since 8.2.1) * @property string $dateCreated * @property string $dateUpdated * @property string $uid diff --git a/src/services/Api.php b/src/services/Api.php index fa68ed9..9014f05 100644 --- a/src/services/Api.php +++ b/src/services/Api.php @@ -536,7 +536,7 @@ public function getShopifyDataByType(string $type, array|string|null|false $pare $data = ShopifyData::find() ->where($criteria) - ->orderBy(['id' => SORT_ASC]) + ->orderBy(['position' => SORT_ASC, 'id' => SORT_ASC]) ->collect(); // The caller can request the raw database rows, instead of just the `data` JSON column: diff --git a/tests/unit/jobs/ProcessBulkOperationDataTest.php b/tests/unit/jobs/ProcessBulkOperationDataTest.php index 71f3d15..9ffd8f6 100644 --- a/tests/unit/jobs/ProcessBulkOperationDataTest.php +++ b/tests/unit/jobs/ProcessBulkOperationDataTest.php @@ -292,6 +292,30 @@ public function testExecuteProcessesEveryLineOnceAcrossBatches(): void self::assertEquals(58, ShopifyData::find()->where(['parentId' => $productGid])->count()); } + public function testExecuteStoresChildrenInTheOrderTheyAppearInTheFile(): void + { + $productGid = 'gid://shopify/Product/order-test-001'; + $imageGids = []; + $lines = [json_encode(['id' => $productGid])]; + foreach (range(1, 5) as $i) { + $imageGids[] = "gid://shopify/MediaImage/order-test-$i"; + $lines[] = json_encode(['id' => end($imageGids), '__parentId' => $productGid]); + } + + $filePath = Assets::tempFilePath('jsonl'); + file_put_contents($filePath, implode("\n", $lines) . "\n"); + + Plugin::getInstance()->set('products', $this->makeEmpty(Products::class, [ + 'createOrUpdateProduct' => true, + ])); + + $this->_runJob($this->_makeExecutableJob($filePath, count($lines), 'all', batchSize: 2)); + + $images = Plugin::getInstance()->getApi()->getShopifyDataByType('MediaImage', $productGid); + + self::assertSame($imageGids, $images->pluck('id')->all()); + } + /** * A TTR of zero makes every batch stop after its first item, leaving the rest of its slice unprocessed. */ From 8778d7701ed0ed2e8a6de1d66efc72105ca7535c Mon Sep 17 00:00:00 2001 From: Nathaniel Hammond Date: Fri, 16 Oct 2026 10:31:06 +0100 Subject: [PATCH 2/4] Fix images, variants and metafields syncing in reverse order --- CHANGELOG.md | 5 +++ src/Plugin.php | 2 +- src/jobs/ProcessBulkOperationData.php | 1 + src/migrations/Install.php | 1 + ...1016_090000_add_position_to_data_table.php | 33 +++++++++++++++++++ src/records/ShopifyData.php | 1 + src/services/Api.php | 2 +- .../jobs/ProcessBulkOperationDataTest.php | 24 ++++++++++++++ 8 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 src/migrations/m261016_090000_add_position_to_data_table.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 5510990..35e371f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Release Notes for Shopify +## Unreleased + +- Added `craft\shopify\records\ShopifyData::$position`. +- Fixed a bug where product images, variants, and metafields were synced in reverse order. ([#161](https://github.com/craftcms/shopify/issues/161)) + ## 7.3.0 - 2026-10-08 - Improved the memory usage of bulk operation processing. diff --git a/src/Plugin.php b/src/Plugin.php index 1ba9d7a..fca8003 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -79,7 +79,7 @@ class Plugin extends BasePlugin /** * @var string */ - public string $schemaVersion = '7.2.0.0'; + public string $schemaVersion = '7.3.1.0'; /** * @inheritdoc diff --git a/src/jobs/ProcessBulkOperationData.php b/src/jobs/ProcessBulkOperationData.php index b02a205..ae83dc9 100644 --- a/src/jobs/ProcessBulkOperationData.php +++ b/src/jobs/ProcessBulkOperationData.php @@ -134,6 +134,7 @@ protected function processItem(mixed $item): void $record->type = $type; $record->data = $item; $record->parentId = $item['__parentId'] ?? null; + $record->position = $this->objectCount > 0 ? $this->objectCount - 1 - $this->itemOffset : null; $record->save(); // Process the data based on the type diff --git a/src/migrations/Install.php b/src/migrations/Install.php index 80e165c..f679397 100644 --- a/src/migrations/Install.php +++ b/src/migrations/Install.php @@ -61,6 +61,7 @@ public function createTables(): void 'type' => $this->string(), 'data' => $this->json(), 'parentId' => $this->string(), + 'position' => $this->integer(), 'dateCreated' => $this->dateTime()->notNull(), 'dateUpdated' => $this->dateTime()->notNull(), 'uid' => $this->uid(), diff --git a/src/migrations/m261016_090000_add_position_to_data_table.php b/src/migrations/m261016_090000_add_position_to_data_table.php new file mode 100644 index 0000000..e6cfa80 --- /dev/null +++ b/src/migrations/m261016_090000_add_position_to_data_table.php @@ -0,0 +1,33 @@ +db->columnExists(Table::DATA, 'position')) { + $this->addColumn(Table::DATA, 'position', $this->integer()->after('parentId')); + } + + return true; + } + + /** + * @inheritdoc + */ + public function safeDown(): bool + { + echo "m261016_090000_add_position_to_data_table cannot be reverted.\n"; + return false; + } +} diff --git a/src/records/ShopifyData.php b/src/records/ShopifyData.php index 59625b0..bfeaa3b 100644 --- a/src/records/ShopifyData.php +++ b/src/records/ShopifyData.php @@ -21,6 +21,7 @@ * @property string $type * @property string|array $data * @property string $parentId + * @property int|null $position The line index of the item in its bulk operation file (since 8.2.1) * @property string $dateCreated * @property string $dateUpdated * @property string $uid diff --git a/src/services/Api.php b/src/services/Api.php index 89f4b41..165074a 100644 --- a/src/services/Api.php +++ b/src/services/Api.php @@ -514,7 +514,7 @@ public function getShopifyDataByType(string $type, array|string|null|false $pare $data = ShopifyData::find() ->where($criteria) - ->orderBy(['id' => SORT_ASC]) + ->orderBy(['position' => SORT_ASC, 'id' => SORT_ASC]) ->collect(); // The caller can request the raw database rows, instead of just the `data` JSON column: diff --git a/tests/unit/jobs/ProcessBulkOperationDataTest.php b/tests/unit/jobs/ProcessBulkOperationDataTest.php index e22faf7..76750d7 100644 --- a/tests/unit/jobs/ProcessBulkOperationDataTest.php +++ b/tests/unit/jobs/ProcessBulkOperationDataTest.php @@ -292,6 +292,30 @@ public function testExecuteProcessesEveryLineOnceAcrossBatches(): void self::assertEquals(58, ShopifyData::find()->where(['parentId' => $productGid])->count()); } + public function testExecuteStoresChildrenInTheOrderTheyAppearInTheFile(): void + { + $productGid = 'gid://shopify/Product/order-test-001'; + $imageGids = []; + $lines = [json_encode(['id' => $productGid])]; + foreach (range(1, 5) as $i) { + $imageGids[] = "gid://shopify/MediaImage/order-test-$i"; + $lines[] = json_encode(['id' => end($imageGids), '__parentId' => $productGid]); + } + + $filePath = Assets::tempFilePath('jsonl'); + file_put_contents($filePath, implode("\n", $lines) . "\n"); + + Plugin::getInstance()->set('products', $this->makeEmpty(Products::class, [ + 'createOrUpdateProduct' => true, + ])); + + $this->_runJob($this->_makeExecutableJob($filePath, count($lines), 'all', batchSize: 2)); + + $images = Plugin::getInstance()->getApi()->getShopifyDataByType('MediaImage', $productGid); + + self::assertSame($imageGids, $images->pluck('id')->all()); + } + /** * A TTR of zero makes every batch stop after its first item, leaving the rest of its slice unprocessed. */ From 64f8e86e21b3ac91c86ea0fe29b09379f471a912 Mon Sep 17 00:00:00 2001 From: Nathaniel Hammond Date: Mon, 19 Oct 2026 08:18:31 +0100 Subject: [PATCH 3/4] Update position property version in ShopifyData.php Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/records/ShopifyData.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/records/ShopifyData.php b/src/records/ShopifyData.php index bfeaa3b..829eccd 100644 --- a/src/records/ShopifyData.php +++ b/src/records/ShopifyData.php @@ -21,7 +21,7 @@ * @property string $type * @property string|array $data * @property string $parentId - * @property int|null $position The line index of the item in its bulk operation file (since 8.2.1) + * @property int|null $position The line index of the item in its bulk operation file (since 7.3.1) * @property string $dateCreated * @property string $dateUpdated * @property string $uid From 055c417a9f23129c9d3526d514edc514bfdef2d5 Mon Sep 17 00:00:00 2001 From: Nathaniel Hammond Date: Mon, 19 Oct 2026 16:54:15 +0100 Subject: [PATCH 4/4] Finish 7.3.1 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 35e371f..415b4d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Release Notes for Shopify -## Unreleased +## 7.3.1 - 2026-10-19 - Added `craft\shopify\records\ShopifyData::$position`. - Fixed a bug where product images, variants, and metafields were synced in reverse order. ([#161](https://github.com/craftcms/shopify/issues/161))