@porulle/plugin-channel-connector 0.77.1 → 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 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
- * Ruling 2026-09-22: import the hero plus the FIRST photo of each other variant, nothing more.
397
- * A "blue long dress" query must be able to show the blue variant, and a fourth photo of the red
398
- * one adds nothing the index can use. Variants are read off the images' own variant references,
399
- * so a connector that lists images against variants it does not enumerate still gets one each.
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 comes back in `deferredMedia` for the host to land later.
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
- * Ruling 2026-09-22: import the hero plus the FIRST photo of each other variant, nothing more.
517
- * A "blue long dress" query must be able to show the blue variant, and a fourth photo of the red
518
- * one adds nothing the index can use. Variants are read off the images' own variant references,
519
- * so a connector that lists images against variants it does not enumerate still gets one each.
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
- return { hero, perVariant };
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
- // Only the images the import ruling allows — the SAME `selectImportImages` the page fast path
1559
- // uses: the hero plus the first photo of each other variant. This path attached EVERY image
1560
- // the item listed, so the first converge of a product whose price changed pulled in the whole
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 comes back in `deferredMedia` for the host to land later.
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
- if (selection.perVariant.length > 0)
2706
- deferredMedia.push({ externalId: item.externalId, entityId, images: selection.perVariant });
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.77.1",
3
+ "version": "0.79.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -22,7 +22,7 @@
22
22
  "dependencies": {
23
23
  "@hono/zod-openapi": "^1.2.2",
24
24
  "hono": "^4.12.5",
25
- "@porulle/core": "0.77.1"
25
+ "@porulle/core": "0.79.0"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@types/node": "^24.5.2",
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
- * Ruling 2026-09-22: import the hero plus the FIRST photo of each other variant, nothing more.
1187
- * A "blue long dress" query must be able to show the blue variant, and a fourth photo of the red
1188
- * one adds nothing the index can use. Variants are read off the images' own variant references,
1189
- * so a connector that lists images against variants it does not enumerate still gets one each.
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
- return { hero, perVariant };
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
- // Only the images the import ruling allows — the SAME `selectImportImages` the page fast path
2404
- // uses: the hero plus the first photo of each other variant. This path attached EVERY image
2405
- // the item listed, so the first converge of a product whose price changed pulled in the whole
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 comes back in `deferredMedia` for the host to land later.
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
- if (selection.perVariant.length > 0) deferredMedia.push({ externalId: item.externalId, entityId, images: selection.perVariant });
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 };