@porulle/plugin-channel-connector 0.11.0 → 0.14.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/catalog-push-trigger.d.ts +26 -0
- package/dist/catalog-push-trigger.js +70 -0
- package/dist/hooks.js +18 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +85 -7
- package/dist/schema.d.ts +812 -55
- package/dist/schema.js +73 -1
- package/dist/service.d.ts +91 -6
- package/dist/service.js +1080 -45
- package/package.json +4 -4
- package/src/catalog-push-trigger.ts +99 -0
- package/src/hooks.ts +30 -6
- package/src/index.ts +124 -5
- package/src/schema.ts +101 -1
- package/src/service.ts +1417 -49
package/dist/schema.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { boolean, index, integer, jsonb, pgTable, text, timestamp, uniqueIndex, uuid, } from "@porulle/core/drizzle";
|
|
1
|
+
import { boolean, index, integer, jsonb, pgTable, text, timestamp, uniqueIndex, uuid, sql, } from "@porulle/core/drizzle";
|
|
2
2
|
export const connectedStores = pgTable("connected_stores", {
|
|
3
3
|
id: uuid("id").defaultRandom().primaryKey(),
|
|
4
4
|
organizationId: text("organization_id").notNull(),
|
|
@@ -33,12 +33,48 @@ export const channelEntityMap = pgTable("channel_entity_map", {
|
|
|
33
33
|
variantId: uuid("variant_id"),
|
|
34
34
|
lastSyncedAt: timestamp("last_synced_at", { withTimezone: true }).defaultNow().notNull(),
|
|
35
35
|
syncHash: text("sync_hash").notNull(),
|
|
36
|
+
outboundHash: text("outbound_hash"),
|
|
37
|
+
outboundPushedAt: timestamp("outbound_pushed_at", { withTimezone: true }),
|
|
38
|
+
outboundFieldPaths: jsonb("outbound_field_paths").$type().notNull().default([]),
|
|
36
39
|
heldFieldPaths: jsonb("held_field_paths").$type().notNull().default([]),
|
|
40
|
+
forcedPushFieldPaths: jsonb("forced_push_field_paths").$type().notNull().default([]),
|
|
37
41
|
}, (table) => ({
|
|
38
42
|
orgIdx: index("idx_channel_entity_map_org").on(table.organizationId),
|
|
39
43
|
storeIdx: index("idx_channel_entity_map_store").on(table.storeId),
|
|
40
44
|
externalUnique: uniqueIndex("channel_entity_map_store_kind_external_unique").on(table.storeId, table.kind, table.externalId),
|
|
41
45
|
}));
|
|
46
|
+
export const channelCatalogConflicts = pgTable("channel_catalog_conflicts", {
|
|
47
|
+
id: uuid("id").defaultRandom().primaryKey(),
|
|
48
|
+
organizationId: text("organization_id").notNull(),
|
|
49
|
+
storeId: uuid("store_id").references(() => connectedStores.id, { onDelete: "cascade" }).notNull(),
|
|
50
|
+
entityId: uuid("entity_id").notNull(),
|
|
51
|
+
fieldPath: text("field_path").notNull(),
|
|
52
|
+
platformValue: jsonb("platform_value").$type().notNull(),
|
|
53
|
+
storeValue: jsonb("store_value").$type().notNull(),
|
|
54
|
+
state: text("state", { enum: ["open", "resolved"] }).notNull().default("open"),
|
|
55
|
+
resolvedBy: text("resolved_by"),
|
|
56
|
+
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
|
57
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
|
|
58
|
+
}, (table) => ({
|
|
59
|
+
orgIdx: index("idx_channel_catalog_conflicts_org").on(table.organizationId),
|
|
60
|
+
stateIdx: index("idx_channel_catalog_conflicts_org_state").on(table.organizationId, table.state),
|
|
61
|
+
openUnique: uniqueIndex("channel_catalog_conflicts_open_unique")
|
|
62
|
+
.on(table.storeId, table.entityId, table.fieldPath)
|
|
63
|
+
.where(sql `${table.state} = 'open'`),
|
|
64
|
+
}));
|
|
65
|
+
export const channelCatalogConflictEvents = pgTable("channel_catalog_conflict_events", {
|
|
66
|
+
id: uuid("id").defaultRandom().primaryKey(),
|
|
67
|
+
organizationId: text("organization_id").notNull(),
|
|
68
|
+
conflictId: uuid("conflict_id").references(() => channelCatalogConflicts.id, { onDelete: "cascade" }).notNull(),
|
|
69
|
+
fromState: text("from_state"),
|
|
70
|
+
toState: text("to_state").notNull(),
|
|
71
|
+
reason: text("reason"),
|
|
72
|
+
changedBy: text("changed_by").notNull(),
|
|
73
|
+
changedAt: timestamp("changed_at", { withTimezone: true }).defaultNow().notNull(),
|
|
74
|
+
}, (table) => ({
|
|
75
|
+
orgIdx: index("idx_channel_catalog_conflict_events_org").on(table.organizationId),
|
|
76
|
+
conflictIdx: index("idx_channel_catalog_conflict_events_conflict").on(table.conflictId),
|
|
77
|
+
}));
|
|
42
78
|
export const channelOrderExports = pgTable("channel_order_exports", {
|
|
43
79
|
id: uuid("id").defaultRandom().primaryKey(),
|
|
44
80
|
organizationId: text("organization_id").notNull(),
|
|
@@ -61,6 +97,42 @@ export const channelOrderExports = pgTable("channel_order_exports", {
|
|
|
61
97
|
stateIdx: index("idx_channel_order_exports_state").on(table.organizationId, table.state),
|
|
62
98
|
orderIdx: index("idx_channel_order_exports_order").on(table.organizationId, table.orderId),
|
|
63
99
|
}));
|
|
100
|
+
export const channelCatalogPushes = pgTable("channel_catalog_pushes", {
|
|
101
|
+
id: uuid("id").defaultRandom().primaryKey(),
|
|
102
|
+
organizationId: text("organization_id").notNull(),
|
|
103
|
+
storeId: uuid("store_id").references(() => connectedStores.id, { onDelete: "cascade" }).notNull(),
|
|
104
|
+
entityId: uuid("entity_id").notNull(),
|
|
105
|
+
payloadSnapshot: jsonb("payload_snapshot").$type(),
|
|
106
|
+
state: text("state", { enum: ["pending", "exported", "confirmed", "failed", "abandoned"] })
|
|
107
|
+
.notNull()
|
|
108
|
+
.default("pending"),
|
|
109
|
+
failureKind: text("failure_kind", { enum: ["definitive", "transient"] }),
|
|
110
|
+
attempts: integer("attempts").notNull().default(0),
|
|
111
|
+
lastError: text("last_error"),
|
|
112
|
+
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
|
113
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
|
|
114
|
+
}, (table) => ({
|
|
115
|
+
orgIdx: index("idx_channel_catalog_pushes_org").on(table.organizationId),
|
|
116
|
+
storeIdx: index("idx_channel_catalog_pushes_store").on(table.storeId),
|
|
117
|
+
stateIdx: index("idx_channel_catalog_pushes_state").on(table.organizationId, table.state),
|
|
118
|
+
entityIdx: index("idx_channel_catalog_pushes_entity").on(table.organizationId, table.entityId),
|
|
119
|
+
storeEntityUnique: uniqueIndex("channel_catalog_pushes_store_entity_unique").on(table.storeId, table.entityId),
|
|
120
|
+
}));
|
|
121
|
+
export const channelCatalogPushEvents = pgTable("channel_catalog_push_events", {
|
|
122
|
+
id: uuid("id").defaultRandom().primaryKey(),
|
|
123
|
+
organizationId: text("organization_id").notNull(),
|
|
124
|
+
pushId: uuid("push_id")
|
|
125
|
+
.references(() => channelCatalogPushes.id, { onDelete: "cascade" })
|
|
126
|
+
.notNull(),
|
|
127
|
+
fromState: text("from_state").notNull(),
|
|
128
|
+
toState: text("to_state").notNull(),
|
|
129
|
+
reason: text("reason"),
|
|
130
|
+
changedBy: text("changed_by").notNull(),
|
|
131
|
+
changedAt: timestamp("changed_at", { withTimezone: true }).defaultNow().notNull(),
|
|
132
|
+
}, (table) => ({
|
|
133
|
+
orgIdx: index("idx_channel_catalog_push_events_org").on(table.organizationId),
|
|
134
|
+
pushIdx: index("idx_channel_catalog_push_events_push").on(table.pushId),
|
|
135
|
+
}));
|
|
64
136
|
export const channelExportEvents = pgTable("channel_export_events", {
|
|
65
137
|
id: uuid("id").defaultRandom().primaryKey(),
|
|
66
138
|
organizationId: text("organization_id").notNull(),
|
package/dist/service.d.ts
CHANGED
|
@@ -1,14 +1,30 @@
|
|
|
1
|
-
import type { Actor, ChannelConnector, ChannelOrderSlice, ChannelPushCatalogItem, PluginDb, PluginResult, PluginTxFn } from "@porulle/core";
|
|
2
|
-
import type { FieldPath } from "@porulle/core";
|
|
1
|
+
import type { Actor, ChannelConnector, ChannelOrderSlice, ChannelPushCatalogField, ChannelPushCatalogImage, ChannelPushCatalogItem, ChannelPushCatalogItemOutcome, ChannelPushCatalogResult, PluginDb, PluginResult, PluginTxFn } from "@porulle/core";
|
|
2
|
+
import type { FieldOwner, FieldPath } from "@porulle/core";
|
|
3
3
|
import type { JobsAdapter } from "@porulle/core";
|
|
4
|
-
import { type ChannelOrderExport, type ChannelRefundRequest, type ConnectedStore } from "./schema.js";
|
|
5
|
-
import { type CatalogFieldMapping } from "./catalog-field-mapping.js";
|
|
4
|
+
import { type ChannelCatalogPush, type ChannelCatalogConflict, type ChannelOrderExport, type ChannelRefundRequest, type ConnectedStore } from "./schema.js";
|
|
5
|
+
import { type CatalogFieldMapping, type CatalogFieldTarget } from "./catalog-field-mapping.js";
|
|
6
6
|
export type ExportState = ChannelOrderExport["state"];
|
|
7
|
+
export type CatalogPushState = ChannelCatalogPush["state"];
|
|
8
|
+
export type CatalogConflictState = ChannelCatalogConflict["state"];
|
|
9
|
+
export declare const CATALOG_PUSH_BATCH_SIZES: Record<string, number>;
|
|
10
|
+
export declare const CATALOG_PUSH_MAX_ATTEMPTS = 8;
|
|
11
|
+
export declare function catalogPushRetryDelayMs(attempts: number): number;
|
|
12
|
+
export interface CatalogPushJobResult extends Record<string, unknown> {
|
|
13
|
+
noop?: boolean;
|
|
14
|
+
rescheduled?: boolean;
|
|
15
|
+
complete?: boolean;
|
|
16
|
+
cursor?: string;
|
|
17
|
+
pushed?: number;
|
|
18
|
+
failed?: number;
|
|
19
|
+
}
|
|
20
|
+
export declare function catalogPushConcurrencyKey(input: Record<string, unknown>): string;
|
|
21
|
+
export declare function isCatalogPushBreakerOpen(breakerState: Record<string, unknown>): boolean;
|
|
7
22
|
export interface ReconcileReport extends Record<string, unknown> {
|
|
8
23
|
imported: number;
|
|
9
24
|
converged: number;
|
|
10
25
|
archived: number;
|
|
11
26
|
inventoryUpdated: number;
|
|
27
|
+
openConflicts: number;
|
|
12
28
|
driftAlert: boolean;
|
|
13
29
|
skipped?: CatalogFieldSkip[];
|
|
14
30
|
conflicts?: CatalogFieldConflict[];
|
|
@@ -25,11 +41,15 @@ export interface CatalogFieldSkip {
|
|
|
25
41
|
entityId: string;
|
|
26
42
|
fieldPath: FieldPath;
|
|
27
43
|
}
|
|
28
|
-
export type CatalogPushSkipReason = "no_mapping" | "held" | "entity_not_active" | "unmapped_entity";
|
|
44
|
+
export type CatalogPushSkipReason = "no_mapping" | "held" | "store_owned" | "entity_not_active" | "unmapped_entity";
|
|
29
45
|
export interface CatalogPushFieldSkip {
|
|
30
46
|
entityId: string;
|
|
31
47
|
fieldPath: FieldPath;
|
|
32
48
|
reason: CatalogPushSkipReason;
|
|
49
|
+
value?: unknown;
|
|
50
|
+
owner?: FieldOwner;
|
|
51
|
+
target?: CatalogFieldTarget;
|
|
52
|
+
remoteKey?: string;
|
|
33
53
|
}
|
|
34
54
|
export type PublicConnectedStore = Omit<ConnectedStore, "credentials" | "webhookSecret"> & {
|
|
35
55
|
credentials: "[REDACTED]";
|
|
@@ -95,13 +115,57 @@ export interface BackfillCatalogOptions {
|
|
|
95
115
|
}
|
|
96
116
|
export interface BuildCatalogPushItemsOptions {
|
|
97
117
|
recordRevision?: boolean;
|
|
118
|
+
forceFieldPaths?: Record<string, FieldPath[]>;
|
|
119
|
+
}
|
|
120
|
+
export interface CatalogPushAssemblyField extends ChannelPushCatalogField {
|
|
121
|
+
target: CatalogFieldTarget;
|
|
122
|
+
}
|
|
123
|
+
export interface CatalogPushAssemblyImage extends ChannelPushCatalogImage {
|
|
124
|
+
fieldPath: FieldPath;
|
|
125
|
+
target: CatalogFieldTarget;
|
|
126
|
+
remoteKey: string;
|
|
127
|
+
}
|
|
128
|
+
export interface CatalogPushAssemblyItem extends Omit<ChannelPushCatalogItem, "fields" | "images"> {
|
|
129
|
+
fields: CatalogPushAssemblyField[];
|
|
130
|
+
images?: CatalogPushAssemblyImage[];
|
|
98
131
|
}
|
|
99
132
|
export interface BuildCatalogPushItemsResult {
|
|
100
|
-
items:
|
|
133
|
+
items: CatalogPushAssemblyItem[];
|
|
134
|
+
skipped: CatalogPushFieldSkip[];
|
|
135
|
+
warnings: string[];
|
|
136
|
+
}
|
|
137
|
+
export interface PushCatalogToStoreResult extends ChannelPushCatalogResult {
|
|
138
|
+
skipped: CatalogPushFieldSkip[];
|
|
139
|
+
warnings: string[];
|
|
140
|
+
}
|
|
141
|
+
export interface CatalogPushPreviewUnavailable {
|
|
142
|
+
status: "unavailable";
|
|
143
|
+
}
|
|
144
|
+
export type CatalogPushPreviewBefore = unknown | CatalogPushPreviewUnavailable;
|
|
145
|
+
export type CatalogPushPreviewBeforeStatus = "value" | "missing" | "unavailable";
|
|
146
|
+
export interface CatalogPushPreviewDiff {
|
|
147
|
+
fieldPath: FieldPath;
|
|
148
|
+
target: CatalogFieldTarget | null;
|
|
149
|
+
remoteKey: string | null;
|
|
150
|
+
before: CatalogPushPreviewBefore;
|
|
151
|
+
beforeStatus: CatalogPushPreviewBeforeStatus;
|
|
152
|
+
after: unknown;
|
|
153
|
+
owner: FieldOwner;
|
|
154
|
+
willWrite: boolean;
|
|
155
|
+
reason?: CatalogPushSkipReason;
|
|
156
|
+
}
|
|
157
|
+
export interface CatalogPushPreviewItem {
|
|
158
|
+
externalId: string;
|
|
159
|
+
diffs: CatalogPushPreviewDiff[];
|
|
160
|
+
}
|
|
161
|
+
export interface CatalogPushPreviewResult {
|
|
162
|
+
items: CatalogPushPreviewItem[];
|
|
101
163
|
skipped: CatalogPushFieldSkip[];
|
|
102
164
|
warnings: string[];
|
|
103
165
|
}
|
|
104
166
|
export declare function canExportTransition(from: ExportState, to: ExportState): boolean;
|
|
167
|
+
export declare function canCatalogPushTransition(from: CatalogPushState, to: CatalogPushState): boolean;
|
|
168
|
+
export declare const CATALOG_OUTBOUND_SUPPRESSION_WINDOW_MS: number;
|
|
105
169
|
export declare class ChannelConnectorService {
|
|
106
170
|
private readonly db;
|
|
107
171
|
private readonly services;
|
|
@@ -118,9 +182,13 @@ export declare class ChannelConnectorService {
|
|
|
118
182
|
private filterOwnedFieldsAtPaths;
|
|
119
183
|
private filterConflictingFields;
|
|
120
184
|
private remoteFieldValue;
|
|
185
|
+
private isOutboundEcho;
|
|
121
186
|
private localFieldValue;
|
|
187
|
+
private lastSyncedSnapshot;
|
|
122
188
|
private detectSharedConflicts;
|
|
189
|
+
private persistCatalogConflicts;
|
|
123
190
|
private setCatalogAttributes;
|
|
191
|
+
private setCatalogAttributesIfWritable;
|
|
124
192
|
private upsertOptionAxes;
|
|
125
193
|
private upsertVariants;
|
|
126
194
|
private applyTaxonomy;
|
|
@@ -130,6 +198,9 @@ export declare class ChannelConnectorService {
|
|
|
130
198
|
getStoresByDomain(shopDomain: string): Promise<ConnectedStore[]>;
|
|
131
199
|
resolveCatalogFieldMapping(store: Pick<ConnectedStore, "provider" | "catalogFieldMapping">, filterableCustomFields?: ReadonlySet<string> | Readonly<Record<string, boolean>>, warnings?: string[]): CatalogFieldMapping;
|
|
132
200
|
buildCatalogPushItems(orgId: string, storeId: string, entityIds: string[], options?: BuildCatalogPushItemsOptions): Promise<PluginResult<BuildCatalogPushItemsResult>>;
|
|
201
|
+
recordOutboundPush(orgId: string, storeId: string, outcomes: ChannelPushCatalogItemOutcome[], items: ChannelPushCatalogItem[], phase?: "write-ahead" | "settle"): Promise<PluginResult<void>>;
|
|
202
|
+
pushCatalogToStore(orgId: string, storeId: string, entityIds: string[]): Promise<PluginResult<PushCatalogToStoreResult>>;
|
|
203
|
+
previewCatalogPush(orgId: string, storeId: string, entityIds?: string[]): Promise<PluginResult<CatalogPushPreviewResult>>;
|
|
133
204
|
getCatalogWriteSettings(orgId: string, storeId: string): Promise<PluginResult<CatalogWriteSettings>>;
|
|
134
205
|
updateCatalogWriteEnabled(orgId: string, storeId: string, enabled: boolean): Promise<PluginResult<CatalogWriteSettings>>;
|
|
135
206
|
updateCatalogFieldMapping(orgId: string, storeId: string, mapping: unknown): Promise<PluginResult<CatalogWriteSettings>>;
|
|
@@ -163,6 +234,9 @@ export declare class ChannelConnectorService {
|
|
|
163
234
|
report: ReconcileReport | null;
|
|
164
235
|
driftAlert: boolean;
|
|
165
236
|
}>>;
|
|
237
|
+
listCatalogConflicts(orgId: string, storeId?: string, state?: ChannelCatalogConflict["state"]): Promise<PluginResult<ChannelCatalogConflict[]>>;
|
|
238
|
+
resolveCatalogConflict(orgId: string, id: string, choose: "platform" | "store", actor: Pick<Actor, "userId">): Promise<PluginResult<ChannelCatalogConflict>>;
|
|
239
|
+
private applyStoreConflictValue;
|
|
166
240
|
syncInventory(orgId: string, storeId: string, actor: Actor): Promise<PluginResult<{
|
|
167
241
|
synced: number;
|
|
168
242
|
}>>;
|
|
@@ -208,5 +282,16 @@ export declare class ChannelConnectorService {
|
|
|
208
282
|
listFailedExports(orgId: string): Promise<PluginResult<ChannelOrderExport[]>>;
|
|
209
283
|
retryExport(orgId: string, exportId: string, changedBy: string): Promise<PluginResult<ChannelOrderExport>>;
|
|
210
284
|
abandonExport(orgId: string, exportId: string, changedBy: string, reason?: string): Promise<PluginResult<ChannelOrderExport>>;
|
|
285
|
+
resolveCatalogPushEntityIds(orgId: string, storeId: string, entityIds?: string[]): Promise<string[]>;
|
|
286
|
+
createCatalogPush(orgId: string, storeId: string, entityId: string): Promise<PluginResult<ChannelCatalogPush>>;
|
|
287
|
+
transitionCatalogPush(orgId: string, pushId: string, toState: CatalogPushState, changedBy: string, reason?: string, failureKind?: "definitive" | "transient", payloadSnapshot?: ChannelPushCatalogItem | null): Promise<PluginResult<ChannelCatalogPush>>;
|
|
288
|
+
private recordCatalogPushRevisions;
|
|
289
|
+
executeCatalogPushJob(orgId: string, storeId: string, options: {
|
|
290
|
+
entityIds?: string[];
|
|
291
|
+
cursor?: string;
|
|
292
|
+
forceFieldPaths?: Record<string, FieldPath[]>;
|
|
293
|
+
}, actor: Actor, runtime: {
|
|
294
|
+
jobs: JobsAdapter;
|
|
295
|
+
}): Promise<PluginResult<CatalogPushJobResult>>;
|
|
211
296
|
}
|
|
212
297
|
export {};
|