@porulle/plugin-channel-connector 0.45.0 → 0.46.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/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/index.ts +1 -0
- package/src/service.ts +44 -4
package/dist/index.d.ts
CHANGED
|
@@ -22,7 +22,7 @@ export declare const CHANNEL_IMPORT_MAX_ITEMS_PER_INVOCATION = 20;
|
|
|
22
22
|
*/
|
|
23
23
|
export declare const CHANNEL_MAX_BATCHES_PER_SWEEP = 5000;
|
|
24
24
|
export { signState, verifyState } from "./oauth-state.js";
|
|
25
|
-
export type { BackfillCatalogOptions, BackfillCatalogReport, BuildCatalogPushItemsOptions, BuildCatalogPushItemsResult, CatalogPushAssemblyField, CatalogPushAssemblyImage, CatalogPushAssemblyItem, CatalogPushPreviewBefore, CatalogPushPreviewBeforeStatus, CatalogPushPreviewDiff, CatalogPushPreviewItem, CatalogPushPreviewResult, CatalogPushPreviewUnavailable, PushCatalogToStoreResult, CatalogPushJobResult, CatalogFieldConflict, CatalogFieldSkip, CatalogPushFieldSkip, CatalogPushSkipReason, CatalogConflictState, CatalogWriteSettings, ChannelComplianceData, ChannelConnectorPluginOptions, ChannelStockLine, ExportState, PublicConnectedStore, ReconcileReport, } from "./service.js";
|
|
25
|
+
export type { BackfillCatalogOptions, BackfillCatalogReport, BuildCatalogPushItemsOptions, BuildCatalogPushItemsResult, CatalogPushAssemblyField, CatalogPushAssemblyImage, CatalogPushAssemblyItem, CatalogPushPreviewBefore, CatalogPushPreviewBeforeStatus, CatalogPushPreviewDiff, CatalogPushPreviewItem, CatalogPushPreviewResult, CatalogPushPreviewUnavailable, PushCatalogToStoreResult, CatalogPushJobResult, CatalogConvergenceFailure, CatalogFieldConflict, CatalogFieldSkip, CatalogPushFieldSkip, CatalogPushSkipReason, CatalogConflictState, CatalogWriteSettings, ChannelComplianceData, ChannelConnectorPluginOptions, ChannelStockLine, ExportState, PublicConnectedStore, ReconcileReport, } from "./service.js";
|
|
26
26
|
export type { OAuthStatePayload, OAuthStateResult } from "./oauth-state.js";
|
|
27
27
|
export type { ChannelCatalogPush, ChannelCatalogPushEvent, ChannelCatalogConflict, ChannelCatalogConflictEvent, ChannelEntityMapEntry, ChannelExportEvent, ChannelOrderExport, ChannelRefundEvent, ChannelRefundRequest, ConnectedStore, } from "./schema.js";
|
|
28
28
|
export declare function channelConnectorPlugin(options?: ChannelConnectorPluginOptions): import("@porulle/core").CommercePlugin;
|
package/dist/service.d.ts
CHANGED
|
@@ -165,6 +165,14 @@ export interface BackfillCatalogReport extends BackfillCounts, Record<string, un
|
|
|
165
165
|
conflicts?: CatalogFieldConflict[];
|
|
166
166
|
warnings?: string[];
|
|
167
167
|
}
|
|
168
|
+
/**
|
|
169
|
+
* One item the converge loop could not finish. `externalId` is the merchant's id, because the
|
|
170
|
+
* entity may not exist yet and a caller reporting the failure has nothing else to name it by.
|
|
171
|
+
*/
|
|
172
|
+
export interface CatalogConvergenceFailure {
|
|
173
|
+
externalId: string;
|
|
174
|
+
error: string;
|
|
175
|
+
}
|
|
168
176
|
export interface BackfillCatalogOptions {
|
|
169
177
|
dryRun?: boolean;
|
|
170
178
|
resume?: boolean;
|
|
@@ -282,6 +290,7 @@ export declare class ChannelConnectorService {
|
|
|
282
290
|
skipped?: CatalogFieldSkip[];
|
|
283
291
|
conflicts?: CatalogFieldConflict[];
|
|
284
292
|
warnings?: string[];
|
|
293
|
+
failures?: CatalogConvergenceFailure[];
|
|
285
294
|
}>>;
|
|
286
295
|
importCatalog(orgId: string, storeId: string, actor: Actor, options?: undefined): Promise<PluginResult<{
|
|
287
296
|
imported: number;
|
|
@@ -289,6 +298,7 @@ export declare class ChannelConnectorService {
|
|
|
289
298
|
skipped?: CatalogFieldSkip[];
|
|
290
299
|
conflicts?: CatalogFieldConflict[];
|
|
291
300
|
warnings?: string[];
|
|
301
|
+
failures?: CatalogConvergenceFailure[];
|
|
292
302
|
}>>;
|
|
293
303
|
private promoteLegacyAttributes;
|
|
294
304
|
private saveBackfillState;
|
package/dist/service.js
CHANGED
|
@@ -1722,6 +1722,7 @@ export class ChannelConnectorService {
|
|
|
1722
1722
|
...(result.value.skipped.length > 0 ? { skipped: uniqueSkipped(result.value.skipped) } : {}),
|
|
1723
1723
|
...(result.value.conflicts.length > 0 ? { conflicts: result.value.conflicts } : {}),
|
|
1724
1724
|
...(result.value.warnings.length > 0 ? { warnings: result.value.warnings } : {}),
|
|
1725
|
+
...(result.value.failures.length > 0 ? { failures: result.value.failures } : {}),
|
|
1725
1726
|
});
|
|
1726
1727
|
}
|
|
1727
1728
|
let { pageCursor, offset } = parseImportResumePosition(store.catalogCursor);
|
|
@@ -1731,6 +1732,7 @@ export class ChannelConnectorService {
|
|
|
1731
1732
|
const skipped = [];
|
|
1732
1733
|
const conflicts = [];
|
|
1733
1734
|
const warnings = [];
|
|
1735
|
+
const failures = [];
|
|
1734
1736
|
while (remaining > 0) {
|
|
1735
1737
|
const page = await connector.importCatalog(store, pageCursor ?? undefined);
|
|
1736
1738
|
if (!page.ok)
|
|
@@ -1756,6 +1758,7 @@ export class ChannelConnectorService {
|
|
|
1756
1758
|
skipped.push(...result.value.skipped);
|
|
1757
1759
|
conflicts.push(...result.value.conflicts);
|
|
1758
1760
|
warnings.push(...result.value.warnings);
|
|
1761
|
+
failures.push(...result.value.failures);
|
|
1759
1762
|
offset += result.value.consumed;
|
|
1760
1763
|
if (offset >= pageItems.length) {
|
|
1761
1764
|
if (page.value.nextCursor) {
|
|
@@ -1787,6 +1790,9 @@ export class ChannelConnectorService {
|
|
|
1787
1790
|
...(skipped.length > 0 ? { skipped: uniqueSkipped(skipped) } : {}),
|
|
1788
1791
|
...(conflicts.length > 0 ? { conflicts } : {}),
|
|
1789
1792
|
...(warnings.length > 0 ? { warnings } : {}),
|
|
1793
|
+
// Always surfaced when non-empty. A silently dropped product is worse than the halt this
|
|
1794
|
+
// replaced: the caller must be able to see which externalIds did not land.
|
|
1795
|
+
...(failures.length > 0 ? { failures } : {}),
|
|
1790
1796
|
});
|
|
1791
1797
|
}
|
|
1792
1798
|
async promoteLegacyAttributes(orgId, storeId, actor, dryRun) {
|
|
@@ -1951,6 +1957,7 @@ export class ChannelConnectorService {
|
|
|
1951
1957
|
skipped: [],
|
|
1952
1958
|
conflicts: [],
|
|
1953
1959
|
warnings: [],
|
|
1960
|
+
failures: [], // a dry run converges nothing, so it can fail nothing
|
|
1954
1961
|
};
|
|
1955
1962
|
const assets = await this.db.select().from(mediaAssets).where(eq(mediaAssets.organizationId, orgId));
|
|
1956
1963
|
for (const item of items) {
|
|
@@ -2082,196 +2089,212 @@ export class ChannelConnectorService {
|
|
|
2082
2089
|
const skipped = [];
|
|
2083
2090
|
const conflicts = [];
|
|
2084
2091
|
const warnings = [];
|
|
2092
|
+
const failures = [];
|
|
2085
2093
|
for (const item of items) {
|
|
2086
2094
|
consumed += 1;
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
.
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2095
|
+
try {
|
|
2096
|
+
const remoteHash = hash(item);
|
|
2097
|
+
const existing = await this.db
|
|
2098
|
+
.select()
|
|
2099
|
+
.from(channelEntityMap)
|
|
2100
|
+
.where(and(eq(channelEntityMap.organizationId, orgId), eq(channelEntityMap.storeId, storeId), eq(channelEntityMap.kind, "entity"), eq(channelEntityMap.externalId, item.externalId)));
|
|
2101
|
+
let entityMapping = existing.find((entry) => entry.kind === "entity");
|
|
2102
|
+
let entityId;
|
|
2103
|
+
let isNew = false;
|
|
2104
|
+
let adoptedOrphan = false;
|
|
2105
|
+
let entityTouched = false;
|
|
2106
|
+
let existingEntity;
|
|
2107
|
+
if (entityMapping) {
|
|
2108
|
+
const [entity] = await this.db.select().from(sellableEntities).where(and(eq(sellableEntities.organizationId, orgId), eq(sellableEntities.id, entityMapping.entityId)));
|
|
2109
|
+
if (!entity) {
|
|
2110
|
+
await this.db.delete(channelEntityMap).where(and(eq(channelEntityMap.organizationId, orgId), eq(channelEntityMap.storeId, storeId), eq(channelEntityMap.entityId, entityMapping.entityId)));
|
|
2111
|
+
entityMapping = undefined;
|
|
2112
|
+
}
|
|
2113
|
+
else {
|
|
2114
|
+
entityId = entityMapping.entityId;
|
|
2115
|
+
existingEntity = entity;
|
|
2116
|
+
}
|
|
2103
2117
|
}
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2118
|
+
if (entityId === undefined) {
|
|
2119
|
+
const [orphan] = await this.db.select().from(sellableEntities).where(and(eq(sellableEntities.organizationId, orgId), eq(sellableEntities.sourceStoreId, storeId), eq(sellableEntities.slug, item.slug)));
|
|
2120
|
+
if (orphan) {
|
|
2121
|
+
entityId = orphan.id;
|
|
2122
|
+
existingEntity = orphan;
|
|
2123
|
+
adoptedOrphan = true;
|
|
2124
|
+
}
|
|
2125
|
+
else {
|
|
2126
|
+
const status = item.status;
|
|
2127
|
+
try {
|
|
2128
|
+
entityId = await this.transact(async (tx) => {
|
|
2129
|
+
const txContext = createTxContext(tx, { actor });
|
|
2130
|
+
const created = await this.catalog.create({
|
|
2131
|
+
type: "product",
|
|
2132
|
+
slug: item.slug,
|
|
2133
|
+
sourceStoreId: storeId,
|
|
2134
|
+
metadata: mergeMetadata(undefined, item.metadata ?? {}),
|
|
2135
|
+
...(status !== undefined ? { status, isVisible: status === "active" } : {}),
|
|
2136
|
+
}, actor, txContext);
|
|
2137
|
+
if (!created.ok)
|
|
2138
|
+
throw new Error(created.error.message);
|
|
2139
|
+
await tx.insert(channelEntityMap).values({
|
|
2140
|
+
organizationId: orgId,
|
|
2141
|
+
storeId,
|
|
2142
|
+
kind: "entity",
|
|
2143
|
+
externalId: item.externalId,
|
|
2144
|
+
entityId: created.value.id,
|
|
2145
|
+
syncHash: PENDING_ENTITY_MAP_SYNC_HASH,
|
|
2146
|
+
heldFieldPaths: [],
|
|
2147
|
+
forcedPushFieldPaths: [],
|
|
2148
|
+
});
|
|
2149
|
+
return created.value.id;
|
|
2150
|
+
});
|
|
2151
|
+
}
|
|
2152
|
+
catch (error) {
|
|
2153
|
+
failures.push({ externalId: item.externalId, error: error instanceof Error ? error.message : "Failed to create catalog entity." });
|
|
2154
|
+
continue;
|
|
2155
|
+
}
|
|
2156
|
+
isNew = true;
|
|
2157
|
+
imported += 1;
|
|
2158
|
+
entityTouched = true;
|
|
2159
|
+
}
|
|
2107
2160
|
}
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
const
|
|
2111
|
-
if (
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
adoptedOrphan = true;
|
|
2161
|
+
const ownershipBeforeSeed = await this.catalog.resolveFieldOwners(entityId, storeId);
|
|
2162
|
+
const seedPaths = importedFieldPaths(item).filter((path) => !ownershipBeforeSeed.has(path));
|
|
2163
|
+
const seeded = await this.catalog.seedImportedFieldOwnership(entityId, storeId, seedPaths);
|
|
2164
|
+
if (!seeded.ok) {
|
|
2165
|
+
failures.push({ externalId: item.externalId, error: seeded.error.message });
|
|
2166
|
+
continue;
|
|
2115
2167
|
}
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2168
|
+
for (const path of seedPaths)
|
|
2169
|
+
ownershipBeforeSeed.set(path, "store");
|
|
2170
|
+
const owners = ownershipBeforeSeed;
|
|
2171
|
+
const outboundEcho = entityMapping ? this.isOutboundEcho(entityMapping, item) : false;
|
|
2172
|
+
const remoteChanged = entityMapping === undefined || entityMapping.syncHash !== remoteHash;
|
|
2173
|
+
// An unchanged remote item writes nothing and advances no baseline:
|
|
2174
|
+
// converging a stale replay would revert local edits to shared and
|
|
2175
|
+
// unowned fields that the store never actually changed.
|
|
2176
|
+
if (!force && !remoteChanged && existingEntity && existingEntity.status !== "archived") {
|
|
2177
|
+
continue;
|
|
2178
|
+
}
|
|
2179
|
+
const shared = existingEntity
|
|
2180
|
+
? await this.detectSharedConflicts(entityId, storeId, existingEntity, entityMapping, item, owners, importedFieldPaths(item), remoteHash, outboundEcho ? { certifiedPaths: new Set(entityMapping?.outboundFieldPaths ?? []) } : undefined)
|
|
2181
|
+
: { paths: [], conflicts: [] };
|
|
2182
|
+
const persistedConflicts = await this.persistCatalogConflicts(orgId, shared.conflicts, requireUserId(actor));
|
|
2183
|
+
if (!persistedConflicts.ok)
|
|
2184
|
+
return persistedConflicts;
|
|
2185
|
+
const owned = this.filterOwnedFields(item, owners);
|
|
2186
|
+
const heldSharedPaths = [...new Set([...(entityMapping?.heldFieldPaths ?? []), ...shared.paths])];
|
|
2187
|
+
// A newly held path revokes any force left from an earlier resolution of
|
|
2188
|
+
// that same path: the force was the operator's answer to a question that
|
|
2189
|
+
// has since been asked again, and it must not pre-empt the new one.
|
|
2190
|
+
const survivingForcedPaths = (entityMapping?.forcedPushFieldPaths ?? []).filter((path) => !heldSharedPaths.includes(path));
|
|
2191
|
+
const held = this.filterConflictingFields(owned.writable, heldSharedPaths);
|
|
2192
|
+
const writable = held.writable;
|
|
2193
|
+
const blockedPaths = new Set([...owned.skipped, ...heldSharedPaths]);
|
|
2194
|
+
skipped.push(...owned.skipped.map((fieldPath) => ({ entityId, fieldPath })));
|
|
2195
|
+
conflicts.push(...shared.conflicts.map(({ platformValue: _platformValue, storeValue: _storeValue, ...conflict }) => conflict));
|
|
2196
|
+
for (const conflict of shared.conflicts) {
|
|
2197
|
+
warnings.push(`Held shared field conflict for entity "${conflict.entityId}", store "${conflict.storeId}", field "${conflict.fieldPath}" (local ${conflict.localValueSummary}, remote ${conflict.remoteValueSummary}).`);
|
|
2198
|
+
}
|
|
2199
|
+
if (existingEntity && entityMapping) {
|
|
2200
|
+
const remoteMetadata = mergeMetadata(existingEntity.metadata, writable.metadata ?? {});
|
|
2201
|
+
const remoteStatus = ownerAllows(owners, "entity.status") && !blockedPaths.has("entity.status")
|
|
2202
|
+
? writable.status ?? (existingEntity.status === "archived" ? "active" : undefined)
|
|
2203
|
+
: undefined;
|
|
2204
|
+
const updateInput = {};
|
|
2205
|
+
if (ownerAllows(owners, "entity.slug") && !blockedPaths.has("entity.slug") && existingEntity.slug !== writable.slug) {
|
|
2206
|
+
updateInput.slug = writable.slug;
|
|
2207
|
+
}
|
|
2208
|
+
if (hash(remoteMetadata) !== hash(existingEntity.metadata ?? {}))
|
|
2209
|
+
updateInput.metadata = remoteMetadata;
|
|
2210
|
+
if (remoteStatus !== undefined && !blockedPaths.has("entity.status") && remoteStatus !== existingEntity.status) {
|
|
2211
|
+
updateInput.status = remoteStatus;
|
|
2212
|
+
updateInput.isVisible = remoteStatus === "active";
|
|
2142
2213
|
}
|
|
2143
|
-
|
|
2144
|
-
|
|
2214
|
+
const shouldUpdate = force
|
|
2215
|
+
? Object.keys(updateInput).length > 0
|
|
2216
|
+
: remoteChanged || existingEntity.status === "archived";
|
|
2217
|
+
if (shouldUpdate) {
|
|
2218
|
+
converged += 1;
|
|
2219
|
+
if (Object.keys(updateInput).length > 0) {
|
|
2220
|
+
const updated = await this.catalog.update(entityMapping.entityId, updateInput, actor, CHANNEL_CONVERGENCE_CTX);
|
|
2221
|
+
if (!updated.ok) {
|
|
2222
|
+
failures.push({ externalId: item.externalId, error: updated.error.message });
|
|
2223
|
+
continue;
|
|
2224
|
+
}
|
|
2225
|
+
entityTouched = true;
|
|
2226
|
+
}
|
|
2145
2227
|
}
|
|
2146
|
-
isNew = true;
|
|
2147
|
-
imported += 1;
|
|
2148
|
-
entityTouched = true;
|
|
2149
2228
|
}
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
const survivingForcedPaths = (entityMapping?.forcedPushFieldPaths ?? []).filter((path) => !heldSharedPaths.includes(path));
|
|
2179
|
-
const held = this.filterConflictingFields(owned.writable, heldSharedPaths);
|
|
2180
|
-
const writable = held.writable;
|
|
2181
|
-
const blockedPaths = new Set([...owned.skipped, ...heldSharedPaths]);
|
|
2182
|
-
skipped.push(...owned.skipped.map((fieldPath) => ({ entityId, fieldPath })));
|
|
2183
|
-
conflicts.push(...shared.conflicts.map(({ platformValue: _platformValue, storeValue: _storeValue, ...conflict }) => conflict));
|
|
2184
|
-
for (const conflict of shared.conflicts) {
|
|
2185
|
-
warnings.push(`Held shared field conflict for entity "${conflict.entityId}", store "${conflict.storeId}", field "${conflict.fieldPath}" (local ${conflict.localValueSummary}, remote ${conflict.remoteValueSummary}).`);
|
|
2186
|
-
}
|
|
2187
|
-
if (existingEntity && entityMapping) {
|
|
2188
|
-
const remoteMetadata = mergeMetadata(existingEntity.metadata, writable.metadata ?? {});
|
|
2189
|
-
const remoteStatus = ownerAllows(owners, "entity.status") && !blockedPaths.has("entity.status")
|
|
2190
|
-
? writable.status ?? (existingEntity.status === "archived" ? "active" : undefined)
|
|
2191
|
-
: undefined;
|
|
2192
|
-
const updateInput = {};
|
|
2193
|
-
if (ownerAllows(owners, "entity.slug") && !blockedPaths.has("entity.slug") && existingEntity.slug !== writable.slug) {
|
|
2194
|
-
updateInput.slug = writable.slug;
|
|
2229
|
+
const optionAxes = await this.upsertOptionAxes(entityId, writable, actor);
|
|
2230
|
+
if (!optionAxes.ok)
|
|
2231
|
+
return optionAxes;
|
|
2232
|
+
const attributes = await this.setCatalogAttributesIfWritable(entityId, writable, actor, blockedPaths, CHANNEL_CONVERGENCE_CTX);
|
|
2233
|
+
if (!attributes.ok)
|
|
2234
|
+
return attributes;
|
|
2235
|
+
const variantIds = await this.upsertVariants(orgId, storeId, entityId, writable, optionAxes.value.value, actor, warnings, !heldSharedPaths.includes("options") && owners.get("options") !== "platform", item);
|
|
2236
|
+
if (!variantIds.ok)
|
|
2237
|
+
return variantIds;
|
|
2238
|
+
const taxonomy = await this.applyTaxonomy(orgId, entityId, writable, actor, warnings);
|
|
2239
|
+
if (!taxonomy.ok)
|
|
2240
|
+
return taxonomy;
|
|
2241
|
+
const media = await this.applyMedia(orgId, entityId, writable, variantIds.value.value, actor, warnings, owners);
|
|
2242
|
+
if (!media.ok)
|
|
2243
|
+
return media;
|
|
2244
|
+
attributesCreated += attributes.value.created;
|
|
2245
|
+
mediaImported += media.value.imported;
|
|
2246
|
+
variantsGivenOptionValues += variantIds.value.repaired;
|
|
2247
|
+
skipped.push(...media.value.skipped.map((fieldPath) => ({ entityId, fieldPath })));
|
|
2248
|
+
entityTouched = entityTouched || optionAxes.value.changed || variantIds.value.changed || media.value.changed || attributes.value.changed;
|
|
2249
|
+
if (entityTouched)
|
|
2250
|
+
entitiesTouched += 1;
|
|
2251
|
+
if (entityTouched) {
|
|
2252
|
+
const revision = await this.catalog.recordEntityRevision(entityId, actor, "import");
|
|
2253
|
+
if (!revision.ok) {
|
|
2254
|
+
failures.push({ externalId: item.externalId, error: revision.error.message });
|
|
2255
|
+
continue;
|
|
2256
|
+
}
|
|
2195
2257
|
}
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2258
|
+
const revisionMarkers = await this.catalog.repository.findRevisionMarkers(entityId);
|
|
2259
|
+
const latestRevisionAt = revisionMarkers.at(-1)?.createdAt;
|
|
2260
|
+
const lastSyncedAt = latestRevisionAt ?? entityMapping?.lastSyncedAt ?? new Date();
|
|
2261
|
+
if (isNew) {
|
|
2262
|
+
await this.db.update(channelEntityMap).set({
|
|
2263
|
+
syncHash: remoteHash,
|
|
2264
|
+
lastSyncedAt,
|
|
2265
|
+
heldFieldPaths: heldSharedPaths,
|
|
2266
|
+
forcedPushFieldPaths: survivingForcedPaths,
|
|
2267
|
+
}).where(and(eq(channelEntityMap.organizationId, orgId), eq(channelEntityMap.storeId, storeId), eq(channelEntityMap.kind, "entity"), eq(channelEntityMap.externalId, item.externalId), eq(channelEntityMap.entityId, entityId)));
|
|
2201
2268
|
}
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2269
|
+
else if (adoptedOrphan) {
|
|
2270
|
+
await this.db.insert(channelEntityMap).values({
|
|
2271
|
+
organizationId: orgId,
|
|
2272
|
+
storeId,
|
|
2273
|
+
kind: "entity",
|
|
2274
|
+
externalId: item.externalId,
|
|
2275
|
+
entityId,
|
|
2276
|
+
syncHash: remoteHash,
|
|
2277
|
+
lastSyncedAt,
|
|
2278
|
+
heldFieldPaths: heldSharedPaths,
|
|
2279
|
+
forcedPushFieldPaths: survivingForcedPaths,
|
|
2280
|
+
});
|
|
2213
2281
|
}
|
|
2282
|
+
else if (entityMapping) {
|
|
2283
|
+
await this.db.update(channelEntityMap).set({
|
|
2284
|
+
syncHash: remoteHash,
|
|
2285
|
+
lastSyncedAt,
|
|
2286
|
+
heldFieldPaths: heldSharedPaths,
|
|
2287
|
+
forcedPushFieldPaths: survivingForcedPaths,
|
|
2288
|
+
}).where(eq(channelEntityMap.id, entityMapping.id));
|
|
2289
|
+
}
|
|
2290
|
+
await this.db.update(channelEntityMap).set({ lastSyncedAt }).where(and(eq(channelEntityMap.organizationId, orgId), eq(channelEntityMap.storeId, storeId), eq(channelEntityMap.entityId, entityId), eq(channelEntityMap.kind, "variant")));
|
|
2214
2291
|
}
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
return attributes;
|
|
2221
|
-
const variantIds = await this.upsertVariants(orgId, storeId, entityId, writable, optionAxes.value.value, actor, warnings, !heldSharedPaths.includes("options") && owners.get("options") !== "platform", item);
|
|
2222
|
-
if (!variantIds.ok)
|
|
2223
|
-
return variantIds;
|
|
2224
|
-
const taxonomy = await this.applyTaxonomy(orgId, entityId, writable, actor, warnings);
|
|
2225
|
-
if (!taxonomy.ok)
|
|
2226
|
-
return taxonomy;
|
|
2227
|
-
const media = await this.applyMedia(orgId, entityId, writable, variantIds.value.value, actor, warnings, owners);
|
|
2228
|
-
if (!media.ok)
|
|
2229
|
-
return media;
|
|
2230
|
-
attributesCreated += attributes.value.created;
|
|
2231
|
-
mediaImported += media.value.imported;
|
|
2232
|
-
variantsGivenOptionValues += variantIds.value.repaired;
|
|
2233
|
-
skipped.push(...media.value.skipped.map((fieldPath) => ({ entityId, fieldPath })));
|
|
2234
|
-
entityTouched = entityTouched || optionAxes.value.changed || variantIds.value.changed || media.value.changed || attributes.value.changed;
|
|
2235
|
-
if (entityTouched)
|
|
2236
|
-
entitiesTouched += 1;
|
|
2237
|
-
if (entityTouched) {
|
|
2238
|
-
const revision = await this.catalog.recordEntityRevision(entityId, actor, "import");
|
|
2239
|
-
if (!revision.ok)
|
|
2240
|
-
return PluginErr(revision.error.message);
|
|
2241
|
-
}
|
|
2242
|
-
const revisionMarkers = await this.catalog.repository.findRevisionMarkers(entityId);
|
|
2243
|
-
const latestRevisionAt = revisionMarkers.at(-1)?.createdAt;
|
|
2244
|
-
const lastSyncedAt = latestRevisionAt ?? entityMapping?.lastSyncedAt ?? new Date();
|
|
2245
|
-
if (isNew) {
|
|
2246
|
-
await this.db.update(channelEntityMap).set({
|
|
2247
|
-
syncHash: remoteHash,
|
|
2248
|
-
lastSyncedAt,
|
|
2249
|
-
heldFieldPaths: heldSharedPaths,
|
|
2250
|
-
forcedPushFieldPaths: survivingForcedPaths,
|
|
2251
|
-
}).where(and(eq(channelEntityMap.organizationId, orgId), eq(channelEntityMap.storeId, storeId), eq(channelEntityMap.kind, "entity"), eq(channelEntityMap.externalId, item.externalId), eq(channelEntityMap.entityId, entityId)));
|
|
2252
|
-
}
|
|
2253
|
-
else if (adoptedOrphan) {
|
|
2254
|
-
await this.db.insert(channelEntityMap).values({
|
|
2255
|
-
organizationId: orgId,
|
|
2256
|
-
storeId,
|
|
2257
|
-
kind: "entity",
|
|
2258
|
-
externalId: item.externalId,
|
|
2259
|
-
entityId,
|
|
2260
|
-
syncHash: remoteHash,
|
|
2261
|
-
lastSyncedAt,
|
|
2262
|
-
heldFieldPaths: heldSharedPaths,
|
|
2263
|
-
forcedPushFieldPaths: survivingForcedPaths,
|
|
2264
|
-
});
|
|
2265
|
-
}
|
|
2266
|
-
else if (entityMapping) {
|
|
2267
|
-
await this.db.update(channelEntityMap).set({
|
|
2268
|
-
syncHash: remoteHash,
|
|
2269
|
-
lastSyncedAt,
|
|
2270
|
-
heldFieldPaths: heldSharedPaths,
|
|
2271
|
-
forcedPushFieldPaths: survivingForcedPaths,
|
|
2272
|
-
}).where(eq(channelEntityMap.id, entityMapping.id));
|
|
2292
|
+
catch (error) {
|
|
2293
|
+
// Anything the stages throw rather than returning. Same disposition: record it against the
|
|
2294
|
+
// item and carry on, so an unforeseen throw costs one product and not the remaining page.
|
|
2295
|
+
failures.push({ externalId: item.externalId, error: error instanceof Error ? error.message : String(error) });
|
|
2296
|
+
continue;
|
|
2273
2297
|
}
|
|
2274
|
-
await this.db.update(channelEntityMap).set({ lastSyncedAt }).where(and(eq(channelEntityMap.organizationId, orgId), eq(channelEntityMap.storeId, storeId), eq(channelEntityMap.entityId, entityId), eq(channelEntityMap.kind, "variant")));
|
|
2275
2298
|
}
|
|
2276
2299
|
return Ok({
|
|
2277
2300
|
imported,
|
|
@@ -2284,6 +2307,7 @@ export class ChannelConnectorService {
|
|
|
2284
2307
|
skipped,
|
|
2285
2308
|
conflicts,
|
|
2286
2309
|
warnings,
|
|
2310
|
+
failures,
|
|
2287
2311
|
});
|
|
2288
2312
|
}
|
|
2289
2313
|
async reconcile(orgId, storeId, actor) {
|