@open-mercato/sync-akeneo 0.9.1-develop.7574.1.837288eec1 → 0.9.1-develop.7575.1.8480826a9b

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.
@@ -3,8 +3,6 @@ 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";
8
6
  import { SyncExternalIdMapping } from "@open-mercato/core/modules/integrations/data/entities";
9
7
  import {
10
8
  CatalogOffer,
@@ -22,9 +20,9 @@ import { normalizeEntityFieldsetConfig } from "@open-mercato/core/modules/entiti
22
20
  import { invalidateDefinitionsCache } from "@open-mercato/core/modules/entities/api/definitions.cache";
23
21
  import { Attachment } from "@open-mercato/core/modules/attachments/data/entities";
24
22
  import { buildAttachmentFileUrl, buildAttachmentImageUrl, slugifyAttachmentFileName } from "@open-mercato/core/modules/attachments/lib/imageUrls";
25
- import {
26
- isScopedAttachmentUploadError
27
- } from "@open-mercato/core/modules/attachments/lib/scoped-upload-service";
23
+ import { deletePartitionFile, storePartitionFile } from "@open-mercato/core/modules/attachments/lib/storage";
24
+ import { ensureDefaultPartitions, resolveDefaultPartitionCode } from "@open-mercato/core/modules/attachments/lib/partitions";
25
+ import { mergeAttachmentMetadata } from "@open-mercato/core/modules/attachments/lib/metadata";
28
26
  import { attachmentCrudEvents, attachmentCrudIndexer } from "@open-mercato/core/modules/attachments/lib/crud";
29
27
  import { emitCatalogQueryIndexEvent } from "@open-mercato/core/modules/catalog/commands/shared";
30
28
  import { SalesChannel } from "@open-mercato/core/modules/sales/data/entities";
@@ -37,14 +35,8 @@ import {
37
35
  slugifyAkeneoCode,
38
36
  buildDefaultAkeneoMapping
39
37
  } from "./shared.js";
40
- import { isAkeneoResponseTooLargeError } from "./client.js";
41
38
  import { inferAkeneoProductMapping } from "./inference.js";
42
39
  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
- ]);
48
40
  const PRODUCT_ENTITY_ID = "catalog:catalog_product";
49
41
  const VARIANT_ENTITY_ID = "catalog:catalog_product_variant";
50
42
  function readPreferredAkeneoValue(values, attributeCode, locale, channel) {
@@ -1756,61 +1748,40 @@ async function createAkeneoImporter(client, scope) {
1756
1748
  }
1757
1749
  }
1758
1750
  }
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
- }
1776
1751
  async function upsertAsset(asset) {
1777
1752
  const existingAttachmentId = await externalIdMappingService.lookupLocalId("sync_akeneo", "attachment", asset.externalId, scope);
1778
1753
  if (existingAttachmentId) return existingAttachmentId;
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
- }
1754
+ await ensureDefaultPartitions(em);
1755
+ const binary = await client.downloadMediaFile(asset.remote.codeOrUrl);
1787
1756
  const fileName = asset.remote.fileNameHint ?? binary.fileName ?? `akeneo-${randomUUID().slice(0, 8)}`;
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);
1757
+ const partitionCode = resolveDefaultPartitionCode(asset.entityId);
1758
+ const stored = await storePartitionFile({
1759
+ partitionCode,
1760
+ orgId: scope.organizationId,
1761
+ tenantId: scope.tenantId,
1762
+ fileName,
1763
+ buffer: binary.buffer
1764
+ });
1765
+ const attachment = em.create(Attachment, {
1766
+ entityId: asset.entityId,
1767
+ recordId: asset.recordId,
1768
+ organizationId: scope.organizationId,
1769
+ tenantId: scope.tenantId,
1770
+ partitionCode,
1771
+ fileName,
1772
+ mimeType: asset.remote.mimeTypeHint ?? binary.contentType ?? "application/octet-stream",
1773
+ fileSize: binary.contentLength ?? binary.buffer.byteLength,
1774
+ storageDriver: "local",
1775
+ storagePath: stored.storagePath,
1776
+ storageMetadata: mergeAttachmentMetadata(null, {
1777
+ assignments: [{ type: asset.entityId, id: asset.recordId }]
1778
+ }),
1779
+ url: buildAttachmentFileUrl("pending"),
1780
+ content: null
1781
+ });
1782
+ em.persist(attachment);
1783
+ await em.flush();
1784
+ attachment.url = asset.kind === "image" ? buildAttachmentImageUrl(attachment.id, { slug: slugifyAttachmentFileName(fileName) }) : buildAttachmentFileUrl(attachment.id);
1814
1785
  attachment.storageMetadata = {
1815
1786
  ...attachment.storageMetadata ?? {},
1816
1787
  source: "akeneo",
@@ -1819,37 +1790,16 @@ async function createAkeneoImporter(client, scope) {
1819
1790
  remoteCode: asset.remote.codeOrUrl
1820
1791
  };
1821
1792
  await em.flush();
1822
- await emitAttachmentCrudChange("updated", attachment);
1793
+ await emitAttachmentCrudChange("created", attachment);
1823
1794
  await externalIdMappingService.storeExternalIdMapping("sync_akeneo", "attachment", attachment.id, asset.externalId, scope);
1824
1795
  return attachment.id;
1825
1796
  }
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
- }
1846
1797
  async function reconcileAttachments(params) {
1847
1798
  const desiredExternalIds = new Set(params.desiredAssets.map((asset) => asset.externalId));
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
- }
1799
+ const desiredImages = params.desiredAssets.filter((asset) => asset.kind === "image");
1800
+ const desiredFiles = params.desiredAssets.filter((asset) => asset.kind === "file");
1801
+ const desiredAttachmentIds = await Promise.all(params.desiredAssets.map((asset) => upsertAsset(asset)));
1802
+ const heroAttachmentId = desiredImages.length > 0 ? desiredAttachmentIds[params.desiredAssets.findIndex((asset) => asset.externalId === desiredImages[0]?.externalId)] : null;
1853
1803
  const heroAttachmentUrl = heroAttachmentId ? buildAttachmentImageUrl(heroAttachmentId) : null;
1854
1804
  const existing = await findWithDecryption(em, Attachment, {
1855
1805
  entityId: params.entityId,
@@ -1864,7 +1814,8 @@ async function createAkeneoImporter(client, scope) {
1864
1814
  if (!externalId || !kind) continue;
1865
1815
  const shouldDelete = kind === "image" ? params.reconciliation.deleteMissingMedia : params.reconciliation.deleteMissingAttachments;
1866
1816
  if (!shouldDelete || desiredExternalIds.has(externalId)) continue;
1867
- if (!await releaseImportedAttachment(attachment)) continue;
1817
+ await deletePartitionFile(attachment.partitionCode, attachment.storagePath, attachment.storageDriver);
1818
+ await em.remove(attachment).flush();
1868
1819
  await emitAttachmentCrudChange("deleted", attachment);
1869
1820
  }
1870
1821
  return {