@open-mercato/sync-akeneo 0.6.8-develop.6986.1.3adb0d0df6 → 0.6.8-develop.6992.1.00c90fecff
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/delete-imported-products.js +5 -0
- package/dist/modules/sync_akeneo/lib/delete-imported-products.js.map +2 -2
- package/package.json +5 -5
- package/src/modules/sync_akeneo/__tests__/delete-imported-products.reset.test.ts +74 -0
- package/src/modules/sync_akeneo/lib/delete-imported-products.ts +10 -0
|
@@ -88,6 +88,7 @@ async function deleteImportedProductsWithProgress(params) {
|
|
|
88
88
|
const em = container.resolve("em");
|
|
89
89
|
const commandBus = container.resolve("commandBus");
|
|
90
90
|
const progressService = container.resolve("progressService");
|
|
91
|
+
const syncRunService = container.resolve("dataSyncRunService");
|
|
91
92
|
const progressContext = {
|
|
92
93
|
tenantId: scope.tenantId,
|
|
93
94
|
organizationId: scope.organizationId,
|
|
@@ -140,6 +141,10 @@ async function deleteImportedProductsWithProgress(params) {
|
|
|
140
141
|
organizationId: scope.organizationId,
|
|
141
142
|
tenantId: scope.tenantId
|
|
142
143
|
});
|
|
144
|
+
await syncRunService.resetResumePosition("sync_akeneo", "products", "import", {
|
|
145
|
+
organizationId: scope.organizationId,
|
|
146
|
+
tenantId: scope.tenantId
|
|
147
|
+
});
|
|
143
148
|
const summary = buildSummary(productIds.length, deletedProductCount);
|
|
144
149
|
await progressService.completeJob(progressJobId, { resultSummary: summary }, progressContext);
|
|
145
150
|
return summary;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/sync_akeneo/lib/delete-imported-products.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { AwilixContainer } from 'awilix'\nimport type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport type { ProgressService, ProgressServiceContext } from '@open-mercato/core/modules/progress/lib/progressService'\nimport { CatalogProduct } from '@open-mercato/core/modules/catalog/data/entities'\nimport { SyncCursor } from '@open-mercato/core/modules/data_sync/data/entities'\nimport { SyncExternalIdMapping } from '@open-mercato/core/modules/integrations/data/entities'\n\nexport const AKENEO_DELETE_IMPORTED_PRODUCTS_QUEUE = 'sync-akeneo-delete-products'\n\nconst PRODUCT_MAPPING_ENTITY_TYPES = [\n 'catalog_product',\n 'catalog_product_variant',\n 'catalog_offer',\n 'catalog_product_price',\n 'attachment',\n] as const\n\nexport type DeleteImportedProductsScope = {\n organizationId: string\n tenantId: string\n userId?: string | null\n}\n\nexport type DeleteImportedProductsJobPayload = {\n progressJobId: string\n scope: DeleteImportedProductsScope\n}\n\nexport type DeleteImportedProductsSummary = {\n message: string\n requestedProductCount: number\n deletedProductCount: number\n skippedProductCount: number\n}\n\nfunction buildCommandContext(\n scope: DeleteImportedProductsScope,\n container: AwilixContainer,\n): CommandRuntimeContext {\n return {\n container,\n auth: null,\n organizationScope: {\n selectedId: scope.organizationId,\n filterIds: [scope.organizationId],\n allowedIds: [scope.organizationId],\n tenantId: scope.tenantId,\n },\n selectedOrganizationId: scope.organizationId,\n organizationIds: [scope.organizationId],\n }\n}\n\nfunction buildSummary(requestedProductCount: number, deletedProductCount: number): DeleteImportedProductsSummary {\n const skippedProductCount = Math.max(0, requestedProductCount - deletedProductCount)\n const baseMessage = deletedProductCount === 1\n ? 'Deleted 1 Akeneo-imported product.'\n : `Deleted ${deletedProductCount} Akeneo-imported products.`\n\n if (skippedProductCount <= 0) {\n return {\n message: baseMessage,\n requestedProductCount,\n deletedProductCount,\n skippedProductCount,\n }\n }\n\n return {\n message: `${baseMessage} Skipped ${skippedProductCount}.`,\n requestedProductCount,\n deletedProductCount,\n skippedProductCount,\n }\n}\n\nexport async function findAkeneoImportedProductIds(\n em: EntityManager,\n scope: Pick<DeleteImportedProductsScope, 'organizationId' | 'tenantId'>,\n): Promise<string[]> {\n const productMappings = await findWithDecryption(\n em,\n SyncExternalIdMapping,\n {\n integrationId: 'sync_akeneo',\n internalEntityType: 'catalog_product',\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n },\n {\n fields: ['internalEntityId', 'createdAt'],\n orderBy: { createdAt: 'asc' },\n },\n scope,\n )\n\n const metadataProducts = await findWithDecryption(\n em,\n CatalogProduct,\n {\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n metadata: {\n source: 'akeneo',\n },\n },\n {\n fields: ['id'],\n orderBy: { createdAt: 'asc' },\n },\n scope,\n )\n\n return Array.from(new Set(\n [\n ...(productMappings as Array<{ internalEntityId: string }>).map((entry) => entry.internalEntityId),\n ...(metadataProducts as Array<{ id: string }>).map((entry) => entry.id),\n ],\n ))\n}\n\nexport async function deleteImportedProductsWithProgress(params: {\n container: AwilixContainer\n progressJobId: string\n scope: DeleteImportedProductsScope\n}): Promise<DeleteImportedProductsSummary> {\n const { container, progressJobId, scope } = params\n const em = container.resolve('em') as EntityManager\n const commandBus = container.resolve('commandBus') as CommandBus\n const progressService = container.resolve('progressService') as ProgressService\n const progressContext: ProgressServiceContext = {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n userId: scope.userId,\n }\n\n await progressService.startJob(progressJobId, progressContext)\n\n const productIds = await findAkeneoImportedProductIds(em, scope)\n if (productIds.length > 0) {\n await progressService.updateProgress(\n progressJobId,\n { totalCount: productIds.length, processedCount: 0 },\n progressContext,\n )\n }\n\n if (productIds.length === 0) {\n const summary = buildSummary(0, 0)\n await progressService.completeJob(progressJobId, { resultSummary: summary }, progressContext)\n return summary\n }\n\n const commandContext = buildCommandContext(scope, container)\n let deletedProductCount = 0\n\n for (const [index, productId] of productIds.entries()) {\n try {\n await commandBus.execute<{ body?: Record<string, unknown> }, { productId: string }>('catalog.products.delete', {\n input: { body: { id: productId } },\n ctx: commandContext,\n })\n deletedProductCount += 1\n } catch {}\n\n await progressService.updateProgress(\n progressJobId,\n {\n totalCount: productIds.length,\n processedCount: index + 1,\n },\n progressContext,\n )\n }\n\n await em.nativeDelete(SyncExternalIdMapping, {\n integrationId: 'sync_akeneo',\n internalEntityType: { $in: [...PRODUCT_MAPPING_ENTITY_TYPES] },\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n })\n\n await em.nativeDelete(SyncCursor, {\n integrationId: 'sync_akeneo',\n entityType: 'products',\n direction: 'import',\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n })\n\n const summary = buildSummary(productIds.length, deletedProductCount)\n await progressService.completeJob(progressJobId, { resultSummary: summary }, progressContext)\n\n return summary\n}\n"],
|
|
5
|
-
"mappings": "AAGA,SAAS,0BAA0B;AAEnC,SAAS,sBAAsB;AAC/B,SAAS,kBAAkB;
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { AwilixContainer } from 'awilix'\nimport type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport type { ProgressService, ProgressServiceContext } from '@open-mercato/core/modules/progress/lib/progressService'\nimport { CatalogProduct } from '@open-mercato/core/modules/catalog/data/entities'\nimport { SyncCursor } from '@open-mercato/core/modules/data_sync/data/entities'\nimport type { SyncRunService } from '@open-mercato/core/modules/data_sync/lib/sync-run-service'\nimport { SyncExternalIdMapping } from '@open-mercato/core/modules/integrations/data/entities'\n\nexport const AKENEO_DELETE_IMPORTED_PRODUCTS_QUEUE = 'sync-akeneo-delete-products'\n\nconst PRODUCT_MAPPING_ENTITY_TYPES = [\n 'catalog_product',\n 'catalog_product_variant',\n 'catalog_offer',\n 'catalog_product_price',\n 'attachment',\n] as const\n\nexport type DeleteImportedProductsScope = {\n organizationId: string\n tenantId: string\n userId?: string | null\n}\n\nexport type DeleteImportedProductsJobPayload = {\n progressJobId: string\n scope: DeleteImportedProductsScope\n}\n\nexport type DeleteImportedProductsSummary = {\n message: string\n requestedProductCount: number\n deletedProductCount: number\n skippedProductCount: number\n}\n\nfunction buildCommandContext(\n scope: DeleteImportedProductsScope,\n container: AwilixContainer,\n): CommandRuntimeContext {\n return {\n container,\n auth: null,\n organizationScope: {\n selectedId: scope.organizationId,\n filterIds: [scope.organizationId],\n allowedIds: [scope.organizationId],\n tenantId: scope.tenantId,\n },\n selectedOrganizationId: scope.organizationId,\n organizationIds: [scope.organizationId],\n }\n}\n\nfunction buildSummary(requestedProductCount: number, deletedProductCount: number): DeleteImportedProductsSummary {\n const skippedProductCount = Math.max(0, requestedProductCount - deletedProductCount)\n const baseMessage = deletedProductCount === 1\n ? 'Deleted 1 Akeneo-imported product.'\n : `Deleted ${deletedProductCount} Akeneo-imported products.`\n\n if (skippedProductCount <= 0) {\n return {\n message: baseMessage,\n requestedProductCount,\n deletedProductCount,\n skippedProductCount,\n }\n }\n\n return {\n message: `${baseMessage} Skipped ${skippedProductCount}.`,\n requestedProductCount,\n deletedProductCount,\n skippedProductCount,\n }\n}\n\nexport async function findAkeneoImportedProductIds(\n em: EntityManager,\n scope: Pick<DeleteImportedProductsScope, 'organizationId' | 'tenantId'>,\n): Promise<string[]> {\n const productMappings = await findWithDecryption(\n em,\n SyncExternalIdMapping,\n {\n integrationId: 'sync_akeneo',\n internalEntityType: 'catalog_product',\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n },\n {\n fields: ['internalEntityId', 'createdAt'],\n orderBy: { createdAt: 'asc' },\n },\n scope,\n )\n\n const metadataProducts = await findWithDecryption(\n em,\n CatalogProduct,\n {\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n metadata: {\n source: 'akeneo',\n },\n },\n {\n fields: ['id'],\n orderBy: { createdAt: 'asc' },\n },\n scope,\n )\n\n return Array.from(new Set(\n [\n ...(productMappings as Array<{ internalEntityId: string }>).map((entry) => entry.internalEntityId),\n ...(metadataProducts as Array<{ id: string }>).map((entry) => entry.id),\n ],\n ))\n}\n\nexport async function deleteImportedProductsWithProgress(params: {\n container: AwilixContainer\n progressJobId: string\n scope: DeleteImportedProductsScope\n}): Promise<DeleteImportedProductsSummary> {\n const { container, progressJobId, scope } = params\n const em = container.resolve('em') as EntityManager\n const commandBus = container.resolve('commandBus') as CommandBus\n const progressService = container.resolve('progressService') as ProgressService\n const syncRunService = container.resolve('dataSyncRunService') as SyncRunService\n const progressContext: ProgressServiceContext = {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n userId: scope.userId,\n }\n\n await progressService.startJob(progressJobId, progressContext)\n\n const productIds = await findAkeneoImportedProductIds(em, scope)\n if (productIds.length > 0) {\n await progressService.updateProgress(\n progressJobId,\n { totalCount: productIds.length, processedCount: 0 },\n progressContext,\n )\n }\n\n if (productIds.length === 0) {\n const summary = buildSummary(0, 0)\n await progressService.completeJob(progressJobId, { resultSummary: summary }, progressContext)\n return summary\n }\n\n const commandContext = buildCommandContext(scope, container)\n let deletedProductCount = 0\n\n for (const [index, productId] of productIds.entries()) {\n try {\n await commandBus.execute<{ body?: Record<string, unknown> }, { productId: string }>('catalog.products.delete', {\n input: { body: { id: productId } },\n ctx: commandContext,\n })\n deletedProductCount += 1\n } catch {}\n\n await progressService.updateProgress(\n progressJobId,\n {\n totalCount: productIds.length,\n processedCount: index + 1,\n },\n progressContext,\n )\n }\n\n await em.nativeDelete(SyncExternalIdMapping, {\n integrationId: 'sync_akeneo',\n internalEntityType: { $in: [...PRODUCT_MAPPING_ENTITY_TYPES] },\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n })\n\n await em.nativeDelete(SyncCursor, {\n integrationId: 'sync_akeneo',\n entityType: 'products',\n direction: 'import',\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n })\n\n // Deleting the shared row only resets entity types that write one. Clear the\n // run-scoped position too, so this stays a full reset if the products adapter\n // ever opts out of the shared cursor.\n await syncRunService.resetResumePosition('sync_akeneo', 'products', 'import', {\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n })\n\n const summary = buildSummary(productIds.length, deletedProductCount)\n await progressService.completeJob(progressJobId, { resultSummary: summary }, progressContext)\n\n return summary\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["summary"]
|
|
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.6992.1.00c90fecff",
|
|
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.6992.1.00c90fecff",
|
|
57
|
+
"@open-mercato/ui": "0.6.8-develop.6992.1.00c90fecff",
|
|
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.6992.1.00c90fecff",
|
|
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.6992.1.00c90fecff",
|
|
68
68
|
"@types/jest": "^30.0.0",
|
|
69
69
|
"@types/react": "^19.2.17",
|
|
70
70
|
"@types/react-dom": "^19.2.3",
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/** @jest-environment node */
|
|
2
|
+
|
|
3
|
+
import type { AwilixContainer } from 'awilix'
|
|
4
|
+
import { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
5
|
+
import { SyncCursor } from '@open-mercato/core/modules/data_sync/data/entities'
|
|
6
|
+
|
|
7
|
+
jest.mock('@open-mercato/shared/lib/encryption/find', () => ({
|
|
8
|
+
findWithDecryption: jest.fn(),
|
|
9
|
+
findOneWithDecryption: jest.fn(),
|
|
10
|
+
}))
|
|
11
|
+
|
|
12
|
+
import { deleteImportedProductsWithProgress } from '../lib/delete-imported-products'
|
|
13
|
+
|
|
14
|
+
const SCOPE = { organizationId: 'org-1', tenantId: 'tenant-1', userId: 'user-1' }
|
|
15
|
+
|
|
16
|
+
function buildHarness(productIds: string[]) {
|
|
17
|
+
;(findWithDecryption as jest.Mock).mockResolvedValue(
|
|
18
|
+
productIds.map((id) => ({ internalEntityId: id, createdAt: new Date('2026-08-01') })),
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
const em = {
|
|
22
|
+
nativeDelete: jest.fn(async () => 1),
|
|
23
|
+
}
|
|
24
|
+
const resetResumePosition = jest.fn(async () => 1)
|
|
25
|
+
const services: Record<string, unknown> = {
|
|
26
|
+
em,
|
|
27
|
+
commandBus: { execute: jest.fn(async () => ({ productId: 'p' })) },
|
|
28
|
+
progressService: {
|
|
29
|
+
startJob: jest.fn(async () => undefined),
|
|
30
|
+
updateProgress: jest.fn(async () => undefined),
|
|
31
|
+
completeJob: jest.fn(async () => undefined),
|
|
32
|
+
},
|
|
33
|
+
dataSyncRunService: { resetResumePosition },
|
|
34
|
+
}
|
|
35
|
+
const container = { resolve: (key: string) => services[key] } as unknown as AwilixContainer
|
|
36
|
+
|
|
37
|
+
return { container, em, resetResumePosition }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe('deleteImportedProductsWithProgress resets both cursor surfaces', () => {
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
jest.clearAllMocks()
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('clears the run-scoped resume position alongside the shared cursor row', async () => {
|
|
46
|
+
const { container, em, resetResumePosition } = buildHarness(['product-1'])
|
|
47
|
+
|
|
48
|
+
await deleteImportedProductsWithProgress({ container, progressJobId: 'job-1', scope: SCOPE })
|
|
49
|
+
|
|
50
|
+
expect(em.nativeDelete).toHaveBeenCalledWith(SyncCursor, expect.objectContaining({
|
|
51
|
+
integrationId: 'sync_akeneo',
|
|
52
|
+
entityType: 'products',
|
|
53
|
+
direction: 'import',
|
|
54
|
+
organizationId: 'org-1',
|
|
55
|
+
tenantId: 'tenant-1',
|
|
56
|
+
}))
|
|
57
|
+
// Deleting the shared row only resets entity types that write one; without
|
|
58
|
+
// this the reset would leave an interrupted run's cursor as the next start
|
|
59
|
+
// position if the products adapter ever opts out.
|
|
60
|
+
expect(resetResumePosition).toHaveBeenCalledWith('sync_akeneo', 'products', 'import', {
|
|
61
|
+
organizationId: 'org-1',
|
|
62
|
+
tenantId: 'tenant-1',
|
|
63
|
+
})
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('does not reset anything when there was nothing imported to delete', async () => {
|
|
67
|
+
const { container, em, resetResumePosition } = buildHarness([])
|
|
68
|
+
|
|
69
|
+
await deleteImportedProductsWithProgress({ container, progressJobId: 'job-1', scope: SCOPE })
|
|
70
|
+
|
|
71
|
+
expect(em.nativeDelete).not.toHaveBeenCalled()
|
|
72
|
+
expect(resetResumePosition).not.toHaveBeenCalled()
|
|
73
|
+
})
|
|
74
|
+
})
|
|
@@ -5,6 +5,7 @@ import { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
|
5
5
|
import type { ProgressService, ProgressServiceContext } from '@open-mercato/core/modules/progress/lib/progressService'
|
|
6
6
|
import { CatalogProduct } from '@open-mercato/core/modules/catalog/data/entities'
|
|
7
7
|
import { SyncCursor } from '@open-mercato/core/modules/data_sync/data/entities'
|
|
8
|
+
import type { SyncRunService } from '@open-mercato/core/modules/data_sync/lib/sync-run-service'
|
|
8
9
|
import { SyncExternalIdMapping } from '@open-mercato/core/modules/integrations/data/entities'
|
|
9
10
|
|
|
10
11
|
export const AKENEO_DELETE_IMPORTED_PRODUCTS_QUEUE = 'sync-akeneo-delete-products'
|
|
@@ -132,6 +133,7 @@ export async function deleteImportedProductsWithProgress(params: {
|
|
|
132
133
|
const em = container.resolve('em') as EntityManager
|
|
133
134
|
const commandBus = container.resolve('commandBus') as CommandBus
|
|
134
135
|
const progressService = container.resolve('progressService') as ProgressService
|
|
136
|
+
const syncRunService = container.resolve('dataSyncRunService') as SyncRunService
|
|
135
137
|
const progressContext: ProgressServiceContext = {
|
|
136
138
|
tenantId: scope.tenantId,
|
|
137
139
|
organizationId: scope.organizationId,
|
|
@@ -192,6 +194,14 @@ export async function deleteImportedProductsWithProgress(params: {
|
|
|
192
194
|
tenantId: scope.tenantId,
|
|
193
195
|
})
|
|
194
196
|
|
|
197
|
+
// Deleting the shared row only resets entity types that write one. Clear the
|
|
198
|
+
// run-scoped position too, so this stays a full reset if the products adapter
|
|
199
|
+
// ever opts out of the shared cursor.
|
|
200
|
+
await syncRunService.resetResumePosition('sync_akeneo', 'products', 'import', {
|
|
201
|
+
organizationId: scope.organizationId,
|
|
202
|
+
tenantId: scope.tenantId,
|
|
203
|
+
})
|
|
204
|
+
|
|
195
205
|
const summary = buildSummary(productIds.length, deletedProductCount)
|
|
196
206
|
await progressService.completeJob(progressJobId, { resultSummary: summary }, progressContext)
|
|
197
207
|
|