@deenruv/merchant-plugin 1.0.17-dev.33 → 1.0.17-dev.35

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.
@@ -8,6 +8,9 @@ export declare class PlatformIntegrationAdminResolver {
8
8
  private facebookPlatformIntegrationService;
9
9
  private platformIntegrationService;
10
10
  constructor(googlePlatformIntegrationService: GooglePlatformIntegrationService, facebookPlatformIntegrationService: FacebookPlatformIntegrationService, platformIntegrationService: PlatformIntegrationService);
11
+ getGoogleMerchantDataSources(ctx: RequestContext, args: {
12
+ merchantId: string;
13
+ }): Promise<string[]>;
11
14
  getMerchantPlatformSettings(ctx: RequestContext, args: {
12
15
  platform: string;
13
16
  }): Promise<MerchantPlatformSettingsEntity | null>;
@@ -26,6 +26,9 @@ let PlatformIntegrationAdminResolver = class PlatformIntegrationAdminResolver {
26
26
  this.facebookPlatformIntegrationService = facebookPlatformIntegrationService;
27
27
  this.platformIntegrationService = platformIntegrationService;
28
28
  }
29
+ async getGoogleMerchantDataSources(ctx, args) {
30
+ return this.googlePlatformIntegrationService.discoverGoogleMerchantDataSources(ctx, args.merchantId);
31
+ }
29
32
  async getMerchantPlatformSettings(ctx, args) {
30
33
  const settings = await this.platformIntegrationService.getBaseSettings(ctx, args.platform);
31
34
  return settings ? this.redactCredentials(settings) : null;
@@ -85,6 +88,15 @@ let PlatformIntegrationAdminResolver = class PlatformIntegrationAdminResolver {
85
88
  }
86
89
  };
87
90
  exports.PlatformIntegrationAdminResolver = PlatformIntegrationAdminResolver;
91
+ __decorate([
92
+ (0, graphql_1.Query)(),
93
+ (0, core_1.Allow)(core_1.Permission.ReadSettings),
94
+ __param(0, (0, core_1.Ctx)()),
95
+ __param(1, (0, graphql_1.Args)()),
96
+ __metadata("design:type", Function),
97
+ __metadata("design:paramtypes", [core_1.RequestContext, Object]),
98
+ __metadata("design:returntype", Promise)
99
+ ], PlatformIntegrationAdminResolver.prototype, "getGoogleMerchantDataSources", null);
88
100
  __decorate([
89
101
  (0, graphql_1.Query)(),
90
102
  __param(0, (0, core_1.Ctx)()),
@@ -88,6 +88,7 @@ exports.adminApiExtensions = (0, graphql_tag_1.gql) `
88
88
  }
89
89
 
90
90
  extend type Query {
91
+ getGoogleMerchantDataSources(merchantId: String!): [String!]!
91
92
  getMerchantPlatformSettings(
92
93
  platform: String!
93
94
  ): MerchantPlatformSettingsEntity
@@ -5,6 +5,7 @@ import type { GoogleProcessedProduct, GoogleProduct, GoogleProductInput, RemoteP
5
5
  export declare const GOOGLE_CONTENT_LANGUAGE = "pl";
6
6
  export declare const GOOGLE_FEED_LABEL = "PL";
7
7
  export declare const DEFAULT_GOOGLE_WRITE_CONCURRENCY = 4;
8
+ export declare const MAX_GOOGLE_DATA_SOURCE_DISCOVERY_RESULTS = 100;
8
9
  type MerchantClientOptions = NonNullable<ConstructorParameters<typeof v1.ProductsServiceClient>[0]>;
9
10
  export type GoogleMerchantCredentials = NonNullable<MerchantClientOptions["credentials"]>;
10
11
  export type GoogleMerchantSettings = {
@@ -70,6 +71,15 @@ export interface GoogleDataSourcesReader {
70
71
  name: string;
71
72
  }): Promise<unknown>;
72
73
  }
74
+ export interface GoogleDataSourcesDiscoveryClient {
75
+ close(): Promise<void>;
76
+ listDataSourcesAsync(request: {
77
+ parent: string;
78
+ }): AsyncIterable<{
79
+ name?: string | null;
80
+ }>;
81
+ }
82
+ export type GoogleDataSourcesDiscoveryClientFactory = (credentials: GoogleMerchantCredentials) => GoogleDataSourcesDiscoveryClient;
73
83
  export type GoogleMerchantRetryOptions = {
74
84
  delay?: (attempt: number) => Promise<void>;
75
85
  maxAttempts?: number;
@@ -106,6 +116,9 @@ export declare function normalizeGoogleMerchantError(error: unknown): Normalized
106
116
  export declare function parseGoogleCredentials(rawCredentials: string): GoogleMerchantCredentials;
107
117
  export declare function normalizeMerchantId(rawMerchantId: string): string;
108
118
  export declare function normalizeDataSource(rawDataSource: string, merchantId: string): string;
119
+ export declare function parseGoogleDataSourceDiscoverySettings(requestedMerchantId: string, entries: readonly SettingEntry[]): Pick<GoogleMerchantSettings, "accountId" | "credentials">;
120
+ export declare function createGoogleDataSourcesDiscoveryClient(credentials: GoogleMerchantCredentials): GoogleDataSourcesDiscoveryClient;
121
+ export declare function discoverGoogleMerchantDataSources(requestedMerchantId: string, entries: readonly SettingEntry[], createClient?: GoogleDataSourcesDiscoveryClientFactory): Promise<string[]>;
109
122
  export declare function parseGoogleMerchantSettings(entries: readonly SettingEntry[]): GoogleMerchantSettings;
110
123
  export declare function createGoogleMerchantClients(credentials: GoogleMerchantCredentials): GoogleMerchantClients;
111
124
  export declare function buildGoogleProductIdentifier(offerId: string, contentLanguage?: string, feedLabel?: string): string;
@@ -18,7 +18,7 @@ var __rest = (this && this.__rest) || function (s, e) {
18
18
  return t;
19
19
  };
20
20
  Object.defineProperty(exports, "__esModule", { value: true });
21
- exports.selectRemoteOrphanProducts = exports.diagnoseGoogleMerchantConnection = exports.collectGoogleProductsForDataSource = exports.summarizeMerchantOperations = exports.executeGoogleWriteOperations = exports.runBoundedMerchantOperations = exports.createGoogleDeleteOperation = exports.createGoogleUpdateOperation = exports.createGoogleInsertOperation = exports.buildGoogleUpdateMask = exports.validateGoogleProductUrls = exports.toGoogleProductInput = exports.buildGoogleProductInputName = exports.buildGoogleProductName = exports.buildGoogleProductIdentifier = exports.createGoogleMerchantClients = exports.parseGoogleMerchantSettings = exports.normalizeDataSource = exports.normalizeMerchantId = exports.parseGoogleCredentials = exports.normalizeGoogleMerchantError = exports.DEFAULT_GOOGLE_WRITE_CONCURRENCY = exports.GOOGLE_FEED_LABEL = exports.GOOGLE_CONTENT_LANGUAGE = void 0;
21
+ exports.selectRemoteOrphanProducts = exports.diagnoseGoogleMerchantConnection = exports.collectGoogleProductsForDataSource = exports.summarizeMerchantOperations = exports.executeGoogleWriteOperations = exports.runBoundedMerchantOperations = exports.createGoogleDeleteOperation = exports.createGoogleUpdateOperation = exports.createGoogleInsertOperation = exports.buildGoogleUpdateMask = exports.validateGoogleProductUrls = exports.toGoogleProductInput = exports.buildGoogleProductInputName = exports.buildGoogleProductName = exports.buildGoogleProductIdentifier = exports.createGoogleMerchantClients = exports.parseGoogleMerchantSettings = exports.discoverGoogleMerchantDataSources = exports.createGoogleDataSourcesDiscoveryClient = exports.parseGoogleDataSourceDiscoverySettings = exports.normalizeDataSource = exports.normalizeMerchantId = exports.parseGoogleCredentials = exports.normalizeGoogleMerchantError = exports.MAX_GOOGLE_DATA_SOURCE_DISCOVERY_RESULTS = exports.DEFAULT_GOOGLE_WRITE_CONCURRENCY = exports.GOOGLE_FEED_LABEL = exports.GOOGLE_CONTENT_LANGUAGE = void 0;
22
22
  const products_1 = require("@google-shopping/products");
23
23
  const datasources_1 = require("@google-shopping/datasources");
24
24
  const GOOGLE_OAUTH_SCOPE = "https://www.googleapis.com/auth/content";
@@ -30,6 +30,7 @@ const FEED_LABEL_PATTERN = /^[A-Z0-9_-]{1,20}$/;
30
30
  exports.GOOGLE_CONTENT_LANGUAGE = "pl";
31
31
  exports.GOOGLE_FEED_LABEL = "PL";
32
32
  exports.DEFAULT_GOOGLE_WRITE_CONCURRENCY = 4;
33
+ exports.MAX_GOOGLE_DATA_SOURCE_DISCOVERY_RESULTS = 100;
33
34
  class MerchantOperationAttemptError {
34
35
  constructor(cause, attempts) {
35
36
  this.cause = cause;
@@ -154,6 +155,105 @@ function normalizeDataSource(rawDataSource, merchantId) {
154
155
  return `accounts/${match[1]}/dataSources/${match[2]}`;
155
156
  }
156
157
  exports.normalizeDataSource = normalizeDataSource;
158
+ function parseGoogleDataSourceDiscoverySettings(requestedMerchantId, entries) {
159
+ var _a;
160
+ const getValue = (key) => { var _a; return (_a = entries.find((entry) => entry.key === key)) === null || _a === void 0 ? void 0 : _a.value; };
161
+ const accountId = normalizeMerchantId(requestedMerchantId);
162
+ const rawCredentials = (_a = getValue("credentials")) === null || _a === void 0 ? void 0 : _a.trim();
163
+ if (!rawCredentials) {
164
+ throw new Error("Saved Google credentials are required");
165
+ }
166
+ try {
167
+ return {
168
+ accountId,
169
+ credentials: parseGoogleCredentials(rawCredentials),
170
+ };
171
+ }
172
+ catch (_b) {
173
+ throw new Error("Saved Google credentials are invalid");
174
+ }
175
+ }
176
+ exports.parseGoogleDataSourceDiscoverySettings = parseGoogleDataSourceDiscoverySettings;
177
+ function createGoogleDataSourcesDiscoveryClient(credentials) {
178
+ const client = new datasources_1.v1.DataSourcesServiceClient({
179
+ credentials,
180
+ scopes: [GOOGLE_OAUTH_SCOPE],
181
+ });
182
+ // Google exposes data-source reads under the broad content scope. Actual write
183
+ // prevention is IAM/operator-owned; this wrapper narrows the runtime API surface.
184
+ return {
185
+ close: () => client.close(),
186
+ listDataSourcesAsync: (request) => client.listDataSourcesAsync(request),
187
+ };
188
+ }
189
+ exports.createGoogleDataSourcesDiscoveryClient = createGoogleDataSourcesDiscoveryClient;
190
+ function normalizeGoogleDataSourceDiscoveryError(error) {
191
+ var _a;
192
+ const normalized = normalizeGoogleMerchantError(error);
193
+ const messages = {
194
+ AUTHENTICATION_FAILED: "Google Merchant authentication failed",
195
+ PERMISSION_DENIED: "Saved Google credentials cannot read this Merchant account",
196
+ UNAVAILABLE: "Google Merchant data-source discovery is temporarily unavailable",
197
+ };
198
+ return new Error((_a = messages[normalized.status]) !== null && _a !== void 0 ? _a : "Google Merchant data-source discovery failed");
199
+ }
200
+ async function discoverGoogleMerchantDataSources(requestedMerchantId, entries, createClient = createGoogleDataSourcesDiscoveryClient) {
201
+ var _a, e_1, _b, _c;
202
+ const settings = parseGoogleDataSourceDiscoverySettings(requestedMerchantId, entries);
203
+ let client;
204
+ try {
205
+ client = createClient(settings.credentials);
206
+ }
207
+ catch (_d) {
208
+ throw new Error("Google Merchant data-source client could not be created");
209
+ }
210
+ const names = new Set();
211
+ let discoveryError;
212
+ try {
213
+ try {
214
+ for (var _e = true, _f = __asyncValues(client.listDataSourcesAsync({
215
+ parent: `accounts/${settings.accountId}`,
216
+ })), _g; _g = await _f.next(), _a = _g.done, !_a; _e = true) {
217
+ _c = _g.value;
218
+ _e = false;
219
+ const dataSource = _c;
220
+ if (!dataSource.name)
221
+ continue;
222
+ try {
223
+ names.add(normalizeDataSource(dataSource.name, settings.accountId));
224
+ if (names.size === exports.MAX_GOOGLE_DATA_SOURCE_DISCOVERY_RESULTS)
225
+ break;
226
+ }
227
+ catch (_h) {
228
+ // Ignore malformed or cross-account resources returned by the remote API.
229
+ }
230
+ }
231
+ }
232
+ catch (e_1_1) { e_1 = { error: e_1_1 }; }
233
+ finally {
234
+ try {
235
+ if (!_e && !_a && (_b = _f.return)) await _b.call(_f);
236
+ }
237
+ finally { if (e_1) throw e_1.error; }
238
+ }
239
+ return [...names].sort();
240
+ }
241
+ catch (error) {
242
+ discoveryError = normalizeGoogleDataSourceDiscoveryError(error);
243
+ throw discoveryError;
244
+ }
245
+ finally {
246
+ try {
247
+ await client.close();
248
+ }
249
+ catch (_j) {
250
+ if (!discoveryError) {
251
+ throw new Error("Google Merchant data-source client could not be closed");
252
+ }
253
+ }
254
+ }
255
+ }
256
+ exports.discoverGoogleMerchantDataSources = discoverGoogleMerchantDataSources;
157
257
  function parseGoogleMerchantSettings(entries) {
158
258
  var _a, _b, _c, _d, _e, _f, _g;
159
259
  const getValue = (key) => { var _a; return (_a = entries.find((entry) => entry.key === key)) === null || _a === void 0 ? void 0 : _a.value; };
@@ -407,7 +507,7 @@ function summarizeMerchantOperations(results) {
407
507
  }
408
508
  exports.summarizeMerchantOperations = summarizeMerchantOperations;
409
509
  async function collectGoogleProductsForDataSource(client, accountId, dataSource) {
410
- var _a, e_1, _b, _c;
510
+ var _a, e_2, _b, _c;
411
511
  var _d, _e;
412
512
  const products = new Map();
413
513
  try {
@@ -426,18 +526,18 @@ async function collectGoogleProductsForDataSource(client, accountId, dataSource)
426
526
  });
427
527
  }
428
528
  }
429
- catch (e_1_1) { e_1 = { error: e_1_1 }; }
529
+ catch (e_2_1) { e_2 = { error: e_2_1 }; }
430
530
  finally {
431
531
  try {
432
532
  if (!_f && !_a && (_b = _g.return)) await _b.call(_g);
433
533
  }
434
- finally { if (e_1) throw e_1.error; }
534
+ finally { if (e_2) throw e_2.error; }
435
535
  }
436
536
  return [...products.values()];
437
537
  }
438
538
  exports.collectGoogleProductsForDataSource = collectGoogleProductsForDataSource;
439
539
  async function diagnoseGoogleMerchantConnection(client, settings, dataSourcesClient) {
440
- var _a, e_2, _b, _c;
540
+ var _a, e_3, _b, _c;
441
541
  var _d, _e, _f, _g, _h, _j, _k;
442
542
  const startedAt = Date.now();
443
543
  const issues = [];
@@ -478,12 +578,12 @@ async function diagnoseGoogleMerchantConnection(client, settings, dataSourcesCli
478
578
  disapprovedProductsCount += 1;
479
579
  }
480
580
  }
481
- catch (e_2_1) { e_2 = { error: e_2_1 }; }
581
+ catch (e_3_1) { e_3 = { error: e_3_1 }; }
482
582
  finally {
483
583
  try {
484
584
  if (!_l && !_a && (_b = _m.return)) await _b.call(_m);
485
585
  }
486
- finally { if (e_2) throw e_2.error; }
586
+ finally { if (e_3) throw e_3.error; }
487
587
  }
488
588
  }
489
589
  catch (error) {
@@ -19,6 +19,7 @@ export declare class GooglePlatformIntegrationService {
19
19
  removeOrphanItems(ctx: RequestContext, items: RemoteProduct[]): Promise<void>;
20
20
  getAllProducts(ctx: RequestContext): Promise<RemoteProduct[]>;
21
21
  getConnectionDiagnostic(ctx: RequestContext): Promise<import("./google-merchant-api.js").GoogleMerchantConnectionDiagnostic>;
22
+ discoverGoogleMerchantDataSources(ctx: RequestContext, requestedMerchantId: string): Promise<string[]>;
22
23
  getGoogleProduct(ctx: RequestContext, { communicateID }: {
23
24
  communicateID: string;
24
25
  }): Promise<GoogleProcessedProduct | null>;
@@ -100,6 +100,15 @@ let GooglePlatformIntegrationService = class GooglePlatformIntegrationService {
100
100
  await this.closeClients(clients);
101
101
  }
102
102
  }
103
+ async discoverGoogleMerchantDataSources(ctx, requestedMerchantId) {
104
+ const rawSettings = await this.connection
105
+ .getRepository(ctx, platform_integration_settings_entity_js_1.MerchantPlatformSettingsEntity)
106
+ .findOne({ relations: ["entries"], where: { platform: "google" } });
107
+ if (!rawSettings) {
108
+ throw new Error("Google Merchant settings not found");
109
+ }
110
+ return (0, google_merchant_api_js_1.discoverGoogleMerchantDataSources)(requestedMerchantId, rawSettings.entries);
111
+ }
103
112
  async getGoogleProduct(ctx, { communicateID }) {
104
113
  const authorization = await this.getAuthorization(ctx);
105
114
  try {
@@ -10,6 +10,11 @@ export declare const getMerchantPlatformSettings: TypedDocumentNode<{
10
10
  }, {} & {
11
11
  platform: string;
12
12
  }>;
13
+ export declare const getGoogleMerchantDataSources: TypedDocumentNode<{
14
+ getGoogleMerchantDataSources: string[];
15
+ }, {
16
+ merchantId: string;
17
+ }>;
13
18
  export declare const getMerchantPlatformInfo: TypedDocumentNode<{
14
19
  getMerchantPlatformInfo?: {
15
20
  isValidConnection: boolean;
@@ -10,6 +10,11 @@ export const getMerchantPlatformSettings = query({
10
10
  merchantPlatformSettingsSelector,
11
11
  ],
12
12
  });
13
+ export const getGoogleMerchantDataSources = gql `
14
+ query GetGoogleMerchantDataSources($merchantId: String!) {
15
+ getGoogleMerchantDataSources(merchantId: $merchantId)
16
+ }
17
+ `;
13
18
  export const getMerchantPlatformInfo = query({
14
19
  getMerchantPlatformInfo: [
15
20
  { platform: $("platform", "String!") },
@@ -27,6 +27,13 @@ declare const _default: {
27
27
  title: string;
28
28
  merchantId: string;
29
29
  dataSource: string;
30
+ dataSourceDiscovery: {
31
+ discover: string;
32
+ loading: string;
33
+ empty: string;
34
+ error: string;
35
+ select: string;
36
+ };
30
37
  contentLanguage: string;
31
38
  feedLabel: string;
32
39
  credentials: string;
@@ -27,6 +27,13 @@
27
27
  "title": "Google Merchant",
28
28
  "merchantId": "Merchant ID",
29
29
  "dataSource": "Data source",
30
+ "dataSourceDiscovery": {
31
+ "discover": "Discover data sources",
32
+ "loading": "Discovering data sources…",
33
+ "empty": "No data sources were found for the entered Merchant account.",
34
+ "error": "Data sources could not be discovered. Check the Merchant ID and saved credentials.",
35
+ "select": "Select a discovered data source"
36
+ },
30
37
  "contentLanguage": "Content language",
31
38
  "feedLabel": "Feed label",
32
39
  "credentials": "Google account credentials",
@@ -27,6 +27,13 @@ declare const _default: {
27
27
  title: string;
28
28
  merchantId: string;
29
29
  dataSource: string;
30
+ dataSourceDiscovery: {
31
+ discover: string;
32
+ loading: string;
33
+ empty: string;
34
+ error: string;
35
+ select: string;
36
+ };
30
37
  contentLanguage: string;
31
38
  feedLabel: string;
32
39
  credentials: string;
@@ -27,6 +27,13 @@
27
27
  "title": "Google Merchant",
28
28
  "merchantId": "Identyfikator sprzedawcy",
29
29
  "dataSource": "Źródło danych",
30
+ "dataSourceDiscovery": {
31
+ "discover": "Wykryj źródła danych",
32
+ "loading": "Wykrywanie źródeł danych…",
33
+ "empty": "Nie znaleziono źródeł danych dla podanego konta Merchant.",
34
+ "error": "Nie udało się wykryć źródeł danych. Sprawdź identyfikator Merchant i zapisane dane logowania.",
35
+ "select": "Wybierz wykryte źródło danych"
36
+ },
30
37
  "contentLanguage": "Język treści",
31
38
  "feedLabel": "Etykieta pliku danych",
32
39
  "credentials": "Dane logowania konta Google",
@@ -1,14 +1,16 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useEffect, useState } from "react";
3
- import { Label, Input, PageBlock, useLazyQuery, useMutation, Checkbox, Button, useTranslation, } from "@deenruv/react-ui-devkit";
4
- import { getGoogleMerchantDiagnostic, getGoogleMerchantSyncHistory, getMerchantPlatformSettings, } from "../graphql/queries";
2
+ import { useEffect, useRef, useState } from "react";
3
+ import { Label, Input, PageBlock, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, useLazyQuery, useMutation, Checkbox, Button, useTranslation, } from "@deenruv/react-ui-devkit";
4
+ import { getGoogleMerchantDiagnostic, getGoogleMerchantDataSources, getGoogleMerchantSyncHistory, getMerchantPlatformSettings, } from "../graphql/queries";
5
5
  import { removeOrphanItems, saveMerchantPlatformSettings, sendAllProductsToMerchantPlatform, } from "../graphql/mutations";
6
6
  import { toast } from "sonner";
7
7
  import { translationNS } from "../translation-ns.js";
8
+ import { createGoogleDataSourceDiscoveryRequestTracker, isValidGoogleMerchantId, selectDiscoveredGoogleDataSource, } from "./google-data-source-discovery.js";
8
9
  export const GooglePage = () => {
9
10
  const { t, i18n } = useTranslation(translationNS);
10
11
  const [fetchMerchantPlatformSettings] = useLazyQuery(getMerchantPlatformSettings);
11
12
  const [fetchMerchantPlatformInfo] = useLazyQuery(getGoogleMerchantDiagnostic);
13
+ const [fetchGoogleMerchantDataSources] = useLazyQuery(getGoogleMerchantDataSources);
12
14
  const [fetchSyncHistory] = useLazyQuery(getGoogleMerchantSyncHistory);
13
15
  const [mutate] = useMutation(saveMerchantPlatformSettings);
14
16
  const [removeOldItems] = useMutation(removeOrphanItems);
@@ -19,6 +21,12 @@ export const GooglePage = () => {
19
21
  productsCount: 0,
20
22
  connectionStatus: false,
21
23
  });
24
+ const [discoveredDataSources, setDiscoveredDataSources] = useState([]);
25
+ const [discoveryAttempted, setDiscoveryAttempted] = useState(false);
26
+ const [discoveryError, setDiscoveryError] = useState(false);
27
+ const [isDiscoveringDataSources, setIsDiscoveringDataSources] = useState(false);
28
+ const discoveryRequestTracker = useRef(createGoogleDataSourceDiscoveryRequestTracker()).current;
29
+ const currentMerchantId = useRef("");
22
30
  const handleSelectedFile = (e) => {
23
31
  const file = e.target.files?.[0];
24
32
  if (file) {
@@ -112,6 +120,14 @@ export const GooglePage = () => {
112
120
  useEffect(() => {
113
121
  refetch();
114
122
  }, []);
123
+ useEffect(() => {
124
+ currentMerchantId.current = settingsForm.merchantId;
125
+ discoveryRequestTracker.invalidate();
126
+ setDiscoveredDataSources([]);
127
+ setDiscoveryAttempted(false);
128
+ setDiscoveryError(false);
129
+ setIsDiscoveringDataSources(false);
130
+ }, [settingsForm.merchantId]);
115
131
  const onSubmit = async (e) => {
116
132
  e.preventDefault();
117
133
  try {
@@ -139,6 +155,34 @@ export const GooglePage = () => {
139
155
  }));
140
156
  }
141
157
  };
158
+ const discoverDataSources = async () => {
159
+ const merchantId = settingsForm.merchantId.trim();
160
+ if (!isValidGoogleMerchantId(merchantId))
161
+ return;
162
+ const request = discoveryRequestTracker.begin(merchantId);
163
+ setDiscoveryAttempted(true);
164
+ setDiscoveryError(false);
165
+ setIsDiscoveringDataSources(true);
166
+ try {
167
+ const data = await fetchGoogleMerchantDataSources({ merchantId });
168
+ if (!discoveryRequestTracker.isCurrent(request, currentMerchantId.current)) {
169
+ return;
170
+ }
171
+ setDiscoveredDataSources(data.getGoogleMerchantDataSources);
172
+ }
173
+ catch {
174
+ if (!discoveryRequestTracker.isCurrent(request, currentMerchantId.current)) {
175
+ return;
176
+ }
177
+ setDiscoveredDataSources([]);
178
+ setDiscoveryError(true);
179
+ }
180
+ finally {
181
+ if (discoveryRequestTracker.isCurrent(request, currentMerchantId.current)) {
182
+ setIsDiscoveringDataSources(false);
183
+ }
184
+ }
185
+ };
142
186
  return (_jsx(PageBlock, { children: _jsxs("div", { style: { position: "relative" }, className: "flex flex-col p-4 relative max-w-[800px] mx-auto", children: [isLoading && (_jsx("div", { role: "status", "aria-label": t("common.loading"), style: {
143
187
  position: "absolute",
144
188
  top: 0,
@@ -157,13 +201,35 @@ export const GooglePage = () => {
157
201
  alignItems: "center",
158
202
  width: "42px",
159
203
  height: "42px",
160
- }, className: "spinner" }) })), _jsx("h2", { className: "mb-4 text-xl font-semibold", children: t("google.title") }), loadError ? (_jsx("div", { className: "mb-4 rounded border border-red-400 p-3 text-red-700", role: "alert", children: t("common.settingsLoadFailed") })) : null, _jsxs("form", { className: "flex flex-col gap-4", onSubmit: onSubmit, children: [_jsxs("div", { className: "flex justify-between gap-4", children: [_jsxs("div", { className: "w-full flex flex-col gap-2", children: [_jsx(Label, { children: t("common.brand") }), _jsx(Input, { className: "w-full", value: settingsForm.brand, onChange: (e) => setSettingsForm({ ...settingsForm, brand: e.target.value }) })] }), _jsxs("div", { className: "w-full flex flex-col gap-2", children: [_jsx(Label, { children: t("google.merchantId") }), _jsx(Input, { className: "w-full", required: true, value: settingsForm.merchantId, onChange: (e) => setSettingsForm({
161
- ...settingsForm,
162
- merchantId: e.target.value,
163
- }) })] })] }), _jsxs("div", { className: "w-full flex flex-col gap-2", children: [_jsx(Label, { children: t("google.dataSource") }), _jsx(Input, { className: "w-full", placeholder: "accounts/{merchantId}/dataSources/{id}", required: true, value: settingsForm.dataSource, onChange: (e) => setSettingsForm({
204
+ }, className: "spinner" }) })), _jsx("h2", { className: "mb-4 text-xl font-semibold", children: t("google.title") }), loadError ? (_jsx("div", { className: "mb-4 rounded border border-red-400 p-3 text-red-700", role: "alert", children: t("common.settingsLoadFailed") })) : null, _jsxs("form", { className: "flex flex-col gap-4", onSubmit: onSubmit, children: [_jsxs("div", { className: "flex justify-between gap-4", children: [_jsxs("div", { className: "w-full flex flex-col gap-2", children: [_jsx(Label, { children: t("common.brand") }), _jsx(Input, { className: "w-full", value: settingsForm.brand, onChange: (e) => setSettingsForm({ ...settingsForm, brand: e.target.value }) })] }), _jsxs("div", { className: "w-full flex flex-col gap-2", children: [_jsx(Label, { children: t("google.merchantId") }), _jsx(Input, { className: "w-full", required: true, value: settingsForm.merchantId, onChange: (e) => {
205
+ currentMerchantId.current = e.target.value;
206
+ discoveryRequestTracker.invalidate();
207
+ setDiscoveredDataSources([]);
208
+ setDiscoveryAttempted(false);
209
+ setDiscoveryError(false);
210
+ setIsDiscoveringDataSources(false);
211
+ setSettingsForm({
212
+ ...settingsForm,
213
+ merchantId: e.target.value,
214
+ });
215
+ } })] })] }), _jsxs("div", { className: "w-full flex flex-col gap-2", children: [_jsx(Label, { children: t("google.dataSource") }), _jsx(Input, { className: "w-full", placeholder: "accounts/{merchantId}/dataSources/{id}", required: true, value: settingsForm.dataSource, onChange: (e) => setSettingsForm({
164
216
  ...settingsForm,
165
217
  dataSource: e.target.value,
166
- }) })] }), _jsxs("div", { className: "flex justify-between gap-4", children: [_jsxs("div", { className: "w-full flex flex-col gap-2", children: [_jsx(Label, { children: t("google.contentLanguage") }), _jsx(Input, { required: true, value: settingsForm.contentLanguage, onChange: (e) => setSettingsForm({
218
+ }) }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Button, { disabled: isDiscoveringDataSources ||
219
+ !isValidGoogleMerchantId(settingsForm.merchantId), onClick: discoverDataSources, type: "button", children: isDiscoveringDataSources
220
+ ? t("google.dataSourceDiscovery.loading")
221
+ : t("google.dataSourceDiscovery.discover") }), discoveredDataSources.length > 0 ? (_jsxs(Select, { onValueChange: (selectedValue) => setSettingsForm((current) => ({
222
+ ...current,
223
+ dataSource: selectDiscoveredGoogleDataSource(current.dataSource, selectedValue, discoveredDataSources),
224
+ })), value: discoveredDataSources.includes(settingsForm.dataSource)
225
+ ? settingsForm.dataSource
226
+ : undefined, children: [_jsx(SelectTrigger, { className: "flex-1", children: _jsx(SelectValue, { placeholder: t("google.dataSourceDiscovery.select") }) }), _jsx(SelectContent, { children: discoveredDataSources.map((dataSource) => (_jsx(SelectItem, { value: dataSource, children: dataSource }, dataSource))) })] })) : null] }), _jsx("div", { "aria-live": "polite", className: "text-sm", children: discoveryError
227
+ ? t("google.dataSourceDiscovery.error")
228
+ : discoveryAttempted &&
229
+ !isDiscoveringDataSources &&
230
+ discoveredDataSources.length === 0
231
+ ? t("google.dataSourceDiscovery.empty")
232
+ : null })] }), _jsxs("div", { className: "flex justify-between gap-4", children: [_jsxs("div", { className: "w-full flex flex-col gap-2", children: [_jsx(Label, { children: t("google.contentLanguage") }), _jsx(Input, { required: true, value: settingsForm.contentLanguage, onChange: (e) => setSettingsForm({
167
233
  ...settingsForm,
168
234
  contentLanguage: e.target.value,
169
235
  }) })] }), _jsxs("div", { className: "w-full flex flex-col gap-2", children: [_jsx(Label, { children: t("google.feedLabel") }), _jsx(Input, { required: true, value: settingsForm.feedLabel, onChange: (e) => setSettingsForm({
@@ -0,0 +1,11 @@
1
+ export type GoogleDataSourceDiscoveryRequest = {
2
+ generation: number;
3
+ merchantId: string;
4
+ };
5
+ export declare function isValidGoogleMerchantId(value: string): boolean;
6
+ export declare function createGoogleDataSourceDiscoveryRequestTracker(): {
7
+ begin: (merchantId: string) => GoogleDataSourceDiscoveryRequest;
8
+ invalidate: () => void;
9
+ isCurrent: (request: GoogleDataSourceDiscoveryRequest, currentMerchantId: string) => boolean;
10
+ };
11
+ export declare function selectDiscoveredGoogleDataSource(currentValue: string, selectedValue: string, discoveredValues: readonly string[]): string;
@@ -0,0 +1,21 @@
1
+ const MERCHANT_ID_PATTERN = /^[1-9]\d*$/;
2
+ export function isValidGoogleMerchantId(value) {
3
+ return MERCHANT_ID_PATTERN.test(value.trim());
4
+ }
5
+ export function createGoogleDataSourceDiscoveryRequestTracker() {
6
+ let generation = 0;
7
+ return {
8
+ begin: (merchantId) => ({
9
+ generation: ++generation,
10
+ merchantId: merchantId.trim(),
11
+ }),
12
+ invalidate: () => {
13
+ generation += 1;
14
+ },
15
+ isCurrent: (request, currentMerchantId) => request.generation === generation &&
16
+ request.merchantId === currentMerchantId.trim(),
17
+ };
18
+ }
19
+ export function selectDiscoveredGoogleDataSource(currentValue, selectedValue, discoveredValues) {
20
+ return discoveredValues.includes(selectedValue) ? selectedValue : currentValue;
21
+ }
@@ -0,0 +1,34 @@
1
+ import { print } from "graphql";
2
+ import { describe, expect, it } from "vitest";
3
+ import { getGoogleMerchantDataSources } from "../graphql/queries.js";
4
+ import { createGoogleDataSourceDiscoveryRequestTracker, isValidGoogleMerchantId, selectDiscoveredGoogleDataSource, } from "./google-data-source-discovery.js";
5
+ describe("Google data-source discovery selection", () => {
6
+ it("selects only a resource returned by discovery", () => {
7
+ const discovered = [
8
+ "accounts/123/dataSources/456",
9
+ "accounts/123/dataSources/789",
10
+ ];
11
+ expect(selectDiscoveredGoogleDataSource("accounts/123/dataSources/manual", "accounts/123/dataSources/789", discovered)).toBe("accounts/123/dataSources/789");
12
+ expect(selectDiscoveredGoogleDataSource("accounts/123/dataSources/manual", "accounts/999/dataSources/789", discovered)).toBe("accounts/123/dataSources/manual");
13
+ });
14
+ it("builds the raw typed discovery query without Zeus regeneration", () => {
15
+ expect(print(getGoogleMerchantDataSources)).toContain("getGoogleMerchantDataSources(merchantId: $merchantId)");
16
+ });
17
+ it("rejects blank or non-numeric account IDs", () => {
18
+ expect(isValidGoogleMerchantId("")).toBe(false);
19
+ expect(isValidGoogleMerchantId("123x")).toBe(false);
20
+ expect(isValidGoogleMerchantId(" 123 ")).toBe(true);
21
+ });
22
+ it("invalidates stale and overlapping discovery requests", () => {
23
+ const tracker = createGoogleDataSourceDiscoveryRequestTracker();
24
+ const first = tracker.begin("123");
25
+ const second = tracker.begin("123");
26
+ expect(tracker.isCurrent(first, "123")).toBe(false);
27
+ expect(tracker.isCurrent(second, "123")).toBe(true);
28
+ tracker.invalidate();
29
+ expect(tracker.isCurrent(second, "123")).toBe(false);
30
+ const third = tracker.begin("456");
31
+ expect(tracker.isCurrent(third, "123")).toBe(false);
32
+ expect(tracker.isCurrent(third, "456")).toBe(true);
33
+ });
34
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deenruv/merchant-plugin",
3
- "version": "1.0.17-dev.33",
3
+ "version": "1.0.17-dev.35",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -32,11 +32,11 @@
32
32
  "react-i18next": "^14.0.5",
33
33
  "recharts": "^2.12.7",
34
34
  "sonner": "^1.4.41",
35
- "@deenruv/admin-types": "^1.0.17-dev.33",
36
- "@deenruv/admin-ui": "^1.0.17-dev.33",
37
- "@deenruv/react-ui-devkit": "^1.0.17-dev.33",
38
- "@deenruv/common": "^1.0.17-dev.33",
39
- "@deenruv/ui-devkit": "^1.0.17-dev.33"
35
+ "@deenruv/common": "^1.0.17-dev.35",
36
+ "@deenruv/admin-types": "^1.0.17-dev.35",
37
+ "@deenruv/admin-ui": "^1.0.17-dev.35",
38
+ "@deenruv/react-ui-devkit": "^1.0.17-dev.35",
39
+ "@deenruv/ui-devkit": "^1.0.17-dev.35"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@graphql-typed-document-node/core": "3.2.0",
@@ -48,7 +48,7 @@
48
48
  "rimraf": "^5.0.10",
49
49
  "tslib": "^2.6.2",
50
50
  "typescript": "5.3.3",
51
- "@deenruv/core": "^1.0.17-dev.33"
51
+ "@deenruv/core": "^1.0.17-dev.35"
52
52
  },
53
53
  "peerDependencies": {
54
54
  "@deenruv/core": "^1.0.0 || ^1.0.17-dev.0"