@porulle/plugin-channel-connector 0.50.1 → 0.50.3
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 +5 -1
- package/dist/service.d.ts +9 -0
- package/dist/service.js +36 -14
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/src/index.ts +5 -1
- package/src/service.ts +49 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@porulle/plugin-channel-connector",
|
|
3
|
-
"version": "0.50.
|
|
3
|
+
"version": "0.50.3",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -19,15 +19,15 @@
|
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@hono/zod-openapi": "^1.2.2",
|
|
21
21
|
"hono": "^4.12.5",
|
|
22
|
-
"@porulle/core": "0.50.
|
|
22
|
+
"@porulle/core": "0.50.3"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"@types/node": "^24.5.2",
|
|
26
26
|
"eslint": "^9.39.1",
|
|
27
27
|
"typescript": "5.9.2",
|
|
28
28
|
"vitest": "^3.2.4",
|
|
29
|
-
"@porulle/
|
|
30
|
-
"@porulle/
|
|
29
|
+
"@porulle/typescript-config": "0.1.0",
|
|
30
|
+
"@porulle/eslint-config": "0.1.0"
|
|
31
31
|
},
|
|
32
32
|
"publishConfig": {
|
|
33
33
|
"access": "public"
|
package/src/index.ts
CHANGED
|
@@ -308,7 +308,11 @@ export function channelConnectorPlugin(options: ChannelConnectorPluginOptions =
|
|
|
308
308
|
handler: async ({ input, ctx }: { input: Record<string, unknown>; ctx: import("@porulle/core").TaskContext }) => {
|
|
309
309
|
const orgId = String(input.orgId);
|
|
310
310
|
const jobs = ctx.services.jobs as JobsAdapter;
|
|
311
|
-
|
|
311
|
+
// Only stores a registered connector can reach: a reconcile for any other provider (the
|
|
312
|
+
// seed's `manual` point-of-sale store) can only fail with "No connector registered".
|
|
313
|
+
const providers = new Set((options.connectors ?? []).map((connector) => connector.providerId));
|
|
314
|
+
const stores = (await ctx.db.select().from(connectedStores).where(and(eq(connectedStores.organizationId, orgId), eq(connectedStores.status, "connected"))))
|
|
315
|
+
.filter((store) => providers.has(store.provider));
|
|
312
316
|
const window = options.reconcileJitterWindowMs ?? 60 * 60 * 1000;
|
|
313
317
|
for (const store of stores) {
|
|
314
318
|
const offset = createHash("sha256").update(store.id).digest().readUInt32BE(0) % window;
|
package/src/service.ts
CHANGED
|
@@ -167,6 +167,9 @@ export interface ReconcileReport extends Record<string, unknown> {
|
|
|
167
167
|
skipped?: CatalogFieldSkip[];
|
|
168
168
|
conflicts?: CatalogFieldConflict[];
|
|
169
169
|
warnings?: string[];
|
|
170
|
+
/** Items this reconcile could not converge, each with its error. A reconcile that dropped an
|
|
171
|
+
* item is never reported as a clean one. */
|
|
172
|
+
failures?: CatalogConvergenceFailure[];
|
|
170
173
|
}
|
|
171
174
|
|
|
172
175
|
export interface CatalogFieldConflict {
|
|
@@ -1828,6 +1831,33 @@ export class ChannelConnectorService {
|
|
|
1828
1831
|
return this.taxonomyCache;
|
|
1829
1832
|
}
|
|
1830
1833
|
|
|
1834
|
+
/**
|
|
1835
|
+
* Create a category or brand, or adopt the one another writer created first. The taxonomy
|
|
1836
|
+
* snapshot is per converge, so two stores converging at once can both see a slug missing and both
|
|
1837
|
+
* create it; the loser's conflict — returned or thrown — means the row it wanted now exists.
|
|
1838
|
+
*/
|
|
1839
|
+
private async createTaxonomyOrAdopt<Row>(
|
|
1840
|
+
create: () => Promise<{ ok: true; value: { id: string } } | { ok: false; error: { message: string } }>,
|
|
1841
|
+
byId: (id: string) => Promise<Row | undefined>,
|
|
1842
|
+
bySlug: () => Promise<Row | undefined>,
|
|
1843
|
+
label: string,
|
|
1844
|
+
): Promise<PluginResult<Row>> {
|
|
1845
|
+
let failure: unknown = `${label} was not persisted.`;
|
|
1846
|
+
try {
|
|
1847
|
+
const created = await create();
|
|
1848
|
+
if (created.ok) {
|
|
1849
|
+
const row = await byId(created.value.id);
|
|
1850
|
+
if (row !== undefined) return Ok(row);
|
|
1851
|
+
} else {
|
|
1852
|
+
failure = created.error;
|
|
1853
|
+
}
|
|
1854
|
+
} catch (error) {
|
|
1855
|
+
failure = error;
|
|
1856
|
+
}
|
|
1857
|
+
const existing = await bySlug();
|
|
1858
|
+
return existing !== undefined ? Ok(existing) : PluginErr(`${label}: ${errorMessage(failure)}`);
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1831
1861
|
private async applyTaxonomy(
|
|
1832
1862
|
orgId: string,
|
|
1833
1863
|
entityId: string,
|
|
@@ -1851,11 +1881,14 @@ export class ChannelConnectorService {
|
|
|
1851
1881
|
continue;
|
|
1852
1882
|
}
|
|
1853
1883
|
if (!category) {
|
|
1854
|
-
const created = await this.
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1884
|
+
const created = await this.createTaxonomyOrAdopt(
|
|
1885
|
+
() => this.catalog.createCategory({ slug }, actor),
|
|
1886
|
+
async (id) => (await this.db.select().from(categories).where(eq(categories.id, id)))[0],
|
|
1887
|
+
async () => (await this.db.select().from(categories).where(and(eq(categories.organizationId, orgId), eq(categories.slug, slug))))[0],
|
|
1888
|
+
`Category "${slug}"`,
|
|
1889
|
+
);
|
|
1890
|
+
if (!created.ok) return created;
|
|
1891
|
+
category = created.value;
|
|
1859
1892
|
categoryRows.push(category);
|
|
1860
1893
|
}
|
|
1861
1894
|
if (linkedCategories.has(category.id)) continue;
|
|
@@ -1868,11 +1901,15 @@ export class ChannelConnectorService {
|
|
|
1868
1901
|
if (item.brand) {
|
|
1869
1902
|
let brand = brandRows.find((row) => row.slug === item.brand);
|
|
1870
1903
|
if (!brand) {
|
|
1871
|
-
const
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1904
|
+
const brandSlug = item.brand;
|
|
1905
|
+
const created = await this.createTaxonomyOrAdopt(
|
|
1906
|
+
() => this.catalog.createBrand({ slug: brandSlug, displayName: brandSlug }, actor),
|
|
1907
|
+
async (id) => (await this.db.select().from(brands).where(eq(brands.id, id)))[0],
|
|
1908
|
+
async () => (await this.db.select().from(brands).where(and(eq(brands.organizationId, orgId), eq(brands.slug, brandSlug))))[0],
|
|
1909
|
+
`Brand "${brandSlug}"`,
|
|
1910
|
+
);
|
|
1911
|
+
if (!created.ok) return created;
|
|
1912
|
+
brand = created.value;
|
|
1876
1913
|
brandRows.push(brand);
|
|
1877
1914
|
}
|
|
1878
1915
|
if (!linkedBrands.has(brand.id)) {
|
|
@@ -3850,7 +3887,7 @@ export class ChannelConnectorService {
|
|
|
3850
3887
|
// that item's failure, reported with its error — not a page error the caller would retry.
|
|
3851
3888
|
if (!variantIds.ok) { failures.push({ externalId: item.externalId, error: variantIds.error }); continue; }
|
|
3852
3889
|
const taxonomy = await this.applyTaxonomy(orgId, entityId, writable, actor, warnings);
|
|
3853
|
-
if (!taxonomy.ok)
|
|
3890
|
+
if (!taxonomy.ok) { failures.push({ externalId: item.externalId, error: taxonomy.error }); continue; }
|
|
3854
3891
|
const media = await this.applyMedia(orgId, entityId, writable, variantIds.value.value, actor, warnings, owners);
|
|
3855
3892
|
if (!media.ok) return media;
|
|
3856
3893
|
attributesCreated += attributes.value.created;
|
|
@@ -4025,6 +4062,7 @@ export class ChannelConnectorService {
|
|
|
4025
4062
|
...(skipped.length > 0 ? { skipped: uniqueSkipped(skipped) } : {}),
|
|
4026
4063
|
...(converged.value.conflicts.length > 0 ? { conflicts: converged.value.conflicts } : {}),
|
|
4027
4064
|
...(converged.value.warnings.length > 0 ? { warnings: converged.value.warnings } : {}),
|
|
4065
|
+
...(converged.value.failures.length > 0 ? { failures: converged.value.failures } : {}),
|
|
4028
4066
|
};
|
|
4029
4067
|
await this.db.update(connectedStores).set({
|
|
4030
4068
|
lastReconcileAt: new Date(),
|