@open-mercato/sync-akeneo 0.6.8-develop.7026.1.23445b3f3e → 0.6.8-develop.7029.1.a1bb3363af

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -69,6 +69,7 @@ const akeneoDataSyncAdapter = {
69
69
  const items = [];
70
70
  const locale = mapping.settings?.categories?.locale ?? "en_US";
71
71
  for (const category of page.items) {
72
+ if (input.signal?.aborted) return;
72
73
  seenCategoryIds.add(category.code);
73
74
  const result = await importer.upsertCategory(category, locale);
74
75
  items.push({
@@ -126,6 +127,7 @@ const akeneoDataSyncAdapter = {
126
127
  });
127
128
  }
128
129
  for (const family of page.items) {
130
+ if (input.signal?.aborted) return;
129
131
  if (familyFilter.length > 0 && !familyFilter.includes(family.code)) continue;
130
132
  seenFamilyIds.add(family.code);
131
133
  const result = await importer.upsertAttributeFamily(
@@ -183,6 +185,7 @@ const akeneoDataSyncAdapter = {
183
185
  });
184
186
  const items = [];
185
187
  for (const product of page.items) {
188
+ if (input.signal?.aborted) return;
186
189
  try {
187
190
  const imported = await importer.upsertProduct(product, mapping);
188
191
  for (const item of imported) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/sync_akeneo/lib/adapter.ts"],
4
- "sourcesContent": ["import { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { DataSyncAdapter, DataMapping, ImportBatch, ImportItem, ValidationResult } from '@open-mercato/core/modules/data_sync/lib/adapter'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { buildListResumeCursor, buildProductResumeCursor, parseCursor } from './cursor'\nimport { createAkeneoClient } from './client'\nimport { createAkeneoImporter } from './catalog-importer'\nimport { loadAkeneoMapping } from './mapping'\nimport { buildDefaultReconciliationSettings, normalizeAkeneoMapping, type AkeneoEntityType, type AkeneoDataMapping } from './shared'\n\nfunction assertEntityType(entityType: string): AkeneoEntityType {\n if (entityType === 'categories' || entityType === 'attributes' || entityType === 'products') {\n return entityType\n }\n throw new Error(`Unsupported Akeneo entity type: ${entityType}`)\n}\n\nasync function resolveMapping(entityType: AkeneoEntityType, scope: { organizationId: string; tenantId: string }): Promise<AkeneoDataMapping> {\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n return loadAkeneoMapping(em, entityType, scope)\n}\n\nexport const akeneoDataSyncAdapter: DataSyncAdapter = {\n providerKey: 'akeneo',\n direction: 'import',\n supportedEntities: ['categories', 'attributes', 'products'],\n\n async getMapping(input): Promise<DataMapping> {\n const entityType = assertEntityType(input.entityType)\n return resolveMapping(entityType, input.scope)\n },\n\n async validateConnection(input): Promise<ValidationResult> {\n try {\n const entityType = assertEntityType(input.entityType)\n const client = createAkeneoClient(input.credentials)\n if (entityType === 'products') {\n await client.listProducts({ batchSize: 1 })\n } else if (entityType === 'categories') {\n await client.listCategories(null, 1)\n } else {\n await client.listFamilies(null, 1)\n }\n\n const discovery = await client.collectDiscoveryData().catch(() => null)\n return {\n ok: true,\n message: 'Akeneo connection validated successfully',\n details: discovery\n ? {\n locales: discovery.locales.length,\n channels: discovery.channels.length,\n attributes: discovery.attributes.length,\n families: discovery.families.length,\n version: discovery.version,\n }\n : undefined,\n }\n } catch (error) {\n return {\n ok: false,\n message: error instanceof Error ? error.message : 'Akeneo validation failed',\n }\n }\n },\n\n async *streamImport(input): AsyncIterable<ImportBatch> {\n const entityType = assertEntityType(input.entityType)\n const mapping = normalizeAkeneoMapping(entityType, input.mapping as unknown as Record<string, unknown>)\n const client = createAkeneoClient(input.credentials)\n const importer = await createAkeneoImporter(client, input.scope)\n\n if (entityType === 'categories') {\n let batchIndex = 0\n let nextUrl = parseCursor(input.cursor)?.nextUrl ?? null\n const safeFullSync = !input.cursor\n const seenCategoryIds = new Set<string>()\n const reconciliation = buildDefaultReconciliationSettings()\n do {\n const page = await client.listCategories(nextUrl, input.batchSize)\n const items: ImportItem[] = []\n const locale = mapping.settings?.categories?.locale ?? 'en_US'\n for (const category of page.items) {\n seenCategoryIds.add(category.code)\n const result = await importer.upsertCategory(category, locale)\n items.push({\n externalId: category.code,\n data: {\n localId: result.localId,\n code: category.code,\n parent: category.parent ?? null,\n },\n action: result.action,\n })\n }\n nextUrl = page.nextUrl\n yield {\n items,\n cursor: buildListResumeCursor(nextUrl),\n hasMore: Boolean(nextUrl),\n totalEstimate: page.totalEstimate ?? undefined,\n processedCount: page.items.length,\n batchIndex,\n }\n batchIndex += 1\n } while (nextUrl)\n if (safeFullSync) {\n const locale = mapping.settings?.categories?.locale ?? 'en_US'\n await importer.reconcileCategories(seenCategoryIds, locale, reconciliation)\n }\n return\n }\n\n if (entityType === 'attributes') {\n const productMapping = await resolveMapping('products', input.scope)\n let batchIndex = 0\n let nextUrl = parseCursor(input.cursor)?.nextUrl ?? null\n const safeFullSync = !input.cursor\n const seenFamilyIds = new Set<string>()\n const reconciliation = buildDefaultReconciliationSettings()\n const locale = productMapping.settings?.products?.locale\n ?? mapping.settings?.categories?.locale\n ?? 'en_US'\n const customFieldItems = await importer.syncMappedCustomFields(productMapping, locale)\n const currentCustomFieldKeys = new Set(\n customFieldItems\n .map((item) => {\n const key = typeof item.data.key === 'string' ? item.data.key : null\n const target = item.data.target === 'variant' ? 'catalog:catalog_product_variant' : item.data.target === 'product' ? 'catalog:catalog_product' : null\n return key && target ? `${target}:${key}` : null\n })\n .filter((value): value is string => Boolean(value)),\n )\n do {\n const page = await client.listFamilies(nextUrl, input.batchSize)\n const items: ImportItem[] = []\n const includeTextAttributes = mapping.settings?.attributes?.includeTextAttributes ?? true\n const includeNumericAttributes = mapping.settings?.attributes?.includeNumericAttributes ?? true\n const familyFilter = mapping.settings?.attributes?.familyCodeFilter ?? []\n if (batchIndex === 0) {\n customFieldItems.forEach((item) => {\n items.push(item)\n })\n }\n for (const family of page.items) {\n if (familyFilter.length > 0 && !familyFilter.includes(family.code)) continue\n seenFamilyIds.add(family.code)\n const result = await importer.upsertAttributeFamily(\n family,\n locale,\n includeTextAttributes,\n includeNumericAttributes,\n )\n if (!result) continue\n items.push({\n externalId: family.code,\n data: {\n localId: result.localId,\n familyCode: family.code,\n },\n action: result.action,\n })\n }\n nextUrl = page.nextUrl\n yield {\n items,\n cursor: buildListResumeCursor(nextUrl),\n hasMore: Boolean(nextUrl),\n totalEstimate: page.totalEstimate ?? undefined,\n processedCount: page.items.length,\n batchIndex,\n }\n batchIndex += 1\n } while (nextUrl)\n if (safeFullSync) {\n await importer.reconcileMappedCustomFieldFieldsets(productMapping)\n await importer.reconcileAttributes({\n seenFamilyExternalIds: seenFamilyIds,\n currentCustomFieldKeys,\n reconciliation,\n })\n }\n return\n }\n\n let batchIndex = 0\n const currentCursor = parseCursor(input.cursor)\n let nextUrl = currentCursor?.nextUrl ?? null\n let updatedAfter = currentCursor?.updatedAfter ?? null\n let maxUpdatedAt = currentCursor?.maxUpdatedAt ?? updatedAfter\n const safeFullSync = !input.cursor\n const seenProductExternalIds = new Set<string>()\n const seenVariantExternalIds = new Set<string>()\n const reconciliation = mapping.settings?.products?.reconciliation ?? buildDefaultReconciliationSettings()\n\n do {\n const productPageSize = Math.min(Math.max(input.batchSize, 1), 10)\n const page = await client.listProducts({\n nextUrl,\n batchSize: productPageSize,\n updatedAfter,\n })\n\n const items: ImportItem[] = []\n for (const product of page.items) {\n try {\n const imported = await importer.upsertProduct(product, mapping)\n for (const item of imported) {\n if (item.data.localProductId) seenProductExternalIds.add(item.externalId)\n if (item.data.localVariantId) seenVariantExternalIds.add(item.externalId)\n items.push(item)\n }\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Akeneo product import failed'\n items.push({\n externalId: product.parent ? `${product.parent}:${product.uuid}` : product.uuid,\n action: 'failed',\n data: {\n errorMessage: message,\n sourceProductUuid: product.uuid,\n sourceIdentifier: product.identifier ?? null,\n sourceParentCode: product.parent ?? null,\n family: product.family ?? null,\n },\n })\n }\n if (typeof product.updated === 'string' && (!maxUpdatedAt || product.updated > maxUpdatedAt)) {\n maxUpdatedAt = product.updated\n }\n }\n\n nextUrl = page.nextUrl\n const nextCursor = nextUrl\n ? buildProductResumeCursor({\n updatedAfter,\n nextUrl,\n maxUpdatedAt,\n })\n : buildProductResumeCursor({\n updatedAfter: maxUpdatedAt ?? updatedAfter,\n nextUrl: null,\n maxUpdatedAt: null,\n })\n\n yield {\n items,\n cursor: nextCursor,\n hasMore: Boolean(nextUrl),\n totalEstimate: page.totalEstimate ?? undefined,\n processedCount: page.items.length,\n refreshCoverageEntityTypes: [\n 'catalog:catalog_product',\n 'catalog:catalog_product_variant',\n ],\n batchIndex,\n }\n batchIndex += 1\n\n if (!nextUrl && maxUpdatedAt) {\n updatedAfter = maxUpdatedAt\n }\n } while (nextUrl)\n\n if (safeFullSync) {\n yield {\n items: [],\n cursor: buildProductResumeCursor({\n updatedAfter: maxUpdatedAt ?? updatedAfter,\n nextUrl: null,\n maxUpdatedAt: null,\n }),\n hasMore: false,\n totalEstimate: undefined,\n processedCount: 0,\n message: 'Reconciling imported Akeneo products after the final batch',\n batchIndex,\n }\n await importer.reconcileMappedCustomFieldFieldsets(mapping)\n await importer.reconcileProducts(seenProductExternalIds, seenVariantExternalIds, reconciliation)\n }\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { DataSyncAdapter, DataMapping, ImportBatch, ImportItem, ValidationResult } from '@open-mercato/core/modules/data_sync/lib/adapter'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { buildListResumeCursor, buildProductResumeCursor, parseCursor } from './cursor'\nimport { createAkeneoClient } from './client'\nimport { createAkeneoImporter } from './catalog-importer'\nimport { loadAkeneoMapping } from './mapping'\nimport { buildDefaultReconciliationSettings, normalizeAkeneoMapping, type AkeneoEntityType, type AkeneoDataMapping } from './shared'\n\nfunction assertEntityType(entityType: string): AkeneoEntityType {\n if (entityType === 'categories' || entityType === 'attributes' || entityType === 'products') {\n return entityType\n }\n throw new Error(`Unsupported Akeneo entity type: ${entityType}`)\n}\n\nasync function resolveMapping(entityType: AkeneoEntityType, scope: { organizationId: string; tenantId: string }): Promise<AkeneoDataMapping> {\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n return loadAkeneoMapping(em, entityType, scope)\n}\n\nexport const akeneoDataSyncAdapter: DataSyncAdapter = {\n providerKey: 'akeneo',\n direction: 'import',\n supportedEntities: ['categories', 'attributes', 'products'],\n\n async getMapping(input): Promise<DataMapping> {\n const entityType = assertEntityType(input.entityType)\n return resolveMapping(entityType, input.scope)\n },\n\n async validateConnection(input): Promise<ValidationResult> {\n try {\n const entityType = assertEntityType(input.entityType)\n const client = createAkeneoClient(input.credentials)\n if (entityType === 'products') {\n await client.listProducts({ batchSize: 1 })\n } else if (entityType === 'categories') {\n await client.listCategories(null, 1)\n } else {\n await client.listFamilies(null, 1)\n }\n\n const discovery = await client.collectDiscoveryData().catch(() => null)\n return {\n ok: true,\n message: 'Akeneo connection validated successfully',\n details: discovery\n ? {\n locales: discovery.locales.length,\n channels: discovery.channels.length,\n attributes: discovery.attributes.length,\n families: discovery.families.length,\n version: discovery.version,\n }\n : undefined,\n }\n } catch (error) {\n return {\n ok: false,\n message: error instanceof Error ? error.message : 'Akeneo validation failed',\n }\n }\n },\n\n async *streamImport(input): AsyncIterable<ImportBatch> {\n const entityType = assertEntityType(input.entityType)\n const mapping = normalizeAkeneoMapping(entityType, input.mapping as unknown as Record<string, unknown>)\n const client = createAkeneoClient(input.credentials)\n const importer = await createAkeneoImporter(client, input.scope)\n\n if (entityType === 'categories') {\n let batchIndex = 0\n let nextUrl = parseCursor(input.cursor)?.nextUrl ?? null\n const safeFullSync = !input.cursor\n const seenCategoryIds = new Set<string>()\n const reconciliation = buildDefaultReconciliationSettings()\n do {\n const page = await client.listCategories(nextUrl, input.batchSize)\n const items: ImportItem[] = []\n const locale = mapping.settings?.categories?.locale ?? 'en_US'\n for (const category of page.items) {\n // Above the yield, so an abandoned page is never yielded and its cursor never committed.\n // Returning here also skips the `safeFullSync` reconcile below, which is required: a\n // partial walk has not seen every category, so reconciling would delete live records.\n if (input.signal?.aborted) return\n seenCategoryIds.add(category.code)\n const result = await importer.upsertCategory(category, locale)\n items.push({\n externalId: category.code,\n data: {\n localId: result.localId,\n code: category.code,\n parent: category.parent ?? null,\n },\n action: result.action,\n })\n }\n nextUrl = page.nextUrl\n yield {\n items,\n cursor: buildListResumeCursor(nextUrl),\n hasMore: Boolean(nextUrl),\n totalEstimate: page.totalEstimate ?? undefined,\n processedCount: page.items.length,\n batchIndex,\n }\n batchIndex += 1\n } while (nextUrl)\n if (safeFullSync) {\n const locale = mapping.settings?.categories?.locale ?? 'en_US'\n await importer.reconcileCategories(seenCategoryIds, locale, reconciliation)\n }\n return\n }\n\n if (entityType === 'attributes') {\n const productMapping = await resolveMapping('products', input.scope)\n let batchIndex = 0\n let nextUrl = parseCursor(input.cursor)?.nextUrl ?? null\n const safeFullSync = !input.cursor\n const seenFamilyIds = new Set<string>()\n const reconciliation = buildDefaultReconciliationSettings()\n const locale = productMapping.settings?.products?.locale\n ?? mapping.settings?.categories?.locale\n ?? 'en_US'\n const customFieldItems = await importer.syncMappedCustomFields(productMapping, locale)\n const currentCustomFieldKeys = new Set(\n customFieldItems\n .map((item) => {\n const key = typeof item.data.key === 'string' ? item.data.key : null\n const target = item.data.target === 'variant' ? 'catalog:catalog_product_variant' : item.data.target === 'product' ? 'catalog:catalog_product' : null\n return key && target ? `${target}:${key}` : null\n })\n .filter((value): value is string => Boolean(value)),\n )\n do {\n const page = await client.listFamilies(nextUrl, input.batchSize)\n const items: ImportItem[] = []\n const includeTextAttributes = mapping.settings?.attributes?.includeTextAttributes ?? true\n const includeNumericAttributes = mapping.settings?.attributes?.includeNumericAttributes ?? true\n const familyFilter = mapping.settings?.attributes?.familyCodeFilter ?? []\n if (batchIndex === 0) {\n customFieldItems.forEach((item) => {\n items.push(item)\n })\n }\n for (const family of page.items) {\n // Above the yield \u2014 see the categories walk. Returning also skips the reconcile, which a\n // partial walk must not run.\n if (input.signal?.aborted) return\n if (familyFilter.length > 0 && !familyFilter.includes(family.code)) continue\n seenFamilyIds.add(family.code)\n const result = await importer.upsertAttributeFamily(\n family,\n locale,\n includeTextAttributes,\n includeNumericAttributes,\n )\n if (!result) continue\n items.push({\n externalId: family.code,\n data: {\n localId: result.localId,\n familyCode: family.code,\n },\n action: result.action,\n })\n }\n nextUrl = page.nextUrl\n yield {\n items,\n cursor: buildListResumeCursor(nextUrl),\n hasMore: Boolean(nextUrl),\n totalEstimate: page.totalEstimate ?? undefined,\n processedCount: page.items.length,\n batchIndex,\n }\n batchIndex += 1\n } while (nextUrl)\n if (safeFullSync) {\n await importer.reconcileMappedCustomFieldFieldsets(productMapping)\n await importer.reconcileAttributes({\n seenFamilyExternalIds: seenFamilyIds,\n currentCustomFieldKeys,\n reconciliation,\n })\n }\n return\n }\n\n let batchIndex = 0\n const currentCursor = parseCursor(input.cursor)\n let nextUrl = currentCursor?.nextUrl ?? null\n let updatedAfter = currentCursor?.updatedAfter ?? null\n let maxUpdatedAt = currentCursor?.maxUpdatedAt ?? updatedAfter\n const safeFullSync = !input.cursor\n const seenProductExternalIds = new Set<string>()\n const seenVariantExternalIds = new Set<string>()\n const reconciliation = mapping.settings?.products?.reconciliation ?? buildDefaultReconciliationSettings()\n\n do {\n const productPageSize = Math.min(Math.max(input.batchSize, 1), 10)\n const page = await client.listProducts({\n nextUrl,\n batchSize: productPageSize,\n updatedAfter,\n })\n\n const items: ImportItem[] = []\n for (const product of page.items) {\n // Above the yield \u2014 see the categories walk. Returning also skips the reconcile, which a\n // partial walk must not run.\n if (input.signal?.aborted) return\n try {\n const imported = await importer.upsertProduct(product, mapping)\n for (const item of imported) {\n if (item.data.localProductId) seenProductExternalIds.add(item.externalId)\n if (item.data.localVariantId) seenVariantExternalIds.add(item.externalId)\n items.push(item)\n }\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Akeneo product import failed'\n items.push({\n externalId: product.parent ? `${product.parent}:${product.uuid}` : product.uuid,\n action: 'failed',\n data: {\n errorMessage: message,\n sourceProductUuid: product.uuid,\n sourceIdentifier: product.identifier ?? null,\n sourceParentCode: product.parent ?? null,\n family: product.family ?? null,\n },\n })\n }\n if (typeof product.updated === 'string' && (!maxUpdatedAt || product.updated > maxUpdatedAt)) {\n maxUpdatedAt = product.updated\n }\n }\n\n nextUrl = page.nextUrl\n const nextCursor = nextUrl\n ? buildProductResumeCursor({\n updatedAfter,\n nextUrl,\n maxUpdatedAt,\n })\n : buildProductResumeCursor({\n updatedAfter: maxUpdatedAt ?? updatedAfter,\n nextUrl: null,\n maxUpdatedAt: null,\n })\n\n yield {\n items,\n cursor: nextCursor,\n hasMore: Boolean(nextUrl),\n totalEstimate: page.totalEstimate ?? undefined,\n processedCount: page.items.length,\n refreshCoverageEntityTypes: [\n 'catalog:catalog_product',\n 'catalog:catalog_product_variant',\n ],\n batchIndex,\n }\n batchIndex += 1\n\n if (!nextUrl && maxUpdatedAt) {\n updatedAfter = maxUpdatedAt\n }\n } while (nextUrl)\n\n if (safeFullSync) {\n yield {\n items: [],\n cursor: buildProductResumeCursor({\n updatedAfter: maxUpdatedAt ?? updatedAfter,\n nextUrl: null,\n maxUpdatedAt: null,\n }),\n hasMore: false,\n totalEstimate: undefined,\n processedCount: 0,\n message: 'Reconciling imported Akeneo products after the final batch',\n batchIndex,\n }\n await importer.reconcileMappedCustomFieldFieldsets(mapping)\n await importer.reconcileProducts(seenProductExternalIds, seenVariantExternalIds, reconciliation)\n }\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["batchIndex", "nextUrl", "safeFullSync", "reconciliation"]
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/sync-akeneo",
3
- "version": "0.6.8-develop.7026.1.23445b3f3e",
3
+ "version": "0.6.8-develop.7029.1.a1bb3363af",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -53,18 +53,18 @@
53
53
  }
54
54
  },
55
55
  "dependencies": {
56
- "@open-mercato/core": "0.6.8-develop.7026.1.23445b3f3e",
57
- "@open-mercato/ui": "0.6.8-develop.7026.1.23445b3f3e",
56
+ "@open-mercato/core": "0.6.8-develop.7029.1.a1bb3363af",
57
+ "@open-mercato/ui": "0.6.8-develop.7029.1.a1bb3363af",
58
58
  "node-html-markdown": "^2.0.0"
59
59
  },
60
60
  "peerDependencies": {
61
61
  "@mikro-orm/postgresql": "^7.0.14",
62
- "@open-mercato/shared": "0.6.8-develop.7026.1.23445b3f3e",
62
+ "@open-mercato/shared": "0.6.8-develop.7029.1.a1bb3363af",
63
63
  "react": "^19.0.0",
64
64
  "react-dom": "^19.0.0"
65
65
  },
66
66
  "devDependencies": {
67
- "@open-mercato/shared": "0.6.8-develop.7026.1.23445b3f3e",
67
+ "@open-mercato/shared": "0.6.8-develop.7029.1.a1bb3363af",
68
68
  "@types/jest": "^30.0.0",
69
69
  "@types/react": "^19.2.17",
70
70
  "@types/react-dom": "^19.2.3",
@@ -10,6 +10,16 @@ jest.mock('../lib/catalog-importer', () => ({
10
10
  createAkeneoImporter: jest.fn(),
11
11
  }))
12
12
 
13
+ // Only the `attributes` walk resolves a second mapping through DI; the categories and products
14
+ // walks use `input.mapping`, so these two mocks are inert for every other case in this file.
15
+ jest.mock('@open-mercato/shared/lib/di/container', () => ({
16
+ createRequestContainer: jest.fn(async () => ({ resolve: () => ({}) })),
17
+ }))
18
+
19
+ jest.mock('../lib/mapping', () => ({
20
+ loadAkeneoMapping: jest.fn(async () => ({ entityType: 'products', fields: [], matchStrategy: 'externalId', settings: {} })),
21
+ }))
22
+
13
23
  import type { ImportBatch } from '@open-mercato/core/modules/data_sync/lib/adapter'
14
24
  import { akeneoDataSyncAdapter } from '../lib/adapter'
15
25
  import { createAkeneoClient } from '../lib/client'
@@ -104,4 +114,127 @@ describe('akeneo adapter product import', () => {
104
114
  }))
105
115
  expect(reconcileProducts).toHaveBeenCalled()
106
116
  })
117
+
118
+ it('stops the product walk mid-page when the signal aborts, without yielding or reconciling', async () => {
119
+ const controller = new AbortController()
120
+ const listProducts = jest.fn(async () => ({
121
+ items: [
122
+ { uuid: 'product-1', identifier: 'sku-1', family: 'shirts', updated: '2026-03-11 12:00:00' },
123
+ { uuid: 'product-2', identifier: 'sku-2', family: 'shirts', updated: '2026-03-11 12:01:00' },
124
+ ],
125
+ nextUrl: null,
126
+ totalEstimate: 2,
127
+ }))
128
+ // Cancel lands while the first product is being upserted.
129
+ const upsertProduct = jest.fn(async () => {
130
+ controller.abort()
131
+ return [{ externalId: 'product-1', action: 'create', data: { localProductId: 'local-product-1' } }]
132
+ })
133
+ const reconcileProducts = jest.fn(async () => undefined)
134
+
135
+ ;(createAkeneoClient as jest.Mock).mockReturnValue({ listProducts })
136
+ ;(createAkeneoImporter as jest.Mock).mockResolvedValue({
137
+ upsertProduct,
138
+ reconcileMappedCustomFieldFieldsets: jest.fn(async () => undefined),
139
+ reconcileProducts,
140
+ })
141
+
142
+ const batches: ImportBatch[] = []
143
+ for await (const batch of akeneoDataSyncAdapter.streamImport!({
144
+ entityType: 'products',
145
+ batchSize: 100,
146
+ credentials: {},
147
+ mapping: {
148
+ entityType: 'products',
149
+ fields: [],
150
+ matchStrategy: 'externalId',
151
+ },
152
+ scope: {
153
+ organizationId: 'org-1',
154
+ tenantId: 'tenant-1',
155
+ },
156
+ signal: controller.signal,
157
+ })) {
158
+ batches.push(batch)
159
+ }
160
+
161
+ // The return sits above the yield, so the abandoned page is never emitted and the engine never
162
+ // commits a cursor past products it did not apply.
163
+ expect(batches).toEqual([])
164
+ expect(upsertProduct).toHaveBeenCalledTimes(1)
165
+ // A partial walk has not seen every product, so reconciling would delete live records.
166
+ expect(reconcileProducts).not.toHaveBeenCalled()
167
+ })
168
+
169
+ it('stops the category walk mid-page when the signal aborts, without yielding or reconciling', async () => {
170
+ const controller = new AbortController()
171
+ const listCategories = jest.fn(async () => ({
172
+ items: [{ code: 'category-1', parent: null }, { code: 'category-2', parent: null }],
173
+ nextUrl: null,
174
+ totalEstimate: 2,
175
+ }))
176
+ const upsertCategory = jest.fn(async () => {
177
+ controller.abort()
178
+ return { localId: 'local-category-1', action: 'create' as const }
179
+ })
180
+ const reconcileCategories = jest.fn(async () => undefined)
181
+
182
+ ;(createAkeneoClient as jest.Mock).mockReturnValue({ listCategories })
183
+ ;(createAkeneoImporter as jest.Mock).mockResolvedValue({ upsertCategory, reconcileCategories })
184
+
185
+ const batches: ImportBatch[] = []
186
+ for await (const batch of akeneoDataSyncAdapter.streamImport!({
187
+ entityType: 'categories',
188
+ batchSize: 100,
189
+ credentials: {},
190
+ mapping: { entityType: 'categories', fields: [], matchStrategy: 'externalId' },
191
+ scope: { organizationId: 'org-1', tenantId: 'tenant-1' },
192
+ signal: controller.signal,
193
+ })) {
194
+ batches.push(batch)
195
+ }
196
+
197
+ expect(batches).toEqual([])
198
+ expect(upsertCategory).toHaveBeenCalledTimes(1)
199
+ // reconcileCategories deletes categories the walk did not see — fatal after a partial walk.
200
+ expect(reconcileCategories).not.toHaveBeenCalled()
201
+ })
202
+
203
+ it('stops the attribute-family walk mid-page when the signal aborts, without yielding or reconciling', async () => {
204
+ const controller = new AbortController()
205
+ const listFamilies = jest.fn(async () => ({
206
+ items: [{ code: 'family-1' }, { code: 'family-2' }],
207
+ nextUrl: null,
208
+ totalEstimate: 2,
209
+ }))
210
+ const upsertAttributeFamily = jest.fn(async () => {
211
+ controller.abort()
212
+ return { items: [], action: 'create' as const }
213
+ })
214
+ const reconcileAttributes = jest.fn(async () => undefined)
215
+
216
+ ;(createAkeneoClient as jest.Mock).mockReturnValue({ listFamilies })
217
+ ;(createAkeneoImporter as jest.Mock).mockResolvedValue({
218
+ upsertAttributeFamily,
219
+ reconcileAttributes,
220
+ syncMappedCustomFields: jest.fn(async () => []),
221
+ reconcileMappedCustomFieldFieldsets: jest.fn(async () => undefined),
222
+ })
223
+
224
+ const batches: ImportBatch[] = []
225
+ for await (const batch of akeneoDataSyncAdapter.streamImport!({
226
+ entityType: 'attributes',
227
+ batchSize: 100,
228
+ credentials: {},
229
+ mapping: { entityType: 'attributes', fields: [], matchStrategy: 'externalId' },
230
+ scope: { organizationId: 'org-1', tenantId: 'tenant-1' },
231
+ signal: controller.signal,
232
+ })) {
233
+ batches.push(batch)
234
+ }
235
+
236
+ expect(batches).toEqual([])
237
+ expect(upsertAttributeFamily).toHaveBeenCalledTimes(1)
238
+ expect(reconcileAttributes).not.toHaveBeenCalled()
239
+ })
107
240
  })
