@porulle/plugin-channel-connector 0.45.0 → 0.47.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.
- package/dist/index.d.ts +1 -1
- package/dist/service.d.ts +10 -0
- package/dist/service.js +201 -177
- package/package.json +2 -2
- package/src/index.ts +1 -0
- package/src/service.ts +44 -4
- package/dist/tsconfig.tsbuildinfo +0 -1
package/src/service.ts
CHANGED
|
@@ -293,6 +293,15 @@ export interface BackfillCatalogReport extends BackfillCounts, Record<string, un
|
|
|
293
293
|
warnings?: string[];
|
|
294
294
|
}
|
|
295
295
|
|
|
296
|
+
/**
|
|
297
|
+
* One item the converge loop could not finish. `externalId` is the merchant's id, because the
|
|
298
|
+
* entity may not exist yet and a caller reporting the failure has nothing else to name it by.
|
|
299
|
+
*/
|
|
300
|
+
export interface CatalogConvergenceFailure {
|
|
301
|
+
externalId: string;
|
|
302
|
+
error: string;
|
|
303
|
+
}
|
|
304
|
+
|
|
296
305
|
interface CatalogConvergenceStats {
|
|
297
306
|
imported: number;
|
|
298
307
|
converged: number;
|
|
@@ -304,6 +313,17 @@ interface CatalogConvergenceStats {
|
|
|
304
313
|
skipped: CatalogFieldSkip[];
|
|
305
314
|
conflicts: CatalogFieldConflict[];
|
|
306
315
|
warnings: string[];
|
|
316
|
+
/**
|
|
317
|
+
* Items that failed. The batch CONTINUES past each one and counts it in `consumed`, so the
|
|
318
|
+
* store cursor advances and a later page is still reached.
|
|
319
|
+
*
|
|
320
|
+
* It used to `return PluginErr` on the first failure, which returned before the cursor write in
|
|
321
|
+
* `importCatalog` while the items already converged stayed committed. The retry then re-fetched
|
|
322
|
+
* the same page, re-converged the same prefix and failed on the same item: one malformed product
|
|
323
|
+
* halted the rest of a merchant's catalogue permanently. Saleor calls this choice
|
|
324
|
+
* REJECT_FAILED_ROWS as against REJECT_EVERYTHING; this is the former.
|
|
325
|
+
*/
|
|
326
|
+
failures: CatalogConvergenceFailure[];
|
|
307
327
|
}
|
|
308
328
|
|
|
309
329
|
type ImportResumePosition = { pageCursor: string | null; offset: number };
|
|
@@ -2399,6 +2419,7 @@ export class ChannelConnectorService {
|
|
|
2399
2419
|
skipped?: CatalogFieldSkip[];
|
|
2400
2420
|
conflicts?: CatalogFieldConflict[];
|
|
2401
2421
|
warnings?: string[];
|
|
2422
|
+
failures?: CatalogConvergenceFailure[];
|
|
2402
2423
|
}>>;
|
|
2403
2424
|
async importCatalog(
|
|
2404
2425
|
orgId: string,
|
|
@@ -2411,6 +2432,7 @@ export class ChannelConnectorService {
|
|
|
2411
2432
|
skipped?: CatalogFieldSkip[];
|
|
2412
2433
|
conflicts?: CatalogFieldConflict[];
|
|
2413
2434
|
warnings?: string[];
|
|
2435
|
+
failures?: CatalogConvergenceFailure[];
|
|
2414
2436
|
}>>;
|
|
2415
2437
|
async importCatalog(
|
|
2416
2438
|
orgId: string,
|
|
@@ -2424,6 +2446,7 @@ export class ChannelConnectorService {
|
|
|
2424
2446
|
skipped?: CatalogFieldSkip[];
|
|
2425
2447
|
conflicts?: CatalogFieldConflict[];
|
|
2426
2448
|
warnings?: string[];
|
|
2449
|
+
failures?: CatalogConvergenceFailure[];
|
|
2427
2450
|
}>> {
|
|
2428
2451
|
const store = await this.getStoreRecord(orgId, storeId);
|
|
2429
2452
|
if (!store || store.status !== "connected") {
|
|
@@ -2459,6 +2482,7 @@ export class ChannelConnectorService {
|
|
|
2459
2482
|
...(result.value.skipped.length > 0 ? { skipped: uniqueSkipped(result.value.skipped) } : {}),
|
|
2460
2483
|
...(result.value.conflicts.length > 0 ? { conflicts: result.value.conflicts } : {}),
|
|
2461
2484
|
...(result.value.warnings.length > 0 ? { warnings: result.value.warnings } : {}),
|
|
2485
|
+
...(result.value.failures.length > 0 ? { failures: result.value.failures } : {}),
|
|
2462
2486
|
});
|
|
2463
2487
|
}
|
|
2464
2488
|
|
|
@@ -2469,6 +2493,7 @@ export class ChannelConnectorService {
|
|
|
2469
2493
|
const skipped: CatalogFieldSkip[] = [];
|
|
2470
2494
|
const conflicts: CatalogFieldConflict[] = [];
|
|
2471
2495
|
const warnings: string[] = [];
|
|
2496
|
+
const failures: CatalogConvergenceFailure[] = [];
|
|
2472
2497
|
|
|
2473
2498
|
while (remaining > 0) {
|
|
2474
2499
|
const page = await connector.importCatalog(store as ChannelStore, pageCursor ?? undefined);
|
|
@@ -2497,6 +2522,7 @@ export class ChannelConnectorService {
|
|
|
2497
2522
|
skipped.push(...result.value.skipped);
|
|
2498
2523
|
conflicts.push(...result.value.conflicts);
|
|
2499
2524
|
warnings.push(...result.value.warnings);
|
|
2525
|
+
failures.push(...result.value.failures);
|
|
2500
2526
|
offset += result.value.consumed;
|
|
2501
2527
|
|
|
2502
2528
|
if (offset >= pageItems.length) {
|
|
@@ -2529,6 +2555,9 @@ export class ChannelConnectorService {
|
|
|
2529
2555
|
...(skipped.length > 0 ? { skipped: uniqueSkipped(skipped) } : {}),
|
|
2530
2556
|
...(conflicts.length > 0 ? { conflicts } : {}),
|
|
2531
2557
|
...(warnings.length > 0 ? { warnings } : {}),
|
|
2558
|
+
// Always surfaced when non-empty. A silently dropped product is worse than the halt this
|
|
2559
|
+
// replaced: the caller must be able to see which externalIds did not land.
|
|
2560
|
+
...(failures.length > 0 ? { failures } : {}),
|
|
2532
2561
|
});
|
|
2533
2562
|
}
|
|
2534
2563
|
|
|
@@ -2716,6 +2745,7 @@ export class ChannelConnectorService {
|
|
|
2716
2745
|
skipped: [],
|
|
2717
2746
|
conflicts: [],
|
|
2718
2747
|
warnings: [],
|
|
2748
|
+
failures: [], // a dry run converges nothing, so it can fail nothing
|
|
2719
2749
|
};
|
|
2720
2750
|
const assets = await this.db.select().from(mediaAssets).where(eq(mediaAssets.organizationId, orgId));
|
|
2721
2751
|
for (const item of items) {
|
|
@@ -2863,8 +2893,10 @@ export class ChannelConnectorService {
|
|
|
2863
2893
|
const skipped: CatalogFieldSkip[] = [];
|
|
2864
2894
|
const conflicts: CatalogFieldConflict[] = [];
|
|
2865
2895
|
const warnings: string[] = [];
|
|
2896
|
+
const failures: CatalogConvergenceFailure[] = [];
|
|
2866
2897
|
for (const item of items) {
|
|
2867
2898
|
consumed += 1;
|
|
2899
|
+
try {
|
|
2868
2900
|
const remoteHash = hash(item);
|
|
2869
2901
|
const existing = await this.db
|
|
2870
2902
|
.select()
|
|
@@ -2934,7 +2966,8 @@ export class ChannelConnectorService {
|
|
|
2934
2966
|
return created.value.id;
|
|
2935
2967
|
});
|
|
2936
2968
|
} catch (error) {
|
|
2937
|
-
|
|
2969
|
+
failures.push({ externalId: item.externalId, error: error instanceof Error ? error.message : "Failed to create catalog entity." });
|
|
2970
|
+
continue;
|
|
2938
2971
|
}
|
|
2939
2972
|
isNew = true;
|
|
2940
2973
|
imported += 1;
|
|
@@ -2945,7 +2978,7 @@ export class ChannelConnectorService {
|
|
|
2945
2978
|
const ownershipBeforeSeed = await this.catalog.resolveFieldOwners(entityId, storeId);
|
|
2946
2979
|
const seedPaths = importedFieldPaths(item).filter((path) => !ownershipBeforeSeed.has(path));
|
|
2947
2980
|
const seeded = await this.catalog.seedImportedFieldOwnership(entityId, storeId, seedPaths);
|
|
2948
|
-
if (!seeded.ok)
|
|
2981
|
+
if (!seeded.ok) { failures.push({ externalId: item.externalId, error: seeded.error.message }); continue; }
|
|
2949
2982
|
for (const path of seedPaths) ownershipBeforeSeed.set(path, "store");
|
|
2950
2983
|
const owners = ownershipBeforeSeed;
|
|
2951
2984
|
const outboundEcho = entityMapping ? this.isOutboundEcho(entityMapping, item) : false;
|
|
@@ -3008,7 +3041,7 @@ export class ChannelConnectorService {
|
|
|
3008
3041
|
converged += 1;
|
|
3009
3042
|
if (Object.keys(updateInput).length > 0) {
|
|
3010
3043
|
const updated = await this.catalog.update(entityMapping.entityId, updateInput, actor, CHANNEL_CONVERGENCE_CTX);
|
|
3011
|
-
if (!updated.ok)
|
|
3044
|
+
if (!updated.ok) { failures.push({ externalId: item.externalId, error: updated.error.message }); continue; }
|
|
3012
3045
|
entityTouched = true;
|
|
3013
3046
|
}
|
|
3014
3047
|
}
|
|
@@ -3043,7 +3076,7 @@ export class ChannelConnectorService {
|
|
|
3043
3076
|
|
|
3044
3077
|
if (entityTouched) {
|
|
3045
3078
|
const revision = await this.catalog.recordEntityRevision(entityId, actor, "import");
|
|
3046
|
-
if (!revision.ok)
|
|
3079
|
+
if (!revision.ok) { failures.push({ externalId: item.externalId, error: revision.error.message }); continue; }
|
|
3047
3080
|
}
|
|
3048
3081
|
|
|
3049
3082
|
const revisionMarkers = await this.catalog.repository.findRevisionMarkers(entityId);
|
|
@@ -3089,6 +3122,12 @@ export class ChannelConnectorService {
|
|
|
3089
3122
|
eq(channelEntityMap.entityId, entityId),
|
|
3090
3123
|
eq(channelEntityMap.kind, "variant"),
|
|
3091
3124
|
));
|
|
3125
|
+
} catch (error) {
|
|
3126
|
+
// Anything the stages throw rather than returning. Same disposition: record it against the
|
|
3127
|
+
// item and carry on, so an unforeseen throw costs one product and not the remaining page.
|
|
3128
|
+
failures.push({ externalId: item.externalId, error: error instanceof Error ? error.message : String(error) });
|
|
3129
|
+
continue;
|
|
3130
|
+
}
|
|
3092
3131
|
}
|
|
3093
3132
|
return Ok({
|
|
3094
3133
|
imported,
|
|
@@ -3101,6 +3140,7 @@ export class ChannelConnectorService {
|
|
|
3101
3140
|
skipped,
|
|
3102
3141
|
conflicts,
|
|
3103
3142
|
warnings,
|
|
3143
|
+
failures,
|
|
3104
3144
|
});
|
|
3105
3145
|
}
|
|
3106
3146
|
|