@open-mercato/sync-akeneo 0.6.8-develop.7100.1.fbf66fca35 → 0.7.0
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,7 +69,6 @@ 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;
|
|
73
72
|
seenCategoryIds.add(category.code);
|
|
74
73
|
const result = await importer.upsertCategory(category, locale);
|
|
75
74
|
items.push({
|
|
@@ -127,7 +126,6 @@ const akeneoDataSyncAdapter = {
|
|
|
127
126
|
});
|
|
128
127
|
}
|
|
129
128
|
for (const family of page.items) {
|
|
130
|
-
if (input.signal?.aborted) return;
|
|
131
129
|
if (familyFilter.length > 0 && !familyFilter.includes(family.code)) continue;
|
|
132
130
|
seenFamilyIds.add(family.code);
|
|
133
131
|
const result = await importer.upsertAttributeFamily(
|
|
@@ -185,7 +183,6 @@ const akeneoDataSyncAdapter = {
|
|
|
185
183
|
});
|
|
186
184
|
const items = [];
|
|
187
185
|
for (const product of page.items) {
|
|
188
|
-
if (input.signal?.aborted) return;
|
|
189
186
|
try {
|
|
190
187
|
const imported = await importer.upsertProduct(product, mapping);
|
|
191
188
|
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 // 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": "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 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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",
|
|
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.
|
|
3
|
+
"version": "0.7.0",
|
|
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.
|
|
57
|
-
"@open-mercato/ui": "0.
|
|
56
|
+
"@open-mercato/core": "0.7.0",
|
|
57
|
+
"@open-mercato/ui": "0.7.0",
|
|
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.
|
|
62
|
+
"@open-mercato/shared": "0.7.0",
|
|
63
63
|
"react": "^19.0.0",
|
|
64
64
|
"react-dom": "^19.0.0"
|
|
65
65
|
},
|
|
66
66
|
"devDependencies": {
|
|
67
|
-
"@open-mercato/shared": "0.
|
|
67
|
+
"@open-mercato/shared": "0.7.0",
|
|
68
68
|
"@types/jest": "^30.0.0",
|
|
69
69
|
"@types/react": "^19.2.17",
|
|
70
70
|
"@types/react-dom": "^19.2.3",
|
|
@@ -83,6 +83,5 @@
|
|
|
83
83
|
"type": "git",
|
|
84
84
|
"url": "https://github.com/open-mercato/open-mercato",
|
|
85
85
|
"directory": "packages/sync-akeneo"
|
|
86
|
-
}
|
|
87
|
-
"stableVersion": "0.6.7"
|
|
86
|
+
}
|
|
88
87
|
}
|
|
@@ -10,16 +10,6 @@ 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
|
-
|
|
23
13
|
import type { ImportBatch } from '@open-mercato/core/modules/data_sync/lib/adapter'
|
|
24
14
|
import { akeneoDataSyncAdapter } from '../lib/adapter'
|
|
25
15
|
import { createAkeneoClient } from '../lib/client'
|
|
@@ -114,127 +104,4 @@ describe('akeneo adapter product import', () => {
|
|
|
114
104
|
}))
|
|
115
105
|
expect(reconcileProducts).toHaveBeenCalled()
|
|
116
106
|
})
|
|
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
|
-
})
|
|
240
107
|
})
|
|
@@ -81,10 +81,6 @@ 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
|
|
88
84
|
seenCategoryIds.add(category.code)
|
|
89
85
|
const result = await importer.upsertCategory(category, locale)
|
|
90
86
|
items.push({
|
|
@@ -147,9 +143,6 @@ export const akeneoDataSyncAdapter: DataSyncAdapter = {
|
|
|
147
143
|
})
|
|
148
144
|
}
|
|
149
145
|
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
|
|
153
146
|
if (familyFilter.length > 0 && !familyFilter.includes(family.code)) continue
|
|
154
147
|
seenFamilyIds.add(family.code)
|
|
155
148
|
const result = await importer.upsertAttributeFamily(
|
|
@@ -210,9 +203,6 @@ export const akeneoDataSyncAdapter: DataSyncAdapter = {
|
|
|
210
203
|
|
|
211
204
|
const items: ImportItem[] = []
|
|
212
205
|
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
|
|
216
206
|
try {
|
|
217
207
|
const imported = await importer.upsertProduct(product, mapping)
|
|
218
208
|
for (const item of imported) {
|