@@ -81,6 +81,10 @@ export const akeneoDataSyncAdapter: DataSyncAdapter = {
81
81
  const items: ImportItem[] = []
82
82
  const locale = mapping.settings?.categories?.locale ?? 'en_US'
83
83
  for (const category of page.items) {
84
+ // Above the yield, so an abandoned page is never yielded and its cursor never committed.
85
+ // Returning here also skips the `safeFullSync` reconcile below, which is required: a
86
+ // partial walk has not seen every category, so reconciling would delete live records.
87
+ if (input.signal?.aborted) return
84
88
  seenCategoryIds.add(category.code)
85
89
  const result = await importer.upsertCategory(category, locale)
86
90
  items.push({
@@ -143,6 +147,9 @@ export const akeneoDataSyncAdapter: DataSyncAdapter = {
143
147
  })
144
148
  }
145
149
  for (const family of page.items) {
150
+ // Above the yield — see the categories walk. Returning also skips the reconcile, which a
151
+ // partial walk must not run.
152
+ if (input.signal?.aborted) return
146
153
  if (familyFilter.length > 0 && !familyFilter.includes(family.code)) continue
147
154
  seenFamilyIds.add(family.code)
148
155
  const result = await importer.upsertAttributeFamily(
@@ -203,6 +210,9 @@ export const akeneoDataSyncAdapter: DataSyncAdapter = {
203
210
 
204
211
  const items: ImportItem[] = []
205
212
  for (const product of page.items) {
213
+ // Above the yield — see the categories walk. Returning also skips the reconcile, which a
214
+ // partial walk must not run.
215
+ if (input.signal?.aborted) return
206
216
  try {
207
217
  const imported = await importer.upsertProduct(product, mapping)
208
218
  for (const item of imported) {