@porulle/plugin-channel-connector 0.56.0 → 0.57.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 -0
- package/dist/index.js +1 -0
- package/dist/service.d.ts +2 -0
- package/dist/service.js +26 -3
- package/package.json +2 -2
- package/src/index.ts +1 -0
- package/src/service.ts +27 -3
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type ChannelConnectorPluginOptions } from "./service.js";
|
|
2
2
|
export { ABSENT_ARCHIVE_FLOOR, ABSENT_ARCHIVE_FRACTION, planAbsentArchives } from "./deletion-policy.js";
|
|
3
|
+
export { channelSyncHash } from "./service.js";
|
|
3
4
|
export type { AbsentArchivePlan } from "./deletion-policy.js";
|
|
4
5
|
export { mockChannelConnector } from "./mock-connector.js";
|
|
5
6
|
export type { MockChannelConnectorOptions } from "./mock-connector.js";
|
package/dist/index.js
CHANGED
|
@@ -8,6 +8,7 @@ import { ChannelConnectorService, catalogPushConcurrencyKey, CHANNEL_INVENTORY_M
|
|
|
8
8
|
import { buildHooks } from "./hooks.js";
|
|
9
9
|
import { oauthStateEventId, signState, verifyState } from "./oauth-state.js";
|
|
10
10
|
export { ABSENT_ARCHIVE_FLOOR, ABSENT_ARCHIVE_FRACTION, planAbsentArchives } from "./deletion-policy.js";
|
|
11
|
+
export { channelSyncHash } from "./service.js";
|
|
11
12
|
export { mockChannelConnector } from "./mock-connector.js";
|
|
12
13
|
export { ChannelConnectorService, HERO_IMAGE_BYTE_CAP, selectImportImages, CATALOG_OUTBOUND_SUPPRESSION_WINDOW_MS, CATALOG_PUSH_BATCH_SIZES, CATALOG_PUSH_MAX_ATTEMPTS, canCatalogPushTransition, canExportTransition, catalogPushConcurrencyKey, catalogPushRetryDelayMs, CHANNEL_INVENTORY_MAX_ITEMS_PER_INVOCATION, isCatalogPushBreakerOpen, } from "./service.js";
|
|
13
14
|
export { isValidCatalogMappingFieldPath, matchFieldPath, mergeCatalogFieldMapping, normalizeCatalogFieldMapping, compareCatalogFieldMappingSpecificity, providerCatalogFieldMappingDefaults, selectCatalogFieldMapping, validateCatalogMappingRow, } from "./catalog-field-mapping.js";
|
package/dist/service.d.ts
CHANGED
|
@@ -236,6 +236,8 @@ export interface CatalogPushPreviewResult {
|
|
|
236
236
|
}
|
|
237
237
|
export declare function canExportTransition(from: ExportState, to: ExportState): boolean;
|
|
238
238
|
export declare function canCatalogPushTransition(from: CatalogPushState, to: CatalogPushState): boolean;
|
|
239
|
+
/** THE sync hash: every path that records or compares a `channel_entity_map.sync_hash` uses this. */
|
|
240
|
+
export declare function channelSyncHash(value: unknown): string;
|
|
239
241
|
/**
|
|
240
242
|
* The suffix a store's product takes when its handle is already another store's slug: the first
|
|
241
243
|
* label of the store domain (`kelly-felder.myshopify.com` → `kelly-felder`), slugified.
|
package/dist/service.js
CHANGED
|
@@ -123,9 +123,30 @@ const catalogPushTransitions = {
|
|
|
123
123
|
export function canCatalogPushTransition(from, to) {
|
|
124
124
|
return catalogPushTransitions[from].includes(to);
|
|
125
125
|
}
|
|
126
|
-
|
|
127
|
-
|
|
126
|
+
/**
|
|
127
|
+
* JSON with every object's keys sorted, recursively — the one form every hash here is taken over.
|
|
128
|
+
*
|
|
129
|
+
* The sync hash was `sha256(JSON.stringify(item))`, and JSON.stringify writes keys in insertion
|
|
130
|
+
* order. The host's page consumer rebuilds each landed item in its own key order while reconcile
|
|
131
|
+
* hashes the adapter's objects in theirs, so the fast path and reconcile hashed the SAME content
|
|
132
|
+
* differently and reconcile re-converged every freshly imported product. Content, not key order,
|
|
133
|
+
* is what a hash here must answer. `undefined` values are dropped exactly as JSON.stringify drops
|
|
134
|
+
* them; array order is kept (it carries meaning: images, variants, options).
|
|
135
|
+
*/
|
|
136
|
+
function canonicalJson(value) {
|
|
137
|
+
if (Array.isArray(value))
|
|
138
|
+
return `[${value.map((entry) => (entry === undefined ? "null" : canonicalJson(entry))).join(",")}]`;
|
|
139
|
+
if (value !== null && typeof value === "object" && !(value instanceof Date)) {
|
|
140
|
+
const entries = Object.entries(value).filter(([, entry]) => entry !== undefined).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
141
|
+
return `{${entries.map(([key, entry]) => `${JSON.stringify(key)}:${canonicalJson(entry)}`).join(",")}}`;
|
|
142
|
+
}
|
|
143
|
+
return JSON.stringify(value) ?? "null";
|
|
144
|
+
}
|
|
145
|
+
/** THE sync hash: every path that records or compares a `channel_entity_map.sync_hash` uses this. */
|
|
146
|
+
export function channelSyncHash(value) {
|
|
147
|
+
return createHash("sha256").update(canonicalJson(value)).digest("hex");
|
|
128
148
|
}
|
|
149
|
+
const hash = channelSyncHash;
|
|
129
150
|
/**
|
|
130
151
|
* The suffix a store's product takes when its handle is already another store's slug: the first
|
|
131
152
|
* label of the store domain (`kelly-felder.myshopify.com` → `kelly-felder`), slugified.
|
|
@@ -2923,7 +2944,9 @@ export class ChannelConnectorService {
|
|
|
2923
2944
|
* products remain.
|
|
2924
2945
|
*/
|
|
2925
2946
|
async claimUnrecordedLinks(orgId, storeId, items) {
|
|
2926
|
-
|
|
2947
|
+
// Nothing listed, nothing to claim: no query. Otherwise one select per converge call (a batch),
|
|
2948
|
+
// never one per product.
|
|
2949
|
+
if (!items.some((item) => (item.tags?.length ?? 0) > 0 || (item.categories?.length ?? 0) > 0 || item.brand !== undefined))
|
|
2927
2950
|
return;
|
|
2928
2951
|
const unclaimed = await this.db.select({ externalId: channelEntityMap.externalId, entityId: channelEntityMap.entityId }).from(channelEntityMap).where(and(eq(channelEntityMap.organizationId, orgId), eq(channelEntityMap.storeId, storeId), eq(channelEntityMap.kind, "entity"), inArray(channelEntityMap.externalId, items.map((item) => item.externalId)), sql `not exists (select 1 from ${channelEntityLinks} where ${channelEntityLinks.storeId} = ${channelEntityMap.storeId} and ${channelEntityLinks.entityId} = ${channelEntityMap.entityId})`));
|
|
2929
2952
|
if (unclaimed.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.57.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@hono/zod-openapi": "^1.2.2",
|
|
21
21
|
"hono": "^4.12.5",
|
|
22
|
-
"@porulle/core": "0.
|
|
22
|
+
"@porulle/core": "0.57.0"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"@types/node": "^24.5.2",
|
package/src/index.ts
CHANGED
|
@@ -55,6 +55,7 @@ type ChannelRouteContext = {
|
|
|
55
55
|
};
|
|
56
56
|
|
|
57
57
|
export { ABSENT_ARCHIVE_FLOOR, ABSENT_ARCHIVE_FRACTION, planAbsentArchives } from "./deletion-policy.js";
|
|
58
|
+
export { channelSyncHash } from "./service.js";
|
|
58
59
|
export type { AbsentArchivePlan } from "./deletion-policy.js";
|
|
59
60
|
export { mockChannelConnector } from "./mock-connector.js";
|
|
60
61
|
export type { MockChannelConnectorOptions } from "./mock-connector.js";
|
package/src/service.ts
CHANGED
|
@@ -637,10 +637,32 @@ export function canCatalogPushTransition(from: CatalogPushState, to: CatalogPush
|
|
|
637
637
|
return catalogPushTransitions[from].includes(to);
|
|
638
638
|
}
|
|
639
639
|
|
|
640
|
-
|
|
641
|
-
|
|
640
|
+
/**
|
|
641
|
+
* JSON with every object's keys sorted, recursively — the one form every hash here is taken over.
|
|
642
|
+
*
|
|
643
|
+
* The sync hash was `sha256(JSON.stringify(item))`, and JSON.stringify writes keys in insertion
|
|
644
|
+
* order. The host's page consumer rebuilds each landed item in its own key order while reconcile
|
|
645
|
+
* hashes the adapter's objects in theirs, so the fast path and reconcile hashed the SAME content
|
|
646
|
+
* differently and reconcile re-converged every freshly imported product. Content, not key order,
|
|
647
|
+
* is what a hash here must answer. `undefined` values are dropped exactly as JSON.stringify drops
|
|
648
|
+
* them; array order is kept (it carries meaning: images, variants, options).
|
|
649
|
+
*/
|
|
650
|
+
function canonicalJson(value: unknown): string {
|
|
651
|
+
if (Array.isArray(value)) return `[${value.map((entry) => (entry === undefined ? "null" : canonicalJson(entry))).join(",")}]`;
|
|
652
|
+
if (value !== null && typeof value === "object" && !(value instanceof Date)) {
|
|
653
|
+
const entries = Object.entries(value).filter(([, entry]) => entry !== undefined).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
654
|
+
return `{${entries.map(([key, entry]) => `${JSON.stringify(key)}:${canonicalJson(entry)}`).join(",")}}`;
|
|
655
|
+
}
|
|
656
|
+
return JSON.stringify(value) ?? "null";
|
|
642
657
|
}
|
|
643
658
|
|
|
659
|
+
/** THE sync hash: every path that records or compares a `channel_entity_map.sync_hash` uses this. */
|
|
660
|
+
export function channelSyncHash(value: unknown): string {
|
|
661
|
+
return createHash("sha256").update(canonicalJson(value)).digest("hex");
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
const hash = channelSyncHash;
|
|
665
|
+
|
|
644
666
|
/**
|
|
645
667
|
* The suffix a store's product takes when its handle is already another store's slug: the first
|
|
646
668
|
* label of the store domain (`kelly-felder.myshopify.com` → `kelly-felder`), slugified.
|
|
@@ -3914,7 +3936,9 @@ export class ChannelConnectorService {
|
|
|
3914
3936
|
* products remain.
|
|
3915
3937
|
*/
|
|
3916
3938
|
private async claimUnrecordedLinks(orgId: string, storeId: string, items: ChannelCatalogItem[]): Promise<void> {
|
|
3917
|
-
|
|
3939
|
+
// Nothing listed, nothing to claim: no query. Otherwise one select per converge call (a batch),
|
|
3940
|
+
// never one per product.
|
|
3941
|
+
if (!items.some((item) => (item.tags?.length ?? 0) > 0 || (item.categories?.length ?? 0) > 0 || item.brand !== undefined)) return;
|
|
3918
3942
|
const unclaimed = await this.db.select({ externalId: channelEntityMap.externalId, entityId: channelEntityMap.entityId }).from(channelEntityMap).where(and(
|
|
3919
3943
|
eq(channelEntityMap.organizationId, orgId),
|
|
3920
3944
|
eq(channelEntityMap.storeId, storeId),
|