@porulle/plugin-channel-connector 0.78.0 → 0.79.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/service.d.ts +9 -5
- package/dist/service.js +28 -14
- package/package.json +4 -4
- package/src/service.ts +28 -13
package/dist/service.d.ts
CHANGED
|
@@ -391,12 +391,16 @@ export interface ImportImageSelection {
|
|
|
391
391
|
hero: ChannelCatalogImage | null;
|
|
392
392
|
/** In variant order; one image per variant the hero does not cover; no url twice. */
|
|
393
393
|
perVariant: ChannelCatalogImage[];
|
|
394
|
+
/** The store's further photos in its order, as entity-level `gallery` images; no url twice. */
|
|
395
|
+
gallery: ChannelCatalogImage[];
|
|
394
396
|
}
|
|
395
397
|
/**
|
|
396
|
-
*
|
|
397
|
-
*
|
|
398
|
-
*
|
|
399
|
-
*
|
|
398
|
+
* The hero, then the first photo of each other variant (a "blue long dress" query must be able to
|
|
399
|
+
* show the blue variant), then the store's further photos as the gallery, up to
|
|
400
|
+
* `IMPORT_IMAGE_LIMIT` in all. Agent feeds publish the gallery as additional images and enrichment
|
|
401
|
+
* reads it; only the hero is embedded, so a gallery photo costs an upload and no model call.
|
|
402
|
+
* Variants are read off the images' own variant references, so a connector that lists images
|
|
403
|
+
* against variants it does not enumerate still gets one each.
|
|
400
404
|
*/
|
|
401
405
|
export declare function selectImportImages(item: ChannelCatalogItem): ImportImageSelection;
|
|
402
406
|
export declare class ChannelConnectorService {
|
|
@@ -598,7 +602,7 @@ export declare class ChannelConnectorService {
|
|
|
598
602
|
*
|
|
599
603
|
* Media: only each new item's hero is fetched here, streamed under `HERO_IMAGE_BYTE_CAP`, and
|
|
600
604
|
* linked at entity level as `primary` plus to the variants it shows. The first photo of every
|
|
601
|
-
* other variant
|
|
605
|
+
* other variant, then the bounded gallery, come back in `deferredMedia` for the host to land later.
|
|
602
606
|
*/
|
|
603
607
|
convergeCatalogPage(orgId: string, storeId: string, rawItems: ChannelCatalogItem[], actor: Actor): Promise<PluginResult<CatalogPageConvergence>>;
|
|
604
608
|
private importHeroes;
|
package/dist/service.js
CHANGED
|
@@ -509,20 +509,24 @@ function redactStore(store) {
|
|
|
509
509
|
* stored, and the product still lands — the index reads text first and media later.
|
|
510
510
|
*/
|
|
511
511
|
export const HERO_IMAGE_BYTE_CAP = 1024 * 1024;
|
|
512
|
+
/** The hero, the variant photos and the gallery together never exceed this — what a projection reads. */
|
|
513
|
+
const IMPORT_IMAGE_LIMIT = 6;
|
|
512
514
|
function imageOrder(a, b) {
|
|
513
515
|
return (a.sortOrder ?? 0) - (b.sortOrder ?? 0);
|
|
514
516
|
}
|
|
515
517
|
/**
|
|
516
|
-
*
|
|
517
|
-
*
|
|
518
|
-
*
|
|
519
|
-
*
|
|
518
|
+
* The hero, then the first photo of each other variant (a "blue long dress" query must be able to
|
|
519
|
+
* show the blue variant), then the store's further photos as the gallery, up to
|
|
520
|
+
* `IMPORT_IMAGE_LIMIT` in all. Agent feeds publish the gallery as additional images and enrichment
|
|
521
|
+
* reads it; only the hero is embedded, so a gallery photo costs an upload and no model call.
|
|
522
|
+
* Variants are read off the images' own variant references, so a connector that lists images
|
|
523
|
+
* against variants it does not enumerate still gets one each.
|
|
520
524
|
*/
|
|
521
525
|
export function selectImportImages(item) {
|
|
522
526
|
const images = [...(item.images ?? [])].sort(imageOrder);
|
|
523
527
|
const hero = images.find((image) => image.role === "primary") ?? images[0] ?? null;
|
|
524
528
|
if (!hero)
|
|
525
|
-
return { hero: null, perVariant: [] };
|
|
529
|
+
return { hero: null, perVariant: [], gallery: [] };
|
|
526
530
|
const covered = new Set(hero.variantExternalIds ?? []);
|
|
527
531
|
const usedUrls = new Set([hero.url]);
|
|
528
532
|
const perVariant = [];
|
|
@@ -539,7 +543,17 @@ export function selectImportImages(item) {
|
|
|
539
543
|
usedUrls.add(image.url);
|
|
540
544
|
perVariant.push(image);
|
|
541
545
|
}
|
|
542
|
-
|
|
546
|
+
const gallery = [];
|
|
547
|
+
for (const image of images) {
|
|
548
|
+
if (1 + perVariant.length + gallery.length >= IMPORT_IMAGE_LIMIT)
|
|
549
|
+
break;
|
|
550
|
+
if (usedUrls.has(image.url))
|
|
551
|
+
continue;
|
|
552
|
+
usedUrls.add(image.url);
|
|
553
|
+
// A further photo of a variant already shown is a product photo, not that variant's image.
|
|
554
|
+
gallery.push({ ...image, role: "gallery", variantExternalIds: [] });
|
|
555
|
+
}
|
|
556
|
+
return { hero, perVariant, gallery };
|
|
543
557
|
}
|
|
544
558
|
/** Streams a response and refuses mid-stream past `cap`; a lying `content-length` cannot get around it. */
|
|
545
559
|
async function fetchBounded(url, cap) {
|
|
@@ -1555,12 +1569,11 @@ export class ChannelConnectorService {
|
|
|
1555
1569
|
async applyMedia(orgId, entityId, item, variantIds, actor, warnings, owners) {
|
|
1556
1570
|
// Uploads what is missing and PLANS the entity's media links; `commitEntityLinks` writes them.
|
|
1557
1571
|
//
|
|
1558
|
-
//
|
|
1559
|
-
//
|
|
1560
|
-
//
|
|
1561
|
-
// gallery the fast path had deliberately left out: an upload, an embed and a bump per photo.
|
|
1572
|
+
// The SAME `selectImportImages` the page fast path uses — hero, variant photos, bounded gallery.
|
|
1573
|
+
// This path once attached EVERY image the item listed, so a product's first price change
|
|
1574
|
+
// pulled in photos the fast path had left out: an upload and an entity bump per photo.
|
|
1562
1575
|
const selection = selectImportImages(item);
|
|
1563
|
-
const images = selection.hero === null ? [] : [selection.hero, ...selection.perVariant];
|
|
1576
|
+
const images = selection.hero === null ? [] : [selection.hero, ...selection.perVariant, ...selection.gallery];
|
|
1564
1577
|
const externalIds = [...new Set(images.map((image) => image.externalId).filter((id) => id != null))];
|
|
1565
1578
|
const urlHashes = [...new Set(images.map((image) => hash(image.url)))];
|
|
1566
1579
|
const keyPredicates = [];
|
|
@@ -2550,7 +2563,7 @@ export class ChannelConnectorService {
|
|
|
2550
2563
|
*
|
|
2551
2564
|
* Media: only each new item's hero is fetched here, streamed under `HERO_IMAGE_BYTE_CAP`, and
|
|
2552
2565
|
* linked at entity level as `primary` plus to the variants it shows. The first photo of every
|
|
2553
|
-
* other variant
|
|
2566
|
+
* other variant, then the bounded gallery, come back in `deferredMedia` for the host to land later.
|
|
2554
2567
|
*/
|
|
2555
2568
|
async convergeCatalogPage(orgId, storeId, rawItems, actor) {
|
|
2556
2569
|
// A host may hand items it never read through this service's intake; the rule is idempotent.
|
|
@@ -2702,8 +2715,9 @@ export class ChannelConnectorService {
|
|
|
2702
2715
|
const deferredMedia = [];
|
|
2703
2716
|
const selections = createdItems.flatMap(({ item, entityId, variantIds }) => {
|
|
2704
2717
|
const selection = selectImportImages(item);
|
|
2705
|
-
|
|
2706
|
-
|
|
2718
|
+
const deferred = [...selection.perVariant, ...selection.gallery];
|
|
2719
|
+
if (deferred.length > 0)
|
|
2720
|
+
deferredMedia.push({ externalId: item.externalId, entityId, images: deferred });
|
|
2707
2721
|
return selection.hero ? [{ item, entityId, variantIds, hero: selection.hero }] : [];
|
|
2708
2722
|
});
|
|
2709
2723
|
if (selections.length === 0)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@porulle/plugin-channel-connector",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.79.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -22,15 +22,15 @@
|
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"@hono/zod-openapi": "^1.2.2",
|
|
24
24
|
"hono": "^4.12.5",
|
|
25
|
-
"@porulle/core": "0.
|
|
25
|
+
"@porulle/core": "0.79.0"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@types/node": "^24.5.2",
|
|
29
29
|
"eslint": "^9.39.1",
|
|
30
30
|
"typescript": "5.9.2",
|
|
31
31
|
"vitest": "^3.2.4",
|
|
32
|
-
"@porulle/
|
|
33
|
-
"@porulle/
|
|
32
|
+
"@porulle/typescript-config": "0.1.0",
|
|
33
|
+
"@porulle/eslint-config": "0.1.0"
|
|
34
34
|
},
|
|
35
35
|
"publishConfig": {
|
|
36
36
|
"access": "public"
|
package/src/service.ts
CHANGED
|
@@ -1176,22 +1176,29 @@ export interface ImportImageSelection {
|
|
|
1176
1176
|
hero: ChannelCatalogImage | null;
|
|
1177
1177
|
/** In variant order; one image per variant the hero does not cover; no url twice. */
|
|
1178
1178
|
perVariant: ChannelCatalogImage[];
|
|
1179
|
+
/** The store's further photos in its order, as entity-level `gallery` images; no url twice. */
|
|
1180
|
+
gallery: ChannelCatalogImage[];
|
|
1179
1181
|
}
|
|
1180
1182
|
|
|
1183
|
+
/** The hero, the variant photos and the gallery together never exceed this — what a projection reads. */
|
|
1184
|
+
const IMPORT_IMAGE_LIMIT = 6;
|
|
1185
|
+
|
|
1181
1186
|
function imageOrder(a: ChannelCatalogImage, b: ChannelCatalogImage): number {
|
|
1182
1187
|
return (a.sortOrder ?? 0) - (b.sortOrder ?? 0);
|
|
1183
1188
|
}
|
|
1184
1189
|
|
|
1185
1190
|
/**
|
|
1186
|
-
*
|
|
1187
|
-
*
|
|
1188
|
-
*
|
|
1189
|
-
*
|
|
1191
|
+
* The hero, then the first photo of each other variant (a "blue long dress" query must be able to
|
|
1192
|
+
* show the blue variant), then the store's further photos as the gallery, up to
|
|
1193
|
+
* `IMPORT_IMAGE_LIMIT` in all. Agent feeds publish the gallery as additional images and enrichment
|
|
1194
|
+
* reads it; only the hero is embedded, so a gallery photo costs an upload and no model call.
|
|
1195
|
+
* Variants are read off the images' own variant references, so a connector that lists images
|
|
1196
|
+
* against variants it does not enumerate still gets one each.
|
|
1190
1197
|
*/
|
|
1191
1198
|
export function selectImportImages(item: ChannelCatalogItem): ImportImageSelection {
|
|
1192
1199
|
const images = [...(item.images ?? [])].sort(imageOrder);
|
|
1193
1200
|
const hero = images.find((image) => image.role === "primary") ?? images[0] ?? null;
|
|
1194
|
-
if (!hero) return { hero: null, perVariant: [] };
|
|
1201
|
+
if (!hero) return { hero: null, perVariant: [], gallery: [] };
|
|
1195
1202
|
const covered = new Set(hero.variantExternalIds ?? []);
|
|
1196
1203
|
const usedUrls = new Set([hero.url]);
|
|
1197
1204
|
const perVariant: ChannelCatalogImage[] = [];
|
|
@@ -1205,7 +1212,15 @@ export function selectImportImages(item: ChannelCatalogItem): ImportImageSelecti
|
|
|
1205
1212
|
usedUrls.add(image.url);
|
|
1206
1213
|
perVariant.push(image);
|
|
1207
1214
|
}
|
|
1208
|
-
|
|
1215
|
+
const gallery: ChannelCatalogImage[] = [];
|
|
1216
|
+
for (const image of images) {
|
|
1217
|
+
if (1 + perVariant.length + gallery.length >= IMPORT_IMAGE_LIMIT) break;
|
|
1218
|
+
if (usedUrls.has(image.url)) continue;
|
|
1219
|
+
usedUrls.add(image.url);
|
|
1220
|
+
// A further photo of a variant already shown is a product photo, not that variant's image.
|
|
1221
|
+
gallery.push({ ...image, role: "gallery", variantExternalIds: [] });
|
|
1222
|
+
}
|
|
1223
|
+
return { hero, perVariant, gallery };
|
|
1209
1224
|
}
|
|
1210
1225
|
|
|
1211
1226
|
type BoundedFetch =
|
|
@@ -2400,12 +2415,11 @@ export class ChannelConnectorService {
|
|
|
2400
2415
|
}>> {
|
|
2401
2416
|
// Uploads what is missing and PLANS the entity's media links; `commitEntityLinks` writes them.
|
|
2402
2417
|
//
|
|
2403
|
-
//
|
|
2404
|
-
//
|
|
2405
|
-
//
|
|
2406
|
-
// gallery the fast path had deliberately left out: an upload, an embed and a bump per photo.
|
|
2418
|
+
// The SAME `selectImportImages` the page fast path uses — hero, variant photos, bounded gallery.
|
|
2419
|
+
// This path once attached EVERY image the item listed, so a product's first price change
|
|
2420
|
+
// pulled in photos the fast path had left out: an upload and an entity bump per photo.
|
|
2407
2421
|
const selection = selectImportImages(item);
|
|
2408
|
-
const images = selection.hero === null ? [] : [selection.hero, ...selection.perVariant];
|
|
2422
|
+
const images = selection.hero === null ? [] : [selection.hero, ...selection.perVariant, ...selection.gallery];
|
|
2409
2423
|
const externalIds = [...new Set(images.map((image) => image.externalId).filter((id): id is string => id != null))];
|
|
2410
2424
|
const urlHashes = [...new Set(images.map((image) => hash(image.url)))];
|
|
2411
2425
|
const keyPredicates = [];
|
|
@@ -3501,7 +3515,7 @@ export class ChannelConnectorService {
|
|
|
3501
3515
|
*
|
|
3502
3516
|
* Media: only each new item's hero is fetched here, streamed under `HERO_IMAGE_BYTE_CAP`, and
|
|
3503
3517
|
* linked at entity level as `primary` plus to the variants it shows. The first photo of every
|
|
3504
|
-
* other variant
|
|
3518
|
+
* other variant, then the bounded gallery, come back in `deferredMedia` for the host to land later.
|
|
3505
3519
|
*/
|
|
3506
3520
|
async convergeCatalogPage(
|
|
3507
3521
|
orgId: string,
|
|
@@ -3669,7 +3683,8 @@ export class ChannelConnectorService {
|
|
|
3669
3683
|
const deferredMedia: CatalogDeferredMedia[] = [];
|
|
3670
3684
|
const selections = createdItems.flatMap(({ item, entityId, variantIds }) => {
|
|
3671
3685
|
const selection = selectImportImages(item);
|
|
3672
|
-
|
|
3686
|
+
const deferred = [...selection.perVariant, ...selection.gallery];
|
|
3687
|
+
if (deferred.length > 0) deferredMedia.push({ externalId: item.externalId, entityId, images: deferred });
|
|
3673
3688
|
return selection.hero ? [{ item, entityId, variantIds, hero: selection.hero }] : [];
|
|
3674
3689
|
});
|
|
3675
3690
|
if (selections.length === 0) return { heroesImported: 0, mediaFailures, deferredMedia };
|