@porulle/plugin-channel-connector 0.50.1 → 0.50.2

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.js CHANGED
@@ -151,7 +151,11 @@ export function channelConnectorPlugin(options = {}) {
151
151
  handler: async ({ input, ctx }) => {
152
152
  const orgId = String(input.orgId);
153
153
  const jobs = ctx.services.jobs;
154
- const stores = await ctx.db.select().from(connectedStores).where(and(eq(connectedStores.organizationId, orgId), eq(connectedStores.status, "connected")));
154
+ // Only stores a registered connector can reach: a reconcile for any other provider (the
155
+ // seed's `manual` point-of-sale store) can only fail with "No connector registered".
156
+ const providers = new Set((options.connectors ?? []).map((connector) => connector.providerId));
157
+ const stores = (await ctx.db.select().from(connectedStores).where(and(eq(connectedStores.organizationId, orgId), eq(connectedStores.status, "connected"))))
158
+ .filter((store) => providers.has(store.provider));
155
159
  const window = options.reconcileJitterWindowMs ?? 60 * 60 * 1000;
156
160
  for (const store of stores) {
157
161
  const offset = createHash("sha256").update(store.id).digest().readUInt32BE(0) % window;
package/dist/service.d.ts CHANGED
@@ -42,6 +42,9 @@ export interface ReconcileReport extends Record<string, unknown> {
42
42
  skipped?: CatalogFieldSkip[];
43
43
  conflicts?: CatalogFieldConflict[];
44
44
  warnings?: string[];
45
+ /** Items this reconcile could not converge, each with its error. A reconcile that dropped an
46
+ * item is never reported as a clean one. */
47
+ failures?: CatalogConvergenceFailure[];
45
48
  }
46
49
  export interface CatalogFieldConflict {
47
50
  entityId: string;
@@ -333,6 +336,12 @@ export declare class ChannelConnectorService {
333
336
  */
334
337
  private taxonomyCache;
335
338
  private taxonomyFor;
339
+ /**
340
+ * Create a category or brand, or adopt the one another writer created first. The taxonomy
341
+ * snapshot is per converge, so two stores converging at once can both see a slug missing and both
342
+ * create it; the loser's conflict — returned or thrown — means the row it wanted now exists.
343
+ */
344
+ private createTaxonomyOrAdopt;
336
345
  private applyTaxonomy;
337
346
  private applyMedia;
338
347
  private getStoreRecord;
package/dist/service.js CHANGED
@@ -1152,6 +1152,30 @@ export class ChannelConnectorService {
1152
1152
  this.taxonomyCache = { orgId, categories: categoryRows, brands: brandRows, tags: tagRows };
1153
1153
  return this.taxonomyCache;
1154
1154
  }
1155
+ /**
1156
+ * Create a category or brand, or adopt the one another writer created first. The taxonomy
1157
+ * snapshot is per converge, so two stores converging at once can both see a slug missing and both
1158
+ * create it; the loser's conflict — returned or thrown — means the row it wanted now exists.
1159
+ */
1160
+ async createTaxonomyOrAdopt(create, byId, bySlug, label) {
1161
+ let failure = `${label} was not persisted.`;
1162
+ try {
1163
+ const created = await create();
1164
+ if (created.ok) {
1165
+ const row = await byId(created.value.id);
1166
+ if (row !== undefined)
1167
+ return Ok(row);
1168
+ }
1169
+ else {
1170
+ failure = created.error;
1171
+ }
1172
+ }
1173
+ catch (error) {
1174
+ failure = error;
1175
+ }
1176
+ const existing = await bySlug();
1177
+ return existing !== undefined ? Ok(existing) : PluginErr(`${label}: ${errorMessage(failure)}`);
1178
+ }
1155
1179
  async applyTaxonomy(orgId, entityId, item, actor, warnings) {
1156
1180
  const taxonomy = await this.taxonomyFor(orgId);
1157
1181
  // The links this entity already has, read only for the classes the item names. A link already
@@ -1169,13 +1193,10 @@ export class ChannelConnectorService {
1169
1193
  continue;
1170
1194
  }
1171
1195
  if (!category) {
1172
- const created = await this.catalog.createCategory({ slug }, actor);
1196
+ const created = await this.createTaxonomyOrAdopt(() => this.catalog.createCategory({ slug }, actor), async (id) => (await this.db.select().from(categories).where(eq(categories.id, id)))[0], async () => (await this.db.select().from(categories).where(and(eq(categories.organizationId, orgId), eq(categories.slug, slug))))[0], `Category "${slug}"`);
1173
1197
  if (!created.ok)
1174
- return PluginErr(created.error.message);
1175
- const [createdCategory] = await this.db.select().from(categories).where(eq(categories.id, created.value.id));
1176
- if (!createdCategory)
1177
- return PluginErr(`Category "${slug}" was not persisted.`);
1178
- category = createdCategory;
1198
+ return created;
1199
+ category = created.value;
1179
1200
  categoryRows.push(category);
1180
1201
  }
1181
1202
  if (linkedCategories.has(category.id))
@@ -1189,13 +1210,11 @@ export class ChannelConnectorService {
1189
1210
  if (item.brand) {
1190
1211
  let brand = brandRows.find((row) => row.slug === item.brand);
1191
1212
  if (!brand) {
1192
- const created = await this.catalog.createBrand({ slug: item.brand, displayName: item.brand }, actor);
1213
+ const brandSlug = item.brand;
1214
+ const created = await this.createTaxonomyOrAdopt(() => this.catalog.createBrand({ slug: brandSlug, displayName: brandSlug }, actor), async (id) => (await this.db.select().from(brands).where(eq(brands.id, id)))[0], async () => (await this.db.select().from(brands).where(and(eq(brands.organizationId, orgId), eq(brands.slug, brandSlug))))[0], `Brand "${brandSlug}"`);
1193
1215
  if (!created.ok)
1194
- return PluginErr(created.error.message);
1195
- const [createdBrand] = await this.db.select().from(brands).where(eq(brands.id, created.value.id));
1196
- if (!createdBrand)
1197
- return PluginErr(`Brand "${item.brand}" was not persisted.`);
1198
- brand = createdBrand;
1216
+ return created;
1217
+ brand = created.value;
1199
1218
  brandRows.push(brand);
1200
1219
  }
1201
1220
  if (!linkedBrands.has(brand.id)) {
@@ -2905,8 +2924,10 @@ export class ChannelConnectorService {
2905
2924
  continue;
2906
2925
  }
2907
2926
  const taxonomy = await this.applyTaxonomy(orgId, entityId, writable, actor, warnings);
2908
- if (!taxonomy.ok)
2909
- return taxonomy;
2927
+ if (!taxonomy.ok) {
2928
+ failures.push({ externalId: item.externalId, error: taxonomy.error });
2929
+ continue;
2930
+ }
2910
2931
  const media = await this.applyMedia(orgId, entityId, writable, variantIds.value.value, actor, warnings, owners);
2911
2932
  if (!media.ok)
2912
2933
  return media;
@@ -3069,6 +3090,7 @@ export class ChannelConnectorService {
3069
3090
  ...(skipped.length > 0 ? { skipped: uniqueSkipped(skipped) } : {}),
3070
3091
  ...(converged.value.conflicts.length > 0 ? { conflicts: converged.value.conflicts } : {}),
3071
3092
  ...(converged.value.warnings.length > 0 ? { warnings: converged.value.warnings } : {}),
3093
+ ...(converged.value.failures.length > 0 ? { failures: converged.value.failures } : {}),
3072
3094
  };
3073
3095
  await this.db.update(connectedStores).set({
3074
3096
  lastReconcileAt: new Date(),