@deenruv/merchant-plugin 1.0.15 → 1.0.17-dev.14
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/README.md +33 -1
- package/dist/plugin-server/api/platform-integration-admin-resolver.js +16 -3
- package/dist/plugin-server/index.d.ts +2 -2
- package/dist/plugin-server/services/facebook-platform-integration.service.d.ts +9 -12
- package/dist/plugin-server/services/facebook-platform-integration.service.js +14 -6
- package/dist/plugin-server/services/google-merchant-api.d.ts +84 -0
- package/dist/plugin-server/services/google-merchant-api.js +310 -0
- package/dist/plugin-server/services/google-platform-integration.service.d.ts +18 -27
- package/dist/plugin-server/services/google-platform-integration.service.js +164 -272
- package/dist/plugin-server/services/platform-integration.service.js +34 -22
- package/dist/plugin-server/types.d.ts +16 -7
- package/dist/plugin-server/ui/pages/GooglePage.tsx +21 -0
- package/dist/plugin-ui/pages/FacebookPage.d.ts +2 -1
- package/dist/plugin-ui/pages/GooglePage.d.ts +2 -1
- package/dist/plugin-ui/pages/GooglePage.js +10 -3
- package/package.json +12 -12
package/README.md
CHANGED
|
@@ -29,10 +29,42 @@ plugins: [
|
|
|
29
29
|
]
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
+
### Google Merchant API settings
|
|
33
|
+
|
|
34
|
+
The Google integration uses the stable Merchant API Products v1 clients. The
|
|
35
|
+
Google settings page requires:
|
|
36
|
+
|
|
37
|
+
- `merchantId` — the numeric Merchant Center account ID.
|
|
38
|
+
- `dataSource` — the full existing data source resource name in the form
|
|
39
|
+
`accounts/{merchantId}/dataSources/{id}`. Its account segment must match
|
|
40
|
+
`merchantId`.
|
|
41
|
+
- `credentials` — Google OAuth credential JSON for a service account or an
|
|
42
|
+
authorized user. Store and distribute this value as a secret.
|
|
43
|
+
- `brand` — the brand applied to exported product attributes.
|
|
44
|
+
|
|
45
|
+
Product inputs are written only to the configured data source. Merchant API
|
|
46
|
+
processing is asynchronous, so a processed product might not be readable for
|
|
47
|
+
several minutes after a successful write.
|
|
48
|
+
|
|
49
|
+
#### Deployment prerequisites (documentation only)
|
|
50
|
+
|
|
51
|
+
An operator must complete the following outside this plugin and outside the
|
|
52
|
+
application deployment. The plugin does not enable APIs, create or register
|
|
53
|
+
data sources, change Merchant Center configuration, or provision credentials:
|
|
54
|
+
|
|
55
|
+
1. Enable Merchant API for the Google Cloud project that owns the credentials.
|
|
56
|
+
2. Grant the credential identity the required access to the Merchant Center
|
|
57
|
+
account.
|
|
58
|
+
3. Create or select an API product data source in Merchant Center and copy its
|
|
59
|
+
full `accounts/{merchantId}/dataSources/{id}` resource name into the plugin
|
|
60
|
+
settings.
|
|
61
|
+
4. Ensure the credentials can request the
|
|
62
|
+
`https://www.googleapis.com/auth/content` OAuth scope.
|
|
63
|
+
|
|
32
64
|
## Features
|
|
33
65
|
|
|
34
66
|
- Strategy-based product export architecture supporting Google and Facebook platforms
|
|
35
|
-
- Google Merchant Center integration via
|
|
67
|
+
- Google Merchant Center integration via Merchant API Products v1
|
|
36
68
|
- Facebook Commerce integration via Facebook Business SDK
|
|
37
69
|
- Per-platform settings storage and management
|
|
38
70
|
- Bulk product sync to merchant platforms
|
|
@@ -30,13 +30,23 @@ let PlatformIntegrationAdminResolver = class PlatformIntegrationAdminResolver {
|
|
|
30
30
|
return this.platformIntegrationService.getBaseSettings(ctx, args.platform);
|
|
31
31
|
}
|
|
32
32
|
async getMerchantPlatformInfo(ctx, args) {
|
|
33
|
-
var _a
|
|
33
|
+
var _a;
|
|
34
34
|
const settings = await this.platformIntegrationService.getBaseSettings(ctx, args.platform);
|
|
35
35
|
if (args.platform === "google") {
|
|
36
|
+
let isValidConnection = false;
|
|
37
|
+
if (settings) {
|
|
38
|
+
try {
|
|
39
|
+
this.googlePlatformIntegrationService.validateGoogleSettings(settings);
|
|
40
|
+
isValidConnection = true;
|
|
41
|
+
}
|
|
42
|
+
catch (_b) {
|
|
43
|
+
isValidConnection = false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
36
46
|
return [
|
|
37
47
|
{
|
|
38
48
|
productsCount: 0,
|
|
39
|
-
isValidConnection
|
|
49
|
+
isValidConnection,
|
|
40
50
|
},
|
|
41
51
|
];
|
|
42
52
|
}
|
|
@@ -44,7 +54,7 @@ let PlatformIntegrationAdminResolver = class PlatformIntegrationAdminResolver {
|
|
|
44
54
|
return [
|
|
45
55
|
{
|
|
46
56
|
productsCount: 0,
|
|
47
|
-
isValidConnection: ((
|
|
57
|
+
isValidConnection: ((_a = settings === null || settings === void 0 ? void 0 : settings.entries.find((entry) => entry.key === "accessToken")) === null || _a === void 0 ? void 0 : _a.value) !== "",
|
|
48
58
|
},
|
|
49
59
|
];
|
|
50
60
|
}
|
|
@@ -55,6 +65,9 @@ let PlatformIntegrationAdminResolver = class PlatformIntegrationAdminResolver {
|
|
|
55
65
|
platform: args.input.platform,
|
|
56
66
|
entries: args.input.entries.map((entry) => new platform_integration_setting_entity_js_1.MerchantPlatformSetting(entry)),
|
|
57
67
|
});
|
|
68
|
+
if (settingsEntity.platform === "google") {
|
|
69
|
+
this.googlePlatformIntegrationService.validateGoogleSettings(settingsEntity);
|
|
70
|
+
}
|
|
58
71
|
const settings = await this.platformIntegrationService.savePlatformIntegrationSettings(ctx, settingsEntity);
|
|
59
72
|
return settings;
|
|
60
73
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { OnApplicationBootstrap } from "@nestjs/common";
|
|
2
2
|
import { ModuleRef } from "@nestjs/core";
|
|
3
|
-
import { FacebookProduct, GoogleProduct, MerchantExportStrategy, MerchantPluginOptions } from "./types.js";
|
|
3
|
+
import { FacebookProduct, GoogleProcessedProduct, GoogleProduct, GoogleProductInput, MerchantExportStrategy, MerchantPluginOptions } from "./types.js";
|
|
4
4
|
declare class MerchantPlugin implements OnApplicationBootstrap {
|
|
5
5
|
private moduleRef;
|
|
6
6
|
static options: MerchantPluginOptions;
|
|
@@ -11,4 +11,4 @@ declare class MerchantPlugin implements OnApplicationBootstrap {
|
|
|
11
11
|
private initMerchantStrategy;
|
|
12
12
|
private destroyMerchantStrategy;
|
|
13
13
|
}
|
|
14
|
-
export { FacebookProduct, GoogleProduct, MerchantExportStrategy, MerchantPlugin, };
|
|
14
|
+
export { FacebookProduct, GoogleProcessedProduct, GoogleProduct, GoogleProductInput, MerchantExportStrategy, MerchantPlugin, };
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { Product, RequestContext, TransactionalConnection } from "@deenruv/core";
|
|
2
2
|
import { MerchantPlatformSettingsEntity } from "../entities/platform-integration-settings.entity.js";
|
|
3
|
-
import { BaseData, BaseProductData } from "../types.js";
|
|
3
|
+
import { BaseData, BaseProductData, RemoteProduct } from "../types.js";
|
|
4
4
|
import { MerchantStrategyService } from "./merchant-strategy.service.js";
|
|
5
|
-
type
|
|
5
|
+
export type FacebookOperationResult = {
|
|
6
6
|
status: "success" | "error";
|
|
7
7
|
message?: string;
|
|
8
8
|
};
|
|
9
|
+
export declare function executeFacebookBatches(batches: readonly BaseProductData<BaseData>[], executeBatch: (batch: BaseProductData<BaseData>) => Promise<FacebookOperationResult>): Promise<FacebookOperationResult>;
|
|
9
10
|
export declare class FacebookPlatformIntegrationService {
|
|
10
11
|
private readonly connection;
|
|
11
12
|
private readonly strategy;
|
|
@@ -13,33 +14,30 @@ export declare class FacebookPlatformIntegrationService {
|
|
|
13
14
|
private log;
|
|
14
15
|
private error;
|
|
15
16
|
constructor(connection: TransactionalConnection, strategy: MerchantStrategyService);
|
|
16
|
-
removeOrphanItems(ctx: RequestContext, items:
|
|
17
|
-
getAllProducts(ctx: RequestContext): Promise<
|
|
18
|
-
communicateID: any;
|
|
19
|
-
name: any;
|
|
20
|
-
}[]>;
|
|
17
|
+
removeOrphanItems(ctx: RequestContext, items: RemoteProduct[]): Promise<void>;
|
|
18
|
+
getAllProducts(ctx: RequestContext): Promise<RemoteProduct[]>;
|
|
21
19
|
private withCatalog;
|
|
22
20
|
private sendBatch;
|
|
23
21
|
createProduct({ ctx, data, }: {
|
|
24
22
|
ctx: RequestContext;
|
|
25
23
|
data: BaseProductData<BaseData>;
|
|
26
24
|
entity?: Product;
|
|
27
|
-
}): Promise<
|
|
25
|
+
}): Promise<FacebookOperationResult>;
|
|
28
26
|
updateProduct({ ctx, data, }: {
|
|
29
27
|
ctx: RequestContext;
|
|
30
28
|
data: BaseProductData<BaseData>;
|
|
31
29
|
entity?: Product;
|
|
32
|
-
}): Promise<
|
|
30
|
+
}): Promise<FacebookOperationResult>;
|
|
33
31
|
deleteProduct({ ctx, data, }: {
|
|
34
32
|
ctx: RequestContext;
|
|
35
33
|
data: BaseProductData<BaseData>;
|
|
36
34
|
entity?: Product;
|
|
37
|
-
}): Promise<
|
|
35
|
+
}): Promise<FacebookOperationResult>;
|
|
38
36
|
batchProductsAction({ ctx, products, }: {
|
|
39
37
|
ctx: RequestContext;
|
|
40
38
|
products: BaseProductData<BaseData>[];
|
|
41
39
|
entity?: Product;
|
|
42
|
-
}): Promise<
|
|
40
|
+
}): Promise<FacebookOperationResult>;
|
|
43
41
|
setFacebookSettings(ctx: RequestContext, rawSettings?: MerchantPlatformSettingsEntity): Promise<{
|
|
44
42
|
autoUpdate: boolean;
|
|
45
43
|
accessToken: string;
|
|
@@ -48,4 +46,3 @@ export declare class FacebookPlatformIntegrationService {
|
|
|
48
46
|
} | null>;
|
|
49
47
|
private prepareFacebookProductPayload;
|
|
50
48
|
}
|
|
51
|
-
export {};
|
|
@@ -20,13 +20,22 @@ var __rest = (this && this.__rest) || function (s, e) {
|
|
|
20
20
|
return t;
|
|
21
21
|
};
|
|
22
22
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
-
exports.FacebookPlatformIntegrationService = void 0;
|
|
23
|
+
exports.FacebookPlatformIntegrationService = exports.executeFacebookBatches = void 0;
|
|
24
24
|
const core_1 = require("@deenruv/core");
|
|
25
25
|
const common_1 = require("@nestjs/common");
|
|
26
26
|
const facebook_nodejs_business_sdk_1 = require("facebook-nodejs-business-sdk");
|
|
27
27
|
const platform_integration_settings_entity_js_1 = require("../entities/platform-integration-settings.entity.js");
|
|
28
28
|
const merchant_strategy_service_js_1 = require("./merchant-strategy.service.js");
|
|
29
29
|
const typeorm_1 = require("typeorm");
|
|
30
|
+
async function executeFacebookBatches(batches, executeBatch) {
|
|
31
|
+
for (const batch of batches) {
|
|
32
|
+
const result = await executeBatch(batch);
|
|
33
|
+
if (result.status === "error")
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
36
|
+
return { status: "success", message: "Products processed successfully" };
|
|
37
|
+
}
|
|
38
|
+
exports.executeFacebookBatches = executeFacebookBatches;
|
|
30
39
|
let FacebookPlatformIntegrationService = class FacebookPlatformIntegrationService {
|
|
31
40
|
constructor(connection, strategy) {
|
|
32
41
|
this.connection = connection;
|
|
@@ -160,12 +169,11 @@ let FacebookPlatformIntegrationService = class FacebookPlatformIntegrationServic
|
|
|
160
169
|
}
|
|
161
170
|
async batchProductsAction({ ctx, products, }) {
|
|
162
171
|
const flatten = products.flat();
|
|
163
|
-
const
|
|
164
|
-
for (let i = 0; i < flatten.length; i +=
|
|
165
|
-
|
|
166
|
-
await this.sendBatch({ ctx, method: "UPDATE", data: batch });
|
|
172
|
+
const batches = [];
|
|
173
|
+
for (let i = 0; i < flatten.length; i += 500) {
|
|
174
|
+
batches.push(flatten.slice(i, i + 500));
|
|
167
175
|
}
|
|
168
|
-
return {
|
|
176
|
+
return executeFacebookBatches(batches, (batch) => this.sendBatch({ ctx, method: "UPDATE", data: batch }));
|
|
169
177
|
}
|
|
170
178
|
async setFacebookSettings(ctx, rawSettings) {
|
|
171
179
|
var _a;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { v1 } from "@google-shopping/products";
|
|
2
|
+
import type { protos } from "@google-shopping/products";
|
|
3
|
+
import type { GoogleProcessedProduct, GoogleProduct, GoogleProductInput, RemoteProduct } from "../types.js";
|
|
4
|
+
export declare const GOOGLE_CONTENT_LANGUAGE = "pl";
|
|
5
|
+
export declare const GOOGLE_FEED_LABEL = "PL";
|
|
6
|
+
export declare const DEFAULT_GOOGLE_WRITE_CONCURRENCY = 4;
|
|
7
|
+
type MerchantClientOptions = NonNullable<ConstructorParameters<typeof v1.ProductsServiceClient>[0]>;
|
|
8
|
+
export type GoogleMerchantCredentials = NonNullable<MerchantClientOptions["credentials"]>;
|
|
9
|
+
export type GoogleMerchantSettings = {
|
|
10
|
+
accountId: string;
|
|
11
|
+
autoUpdate: boolean;
|
|
12
|
+
brand: string;
|
|
13
|
+
credentials: GoogleMerchantCredentials;
|
|
14
|
+
dataSource: string;
|
|
15
|
+
};
|
|
16
|
+
export type GoogleMerchantClients = {
|
|
17
|
+
productInputs: v1.ProductInputsServiceClient;
|
|
18
|
+
products: v1.ProductsServiceClient;
|
|
19
|
+
};
|
|
20
|
+
type InsertProductInputRequest = protos.google.shopping.merchant.products.v1.IInsertProductInputRequest;
|
|
21
|
+
type UpdateProductInputRequest = protos.google.shopping.merchant.products.v1.IUpdateProductInputRequest;
|
|
22
|
+
type DeleteProductInputRequest = protos.google.shopping.merchant.products.v1.IDeleteProductInputRequest;
|
|
23
|
+
type ListProductsRequest = protos.google.shopping.merchant.products.v1.IListProductsRequest;
|
|
24
|
+
export type GoogleWriteOperation = {
|
|
25
|
+
communicateID: string;
|
|
26
|
+
method: "insert";
|
|
27
|
+
request: InsertProductInputRequest;
|
|
28
|
+
} | {
|
|
29
|
+
communicateID: string;
|
|
30
|
+
method: "update";
|
|
31
|
+
request: UpdateProductInputRequest;
|
|
32
|
+
} | {
|
|
33
|
+
communicateID: string;
|
|
34
|
+
method: "delete";
|
|
35
|
+
request: DeleteProductInputRequest;
|
|
36
|
+
};
|
|
37
|
+
export type MerchantOperationResult<T> = {
|
|
38
|
+
index: number;
|
|
39
|
+
item: T;
|
|
40
|
+
status: "success";
|
|
41
|
+
} | {
|
|
42
|
+
error: unknown;
|
|
43
|
+
index: number;
|
|
44
|
+
item: T;
|
|
45
|
+
status: "error";
|
|
46
|
+
};
|
|
47
|
+
export type MerchantOperationSummary<T> = {
|
|
48
|
+
failures: Array<Extract<MerchantOperationResult<T>, {
|
|
49
|
+
status: "error";
|
|
50
|
+
}>>;
|
|
51
|
+
results: Array<MerchantOperationResult<T>>;
|
|
52
|
+
status: "success" | "error";
|
|
53
|
+
};
|
|
54
|
+
export interface GoogleProductInputsWriter {
|
|
55
|
+
deleteProductInput(request: DeleteProductInputRequest): Promise<unknown>;
|
|
56
|
+
insertProductInput(request: InsertProductInputRequest): Promise<unknown>;
|
|
57
|
+
updateProductInput(request: UpdateProductInputRequest): Promise<unknown>;
|
|
58
|
+
}
|
|
59
|
+
export interface GoogleProductsReader {
|
|
60
|
+
listProductsAsync(request: ListProductsRequest): AsyncIterable<GoogleProcessedProduct>;
|
|
61
|
+
}
|
|
62
|
+
type SettingEntry = {
|
|
63
|
+
key: string;
|
|
64
|
+
value: string;
|
|
65
|
+
};
|
|
66
|
+
export declare function parseGoogleCredentials(rawCredentials: string): GoogleMerchantCredentials;
|
|
67
|
+
export declare function normalizeMerchantId(rawMerchantId: string): string;
|
|
68
|
+
export declare function normalizeDataSource(rawDataSource: string, merchantId: string): string;
|
|
69
|
+
export declare function parseGoogleMerchantSettings(entries: readonly SettingEntry[]): GoogleMerchantSettings;
|
|
70
|
+
export declare function createGoogleMerchantClients(credentials: GoogleMerchantCredentials): GoogleMerchantClients;
|
|
71
|
+
export declare function buildGoogleProductIdentifier(offerId: string): string;
|
|
72
|
+
export declare function buildGoogleProductName(accountId: string, offerId: string): string;
|
|
73
|
+
export declare function buildGoogleProductInputName(accountId: string, offerId: string): string;
|
|
74
|
+
export declare function toGoogleProductInput(product: GoogleProduct, brand: string): GoogleProductInput;
|
|
75
|
+
export declare function buildGoogleUpdateMask(productInput: GoogleProductInput): protos.google.protobuf.IFieldMask;
|
|
76
|
+
export declare function createGoogleInsertOperation(product: GoogleProduct, settings: GoogleMerchantSettings): GoogleWriteOperation;
|
|
77
|
+
export declare function createGoogleUpdateOperation(product: GoogleProduct, settings: GoogleMerchantSettings): GoogleWriteOperation;
|
|
78
|
+
export declare function createGoogleDeleteOperation(communicateID: string, settings: GoogleMerchantSettings): GoogleWriteOperation;
|
|
79
|
+
export declare function runBoundedMerchantOperations<T>(items: readonly T[], operation: (item: T, index: number) => Promise<void>, concurrency?: number): Promise<Array<MerchantOperationResult<T>>>;
|
|
80
|
+
export declare function executeGoogleWriteOperations(client: GoogleProductInputsWriter, operations: readonly GoogleWriteOperation[], concurrency?: number): Promise<Array<MerchantOperationResult<GoogleWriteOperation>>>;
|
|
81
|
+
export declare function summarizeMerchantOperations<T>(results: Array<MerchantOperationResult<T>>): MerchantOperationSummary<T>;
|
|
82
|
+
export declare function collectGoogleProductsForDataSource(client: GoogleProductsReader, accountId: string, dataSource: string): Promise<RemoteProduct[]>;
|
|
83
|
+
export declare function selectRemoteOrphanProducts(remoteProducts: readonly RemoteProduct[], localProducts: readonly Pick<GoogleProduct, "communicateID">[]): RemoteProduct[];
|
|
84
|
+
export {};
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __asyncValues = (this && this.__asyncValues) || function (o) {
|
|
3
|
+
if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
|
|
4
|
+
var m = o[Symbol.asyncIterator], i;
|
|
5
|
+
return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i);
|
|
6
|
+
function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }
|
|
7
|
+
function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
|
|
8
|
+
};
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.selectRemoteOrphanProducts = exports.collectGoogleProductsForDataSource = exports.summarizeMerchantOperations = exports.executeGoogleWriteOperations = exports.runBoundedMerchantOperations = exports.createGoogleDeleteOperation = exports.createGoogleUpdateOperation = exports.createGoogleInsertOperation = exports.buildGoogleUpdateMask = exports.toGoogleProductInput = exports.buildGoogleProductInputName = exports.buildGoogleProductName = exports.buildGoogleProductIdentifier = exports.createGoogleMerchantClients = exports.parseGoogleMerchantSettings = exports.normalizeDataSource = exports.normalizeMerchantId = exports.parseGoogleCredentials = exports.DEFAULT_GOOGLE_WRITE_CONCURRENCY = exports.GOOGLE_FEED_LABEL = exports.GOOGLE_CONTENT_LANGUAGE = void 0;
|
|
11
|
+
const products_1 = require("@google-shopping/products");
|
|
12
|
+
const GOOGLE_OAUTH_SCOPE = "https://www.googleapis.com/auth/content";
|
|
13
|
+
const PLAIN_IDENTIFIER_COMPONENT = /^[A-Za-z0-9_-]+$/;
|
|
14
|
+
const MERCHANT_ID_PATTERN = /^[1-9]\d*$/;
|
|
15
|
+
const DATA_SOURCE_PATTERN = /^accounts\/([1-9]\d*)\/dataSources\/([1-9]\d*)$/;
|
|
16
|
+
exports.GOOGLE_CONTENT_LANGUAGE = "pl";
|
|
17
|
+
exports.GOOGLE_FEED_LABEL = "PL";
|
|
18
|
+
exports.DEFAULT_GOOGLE_WRITE_CONCURRENCY = 4;
|
|
19
|
+
function isRecord(value) {
|
|
20
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
21
|
+
}
|
|
22
|
+
function optionalString(value, key) {
|
|
23
|
+
const candidate = value[key];
|
|
24
|
+
return typeof candidate === "string" && candidate.length > 0
|
|
25
|
+
? candidate
|
|
26
|
+
: undefined;
|
|
27
|
+
}
|
|
28
|
+
function requiredString(value, key) {
|
|
29
|
+
const candidate = optionalString(value, key);
|
|
30
|
+
if (!candidate || candidate.trim().length === 0) {
|
|
31
|
+
throw new Error(`Google credentials field ${key} is required`);
|
|
32
|
+
}
|
|
33
|
+
return candidate;
|
|
34
|
+
}
|
|
35
|
+
function parseGoogleCredentials(rawCredentials) {
|
|
36
|
+
let parsed;
|
|
37
|
+
try {
|
|
38
|
+
parsed = JSON.parse(rawCredentials);
|
|
39
|
+
}
|
|
40
|
+
catch (_a) {
|
|
41
|
+
throw new Error("Google credentials must be valid JSON");
|
|
42
|
+
}
|
|
43
|
+
if (!isRecord(parsed)) {
|
|
44
|
+
throw new Error("Google credentials must be a JSON object");
|
|
45
|
+
}
|
|
46
|
+
const credentialType = optionalString(parsed, "type");
|
|
47
|
+
if (credentialType === "authorized_user") {
|
|
48
|
+
return Object.assign({ type: "authorized_user", client_id: requiredString(parsed, "client_id").trim(), client_secret: requiredString(parsed, "client_secret"), refresh_token: requiredString(parsed, "refresh_token") }, (optionalString(parsed, "quota_project_id") && {
|
|
49
|
+
quota_project_id: optionalString(parsed, "quota_project_id"),
|
|
50
|
+
}));
|
|
51
|
+
}
|
|
52
|
+
if (credentialType === undefined || credentialType === "service_account") {
|
|
53
|
+
return Object.assign(Object.assign(Object.assign(Object.assign({ type: "service_account", client_email: requiredString(parsed, "client_email").trim(), private_key: requiredString(parsed, "private_key") }, (optionalString(parsed, "private_key_id") && {
|
|
54
|
+
private_key_id: optionalString(parsed, "private_key_id"),
|
|
55
|
+
})), (optionalString(parsed, "project_id") && {
|
|
56
|
+
project_id: optionalString(parsed, "project_id"),
|
|
57
|
+
})), (optionalString(parsed, "client_id") && {
|
|
58
|
+
client_id: optionalString(parsed, "client_id"),
|
|
59
|
+
})), (optionalString(parsed, "quota_project_id") && {
|
|
60
|
+
quota_project_id: optionalString(parsed, "quota_project_id"),
|
|
61
|
+
}));
|
|
62
|
+
}
|
|
63
|
+
throw new Error(`Unsupported Google credentials type: ${credentialType !== null && credentialType !== void 0 ? credentialType : "missing"}`);
|
|
64
|
+
}
|
|
65
|
+
exports.parseGoogleCredentials = parseGoogleCredentials;
|
|
66
|
+
function normalizeMerchantId(rawMerchantId) {
|
|
67
|
+
const merchantId = rawMerchantId.trim();
|
|
68
|
+
if (!MERCHANT_ID_PATTERN.test(merchantId)) {
|
|
69
|
+
throw new Error("Merchant ID must be a positive numeric account ID");
|
|
70
|
+
}
|
|
71
|
+
return merchantId;
|
|
72
|
+
}
|
|
73
|
+
exports.normalizeMerchantId = normalizeMerchantId;
|
|
74
|
+
function normalizeDataSource(rawDataSource, merchantId) {
|
|
75
|
+
const dataSource = rawDataSource.trim();
|
|
76
|
+
const match = DATA_SOURCE_PATTERN.exec(dataSource);
|
|
77
|
+
if (!match) {
|
|
78
|
+
throw new Error("Google dataSource must match accounts/{merchantId}/dataSources/{id}");
|
|
79
|
+
}
|
|
80
|
+
if (match[1] !== merchantId) {
|
|
81
|
+
throw new Error("Google dataSource account must match Merchant ID");
|
|
82
|
+
}
|
|
83
|
+
return `accounts/${match[1]}/dataSources/${match[2]}`;
|
|
84
|
+
}
|
|
85
|
+
exports.normalizeDataSource = normalizeDataSource;
|
|
86
|
+
function parseGoogleMerchantSettings(entries) {
|
|
87
|
+
var _a, _b, _c, _d, _e;
|
|
88
|
+
const getValue = (key) => { var _a; return (_a = entries.find((entry) => entry.key === key)) === null || _a === void 0 ? void 0 : _a.value; };
|
|
89
|
+
const accountId = normalizeMerchantId((_a = getValue("merchantId")) !== null && _a !== void 0 ? _a : "");
|
|
90
|
+
const dataSource = normalizeDataSource((_b = getValue("dataSource")) !== null && _b !== void 0 ? _b : "", accountId);
|
|
91
|
+
const brand = ((_c = getValue("brand")) !== null && _c !== void 0 ? _c : "").trim();
|
|
92
|
+
if (!brand) {
|
|
93
|
+
throw new Error("Google brand is required");
|
|
94
|
+
}
|
|
95
|
+
const credentials = parseGoogleCredentials((_d = getValue("credentials")) !== null && _d !== void 0 ? _d : "");
|
|
96
|
+
return {
|
|
97
|
+
accountId,
|
|
98
|
+
autoUpdate: ((_e = getValue("autoUpdate")) !== null && _e !== void 0 ? _e : "").toLowerCase() === "true",
|
|
99
|
+
brand,
|
|
100
|
+
credentials,
|
|
101
|
+
dataSource,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
exports.parseGoogleMerchantSettings = parseGoogleMerchantSettings;
|
|
105
|
+
function createGoogleMerchantClients(credentials) {
|
|
106
|
+
const options = {
|
|
107
|
+
credentials,
|
|
108
|
+
scopes: [GOOGLE_OAUTH_SCOPE],
|
|
109
|
+
};
|
|
110
|
+
return {
|
|
111
|
+
productInputs: new products_1.v1.ProductInputsServiceClient(options),
|
|
112
|
+
products: new products_1.v1.ProductsServiceClient(options),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
exports.createGoogleMerchantClients = createGoogleMerchantClients;
|
|
116
|
+
function buildGoogleProductIdentifier(offerId) {
|
|
117
|
+
if (!offerId) {
|
|
118
|
+
throw new Error("Google offer ID is required");
|
|
119
|
+
}
|
|
120
|
+
const components = [exports.GOOGLE_CONTENT_LANGUAGE, exports.GOOGLE_FEED_LABEL, offerId];
|
|
121
|
+
const identifier = components.join("~");
|
|
122
|
+
return components.every((component) => PLAIN_IDENTIFIER_COMPONENT.test(component))
|
|
123
|
+
? identifier
|
|
124
|
+
: Buffer.from(identifier, "utf8").toString("base64url");
|
|
125
|
+
}
|
|
126
|
+
exports.buildGoogleProductIdentifier = buildGoogleProductIdentifier;
|
|
127
|
+
function buildGoogleProductName(accountId, offerId) {
|
|
128
|
+
return `accounts/${accountId}/products/${buildGoogleProductIdentifier(offerId)}`;
|
|
129
|
+
}
|
|
130
|
+
exports.buildGoogleProductName = buildGoogleProductName;
|
|
131
|
+
function buildGoogleProductInputName(accountId, offerId) {
|
|
132
|
+
return `accounts/${accountId}/productInputs/${buildGoogleProductIdentifier(offerId)}`;
|
|
133
|
+
}
|
|
134
|
+
exports.buildGoogleProductInputName = buildGoogleProductInputName;
|
|
135
|
+
function toGoogleProductInput(product, brand) {
|
|
136
|
+
return Object.assign(Object.assign({ offerId: String(product.communicateID), contentLanguage: exports.GOOGLE_CONTENT_LANGUAGE, feedLabel: exports.GOOGLE_FEED_LABEL, productAttributes: Object.assign(Object.assign({}, product.productAttributes), { brand }) }, (product.customAttributes && {
|
|
137
|
+
customAttributes: product.customAttributes,
|
|
138
|
+
})), (product.versionNumber !== undefined && {
|
|
139
|
+
versionNumber: product.versionNumber,
|
|
140
|
+
}));
|
|
141
|
+
}
|
|
142
|
+
exports.toGoogleProductInput = toGoogleProductInput;
|
|
143
|
+
function camelToSnake(value) {
|
|
144
|
+
return value.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
|
|
145
|
+
}
|
|
146
|
+
function buildGoogleUpdateMask(productInput) {
|
|
147
|
+
var _a, _b;
|
|
148
|
+
const paths = Object.entries((_a = productInput.productAttributes) !== null && _a !== void 0 ? _a : {})
|
|
149
|
+
.filter(([, value]) => value !== undefined)
|
|
150
|
+
.map(([key]) => `product_attributes.${camelToSnake(key)}`);
|
|
151
|
+
for (const customAttribute of (_b = productInput.customAttributes) !== null && _b !== void 0 ? _b : []) {
|
|
152
|
+
if (customAttribute.name) {
|
|
153
|
+
paths.push(`custom_attribute.${customAttribute.name}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (paths.length === 0) {
|
|
157
|
+
throw new Error("Google update requires at least one mutable attribute");
|
|
158
|
+
}
|
|
159
|
+
return { paths };
|
|
160
|
+
}
|
|
161
|
+
exports.buildGoogleUpdateMask = buildGoogleUpdateMask;
|
|
162
|
+
function createGoogleInsertOperation(product, settings) {
|
|
163
|
+
const communicateID = String(product.communicateID);
|
|
164
|
+
return {
|
|
165
|
+
communicateID,
|
|
166
|
+
method: "insert",
|
|
167
|
+
request: {
|
|
168
|
+
parent: `accounts/${settings.accountId}`,
|
|
169
|
+
dataSource: settings.dataSource,
|
|
170
|
+
productInput: toGoogleProductInput(product, settings.brand),
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
exports.createGoogleInsertOperation = createGoogleInsertOperation;
|
|
175
|
+
function createGoogleUpdateOperation(product, settings) {
|
|
176
|
+
const communicateID = String(product.communicateID);
|
|
177
|
+
const input = toGoogleProductInput(product, settings.brand);
|
|
178
|
+
const productInput = Object.assign({ name: buildGoogleProductInputName(settings.accountId, communicateID), productAttributes: input.productAttributes }, (input.customAttributes && {
|
|
179
|
+
customAttributes: input.customAttributes,
|
|
180
|
+
}));
|
|
181
|
+
return {
|
|
182
|
+
communicateID,
|
|
183
|
+
method: "update",
|
|
184
|
+
request: {
|
|
185
|
+
dataSource: settings.dataSource,
|
|
186
|
+
productInput,
|
|
187
|
+
updateMask: buildGoogleUpdateMask(productInput),
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
exports.createGoogleUpdateOperation = createGoogleUpdateOperation;
|
|
192
|
+
function createGoogleDeleteOperation(communicateID, settings) {
|
|
193
|
+
return {
|
|
194
|
+
communicateID,
|
|
195
|
+
method: "delete",
|
|
196
|
+
request: {
|
|
197
|
+
name: buildGoogleProductInputName(settings.accountId, communicateID),
|
|
198
|
+
dataSource: settings.dataSource,
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
exports.createGoogleDeleteOperation = createGoogleDeleteOperation;
|
|
203
|
+
async function runBoundedMerchantOperations(items, operation, concurrency = exports.DEFAULT_GOOGLE_WRITE_CONCURRENCY) {
|
|
204
|
+
if (!Number.isInteger(concurrency) || concurrency < 1) {
|
|
205
|
+
throw new Error("Merchant operation concurrency must be a positive integer");
|
|
206
|
+
}
|
|
207
|
+
if (items.length === 0)
|
|
208
|
+
return [];
|
|
209
|
+
const results = new Array(items.length);
|
|
210
|
+
let nextIndex = 0;
|
|
211
|
+
const workerCount = Math.min(concurrency, items.length);
|
|
212
|
+
const workers = Array.from({ length: workerCount }, async () => {
|
|
213
|
+
while (true) {
|
|
214
|
+
const index = nextIndex;
|
|
215
|
+
nextIndex += 1;
|
|
216
|
+
if (index >= items.length)
|
|
217
|
+
return;
|
|
218
|
+
const item = items[index];
|
|
219
|
+
try {
|
|
220
|
+
await operation(item, index);
|
|
221
|
+
results[index] = { index, item, status: "success" };
|
|
222
|
+
}
|
|
223
|
+
catch (error) {
|
|
224
|
+
results[index] = { error, index, item, status: "error" };
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
await Promise.all(workers);
|
|
229
|
+
return results;
|
|
230
|
+
}
|
|
231
|
+
exports.runBoundedMerchantOperations = runBoundedMerchantOperations;
|
|
232
|
+
function isNotFoundError(error) {
|
|
233
|
+
if (!isRecord(error))
|
|
234
|
+
return false;
|
|
235
|
+
if (error.code === 5 || error.code === 404 || error.code === "404")
|
|
236
|
+
return true;
|
|
237
|
+
const response = error.response;
|
|
238
|
+
return isRecord(response) && response.status === 404;
|
|
239
|
+
}
|
|
240
|
+
async function executeGoogleWriteOperations(client, operations, concurrency = exports.DEFAULT_GOOGLE_WRITE_CONCURRENCY) {
|
|
241
|
+
return runBoundedMerchantOperations(operations, async (operation) => {
|
|
242
|
+
try {
|
|
243
|
+
if (operation.method === "insert") {
|
|
244
|
+
await client.insertProductInput(operation.request);
|
|
245
|
+
}
|
|
246
|
+
else if (operation.method === "update") {
|
|
247
|
+
await client.updateProductInput(operation.request);
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
await client.deleteProductInput(operation.request);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
catch (error) {
|
|
254
|
+
if (operation.method === "delete" && isNotFoundError(error))
|
|
255
|
+
return;
|
|
256
|
+
throw error;
|
|
257
|
+
}
|
|
258
|
+
}, concurrency);
|
|
259
|
+
}
|
|
260
|
+
exports.executeGoogleWriteOperations = executeGoogleWriteOperations;
|
|
261
|
+
function summarizeMerchantOperations(results) {
|
|
262
|
+
const failures = results.filter((result) => result.status === "error");
|
|
263
|
+
return {
|
|
264
|
+
failures,
|
|
265
|
+
results,
|
|
266
|
+
status: failures.length === 0 ? "success" : "error",
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
exports.summarizeMerchantOperations = summarizeMerchantOperations;
|
|
270
|
+
async function collectGoogleProductsForDataSource(client, accountId, dataSource) {
|
|
271
|
+
var _a, e_1, _b, _c;
|
|
272
|
+
var _d, _e;
|
|
273
|
+
const products = new Map();
|
|
274
|
+
try {
|
|
275
|
+
for (var _f = true, _g = __asyncValues(client.listProductsAsync({
|
|
276
|
+
parent: `accounts/${accountId}`,
|
|
277
|
+
pageSize: 1000,
|
|
278
|
+
})), _h; _h = await _g.next(), _a = _h.done, !_a; _f = true) {
|
|
279
|
+
_c = _h.value;
|
|
280
|
+
_f = false;
|
|
281
|
+
const product = _c;
|
|
282
|
+
if (product.dataSource !== dataSource || !product.offerId)
|
|
283
|
+
continue;
|
|
284
|
+
products.set(product.offerId, {
|
|
285
|
+
communicateID: product.offerId,
|
|
286
|
+
name: (_e = (_d = product.productAttributes) === null || _d === void 0 ? void 0 : _d.title) !== null && _e !== void 0 ? _e : undefined,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
catch (e_1_1) { e_1 = { error: e_1_1 }; }
|
|
291
|
+
finally {
|
|
292
|
+
try {
|
|
293
|
+
if (!_f && !_a && (_b = _g.return)) await _b.call(_g);
|
|
294
|
+
}
|
|
295
|
+
finally { if (e_1) throw e_1.error; }
|
|
296
|
+
}
|
|
297
|
+
return [...products.values()];
|
|
298
|
+
}
|
|
299
|
+
exports.collectGoogleProductsForDataSource = collectGoogleProductsForDataSource;
|
|
300
|
+
function selectRemoteOrphanProducts(remoteProducts, localProducts) {
|
|
301
|
+
const localIds = new Set(localProducts.map((product) => String(product.communicateID)));
|
|
302
|
+
const selected = new Map();
|
|
303
|
+
for (const remoteProduct of remoteProducts) {
|
|
304
|
+
if (!localIds.has(remoteProduct.communicateID)) {
|
|
305
|
+
selected.set(remoteProduct.communicateID, remoteProduct);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return [...selected.values()];
|
|
309
|
+
}
|
|
310
|
+
exports.selectRemoteOrphanProducts = selectRemoteOrphanProducts;
|