@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": "AAAA,SAAS,8BAA8B;AAGvC,SAAS,uBAAuB,0BAA0B,mBAAmB;AAC7E,SAAS,0BAA0B;AACnC,SAAS,4BAA4B;AACrC,SAAS,yBAAyB;AAClC,SAAS,oCAAoC,8BAA6E;AAE1H,SAAS,iBAAiB,YAAsC;AAC9D,MAAI,eAAe,gBAAgB,eAAe,gBAAgB,eAAe,YAAY;AAC3F,WAAO;AAAA,EACT;AACA,QAAM,IAAI,MAAM,mCAAmC,UAAU,EAAE;AACjE;AAEA,eAAe,eAAe,YAA8B,OAAiF;AAC3I,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,KAAK,UAAU,QAAQ,IAAI;AACjC,SAAO,kBAAkB,IAAI,YAAY,KAAK;AAChD;AAEO,MAAM,wBAAyC;AAAA,EACpD,aAAa;AAAA,EACb,WAAW;AAAA,EACX,mBAAmB,CAAC,cAAc,cAAc,UAAU;AAAA,EAE1D,MAAM,WAAW,OAA6B;AAC5C,UAAM,aAAa,iBAAiB,MAAM,UAAU;AACpD,WAAO,eAAe,YAAY,MAAM,KAAK;AAAA,EAC/C;AAAA,EAEA,MAAM,mBAAmB,OAAkC;AACzD,QAAI;AACF,YAAM,aAAa,iBAAiB,MAAM,UAAU;AACpD,YAAM,SAAS,mBAAmB,MAAM,WAAW;AACnD,UAAI,eAAe,YAAY;AAC7B,cAAM,OAAO,aAAa,EAAE,WAAW,EAAE,CAAC;AAAA,MAC5C,WAAW,eAAe,cAAc;AACtC,cAAM,OAAO,eAAe,MAAM,CAAC;AAAA,MACrC,OAAO;AACL,cAAM,OAAO,aAAa,MAAM,CAAC;AAAA,MACnC;AAEA,YAAM,YAAY,MAAM,OAAO,qBAAqB,EAAE,MAAM,MAAM,IAAI;AACtE,aAAO;AAAA,QACL,IAAI;AAAA,QACJ,SAAS;AAAA,QACT,SAAS,YACL;AAAA,UACE,SAAS,UAAU,QAAQ;AAAA,UAC3B,UAAU,UAAU,SAAS;AAAA,UAC7B,YAAY,UAAU,WAAW;AAAA,UACjC,UAAU,UAAU,SAAS;AAAA,UAC7B,SAAS,UAAU;AAAA,QACrB,IACA;AAAA,MACN;AAAA,IACF,SAAS,OAAO;AACd,aAAO;AAAA,QACL,IAAI;AAAA,QACJ,SAAS,iBAAiB,QAAQ,MAAM,UAAU;AAAA,MACpD;AAAA,IACF;AAAA,EACF;AAAA,EAEA,OAAO,aAAa,OAAmC;AACrD,UAAM,aAAa,iBAAiB,MAAM,UAAU;AACpD,UAAM,UAAU,uBAAuB,YAAY,MAAM,OAA6C;AACtG,UAAM,SAAS,mBAAmB,MAAM,WAAW;AACnD,UAAM,WAAW,MAAM,qBAAqB,QAAQ,MAAM,KAAK;AAE/D,QAAI,eAAe,cAAc;AAC/B,UAAIA,cAAa;AACjB,UAAIC,WAAU,YAAY,MAAM,MAAM,GAAG,WAAW;AACpD,YAAMC,gBAAe,CAAC,MAAM;AAC5B,YAAM,kBAAkB,oBAAI,IAAY;AACxC,YAAMC,kBAAiB,mCAAmC;AAC1D,SAAG;AACD,cAAM,OAAO,MAAM,OAAO,eAAeF,UAAS,MAAM,SAAS;AACjE,cAAM,QAAsB,CAAC;AAC7B,cAAM,SAAS,QAAQ,UAAU,YAAY,UAAU;AACvD,mBAAW,YAAY,KAAK,OAAO;
|
|
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.
|
|
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.
|
|
57
|
-
"@open-mercato/ui": "0.6.8-develop.
|
|
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.
|
|
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.
|
|
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) {
|