@open-mercato/sync-akeneo 0.10.1-develop.7576.1.21d5b50435 → 0.10.1-develop.7578.1.677eeb8343
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.
- package/dist/modules/sync_akeneo/lib/catalog-importer.js +89 -40
- package/dist/modules/sync_akeneo/lib/catalog-importer.js.map +2 -2
- package/dist/modules/sync_akeneo/lib/client.js +143 -70
- package/dist/modules/sync_akeneo/lib/client.js.map +2 -2
- package/package.json +5 -5
- package/src/modules/sync_akeneo/__tests__/catalog-importer.media-limits.test.ts +426 -0
- package/src/modules/sync_akeneo/__tests__/client.response-limits.test.ts +339 -0
- package/src/modules/sync_akeneo/i18n/de.json +1 -0
- package/src/modules/sync_akeneo/i18n/en.json +1 -0
- package/src/modules/sync_akeneo/i18n/es.json +1 -0
- package/src/modules/sync_akeneo/i18n/ko.json +1 -0
- package/src/modules/sync_akeneo/i18n/pl.json +1 -0
- package/src/modules/sync_akeneo/lib/catalog-importer.ts +97 -44
- package/src/modules/sync_akeneo/lib/client.ts +174 -98
|
@@ -3,6 +3,8 @@ import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
|
3
3
|
import { emitCrudSideEffects, setCustomFieldsIfAny } from "@open-mercato/shared/lib/commands/helpers";
|
|
4
4
|
import { withAtomicFlush } from "@open-mercato/shared/lib/commands/flush";
|
|
5
5
|
import { findOneWithDecryption, findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
6
|
+
import { isCrudHttpError } from "@open-mercato/shared/lib/crud/errors";
|
|
7
|
+
import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
|
|
6
8
|
import { SyncExternalIdMapping } from "@open-mercato/core/modules/integrations/data/entities";
|
|
7
9
|
import {
|
|
8
10
|
CatalogOffer,
|
|
@@ -20,9 +22,9 @@ import { normalizeEntityFieldsetConfig } from "@open-mercato/core/modules/entiti
|
|
|
20
22
|
import { invalidateDefinitionsCache } from "@open-mercato/core/modules/entities/api/definitions.cache";
|
|
21
23
|
import { Attachment } from "@open-mercato/core/modules/attachments/data/entities";
|
|
22
24
|
import { buildAttachmentFileUrl, buildAttachmentImageUrl, slugifyAttachmentFileName } from "@open-mercato/core/modules/attachments/lib/imageUrls";
|
|
23
|
-
import {
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
import {
|
|
26
|
+
isScopedAttachmentUploadError
|
|
27
|
+
} from "@open-mercato/core/modules/attachments/lib/scoped-upload-service";
|
|
26
28
|
import { attachmentCrudEvents, attachmentCrudIndexer } from "@open-mercato/core/modules/attachments/lib/crud";
|
|
27
29
|
import { emitCatalogQueryIndexEvent } from "@open-mercato/core/modules/catalog/commands/shared";
|
|
28
30
|
import { SalesChannel } from "@open-mercato/core/modules/sales/data/entities";
|
|
@@ -35,8 +37,14 @@ import {
|
|
|
35
37
|
slugifyAkeneoCode,
|
|
36
38
|
buildDefaultAkeneoMapping
|
|
37
39
|
} from "./shared.js";
|
|
40
|
+
import { isAkeneoResponseTooLargeError } from "./client.js";
|
|
38
41
|
import { inferAkeneoProductMapping } from "./inference.js";
|
|
39
42
|
import { normalizeMarkdownText } from "./markdown.js";
|
|
43
|
+
const REJECTED_MEDIA_UPLOAD_CODES = /* @__PURE__ */ new Set([
|
|
44
|
+
"dangerous_executable",
|
|
45
|
+
"max_upload_size",
|
|
46
|
+
"active_content"
|
|
47
|
+
]);
|
|
40
48
|
const PRODUCT_ENTITY_ID = "catalog:catalog_product";
|
|
41
49
|
const VARIANT_ENTITY_ID = "catalog:catalog_product_variant";
|
|
42
50
|
function readPreferredAkeneoValue(values, attributeCode, locale, channel) {
|
|
@@ -1748,40 +1756,61 @@ async function createAkeneoImporter(client, scope) {
|
|
|
1748
1756
|
}
|
|
1749
1757
|
}
|
|
1750
1758
|
}
|
|
1759
|
+
async function recordRejectedMedia(asset, reason) {
|
|
1760
|
+
const integrationLogService = container.resolve("integrationLogService");
|
|
1761
|
+
await integrationLogService.write({
|
|
1762
|
+
integrationId: "sync_akeneo",
|
|
1763
|
+
level: "warn",
|
|
1764
|
+
code: "sync_akeneo.media_rejected",
|
|
1765
|
+
message: `Akeneo media ${asset.remote.codeOrUrl} was not imported: ${reason}`,
|
|
1766
|
+
scopeEntityType: asset.entityId,
|
|
1767
|
+
scopeEntityId: asset.recordId,
|
|
1768
|
+
payload: {
|
|
1769
|
+
externalId: asset.externalId,
|
|
1770
|
+
remoteCode: asset.remote.codeOrUrl,
|
|
1771
|
+
kind: asset.kind,
|
|
1772
|
+
reason
|
|
1773
|
+
}
|
|
1774
|
+
}, scope);
|
|
1775
|
+
}
|
|
1751
1776
|
async function upsertAsset(asset) {
|
|
1752
1777
|
const existingAttachmentId = await externalIdMappingService.lookupLocalId("sync_akeneo", "attachment", asset.externalId, scope);
|
|
1753
1778
|
if (existingAttachmentId) return existingAttachmentId;
|
|
1754
|
-
|
|
1755
|
-
|
|
1779
|
+
let binary;
|
|
1780
|
+
try {
|
|
1781
|
+
binary = await client.downloadMediaFile(asset.remote.codeOrUrl);
|
|
1782
|
+
} catch (error) {
|
|
1783
|
+
if (!isAkeneoResponseTooLargeError(error)) throw error;
|
|
1784
|
+
await recordRejectedMedia(asset, "max_upload_size");
|
|
1785
|
+
return null;
|
|
1786
|
+
}
|
|
1756
1787
|
const fileName = asset.remote.fileNameHint ?? binary.fileName ?? `akeneo-${randomUUID().slice(0, 8)}`;
|
|
1757
|
-
const
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
}
|
|
1782
|
-
|
|
1783
|
-
await em.flush();
|
|
1784
|
-
attachment.url = asset.kind === "image" ? buildAttachmentImageUrl(attachment.id, { slug: slugifyAttachmentFileName(fileName) }) : buildAttachmentFileUrl(attachment.id);
|
|
1788
|
+
const uploadService = container.resolve("attachmentScopedUploadService");
|
|
1789
|
+
let attachment;
|
|
1790
|
+
try {
|
|
1791
|
+
attachment = await uploadService.upload({
|
|
1792
|
+
tenantId: scope.tenantId,
|
|
1793
|
+
organizationId: scope.organizationId,
|
|
1794
|
+
entityId: asset.entityId,
|
|
1795
|
+
recordId: asset.recordId,
|
|
1796
|
+
fileName,
|
|
1797
|
+
declaredMimeType: asset.remote.mimeTypeHint ?? binary.contentType,
|
|
1798
|
+
buffer: binary.buffer
|
|
1799
|
+
});
|
|
1800
|
+
} catch (error) {
|
|
1801
|
+
if (!isScopedAttachmentUploadError(error)) throw error;
|
|
1802
|
+
if (REJECTED_MEDIA_UPLOAD_CODES.has(error.code)) {
|
|
1803
|
+
await recordRejectedMedia(asset, error.code);
|
|
1804
|
+
return null;
|
|
1805
|
+
}
|
|
1806
|
+
const { translate } = await resolveTranslations();
|
|
1807
|
+
throw new Error(translate(
|
|
1808
|
+
"sync_akeneo.errors.mediaStorageRejected",
|
|
1809
|
+
"Akeneo media {code} was rejected by attachment storage ({reason}).",
|
|
1810
|
+
{ code: asset.remote.codeOrUrl, reason: error.code }
|
|
1811
|
+
));
|
|
1812
|
+
}
|
|
1813
|
+
attachment.url = asset.kind === "image" ? buildAttachmentImageUrl(attachment.id, { slug: slugifyAttachmentFileName(attachment.fileName) }) : buildAttachmentFileUrl(attachment.id);
|
|
1785
1814
|
attachment.storageMetadata = {
|
|
1786
1815
|
...attachment.storageMetadata ?? {},
|
|
1787
1816
|
source: "akeneo",
|
|
@@ -1790,16 +1819,37 @@ async function createAkeneoImporter(client, scope) {
|
|
|
1790
1819
|
remoteCode: asset.remote.codeOrUrl
|
|
1791
1820
|
};
|
|
1792
1821
|
await em.flush();
|
|
1793
|
-
await emitAttachmentCrudChange("
|
|
1822
|
+
await emitAttachmentCrudChange("updated", attachment);
|
|
1794
1823
|
await externalIdMappingService.storeExternalIdMapping("sync_akeneo", "attachment", attachment.id, asset.externalId, scope);
|
|
1795
1824
|
return attachment.id;
|
|
1796
1825
|
}
|
|
1826
|
+
async function releaseImportedAttachment(attachment) {
|
|
1827
|
+
const attachmentService = container.resolve("attachmentService");
|
|
1828
|
+
if (!attachmentService.releaseScoped) {
|
|
1829
|
+
throw new Error("[internal] attachmentService.releaseScoped is not available");
|
|
1830
|
+
}
|
|
1831
|
+
try {
|
|
1832
|
+
await attachmentService.releaseScoped({
|
|
1833
|
+
attachmentId: attachment.id,
|
|
1834
|
+
tenantId: scope.tenantId,
|
|
1835
|
+
organizationId: scope.organizationId,
|
|
1836
|
+
expectedOwner: { entityId: attachment.entityId, recordId: attachment.recordId },
|
|
1837
|
+
expectedAssignment: { type: attachment.entityId, id: attachment.recordId },
|
|
1838
|
+
expectedPartitionCode: attachment.partitionCode
|
|
1839
|
+
});
|
|
1840
|
+
return true;
|
|
1841
|
+
} catch (error) {
|
|
1842
|
+
if (isCrudHttpError(error) && error.status === 409) return false;
|
|
1843
|
+
throw error;
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1797
1846
|
async function reconcileAttachments(params) {
|
|
1798
1847
|
const desiredExternalIds = new Set(params.desiredAssets.map((asset) => asset.externalId));
|
|
1799
|
-
|
|
1800
|
-
const
|
|
1801
|
-
|
|
1802
|
-
|
|
1848
|
+
let heroAttachmentId = null;
|
|
1849
|
+
for (const asset of params.desiredAssets) {
|
|
1850
|
+
const attachmentId = await upsertAsset(asset);
|
|
1851
|
+
if (!heroAttachmentId && attachmentId && asset.kind === "image") heroAttachmentId = attachmentId;
|
|
1852
|
+
}
|
|
1803
1853
|
const heroAttachmentUrl = heroAttachmentId ? buildAttachmentImageUrl(heroAttachmentId) : null;
|
|
1804
1854
|
const existing = await findWithDecryption(em, Attachment, {
|
|
1805
1855
|
entityId: params.entityId,
|
|
@@ -1814,8 +1864,7 @@ async function createAkeneoImporter(client, scope) {
|
|
|
1814
1864
|
if (!externalId || !kind) continue;
|
|
1815
1865
|
const shouldDelete = kind === "image" ? params.reconciliation.deleteMissingMedia : params.reconciliation.deleteMissingAttachments;
|
|
1816
1866
|
if (!shouldDelete || desiredExternalIds.has(externalId)) continue;
|
|
1817
|
-
await
|
|
1818
|
-
await em.remove(attachment).flush();
|
|
1867
|
+
if (!await releaseImportedAttachment(attachment)) continue;
|
|
1819
1868
|
await emitAttachmentCrudChange("deleted", attachment);
|
|
1820
1869
|
}
|
|
1821
1870
|
return {
|