google-ads-api 19.0.1-rest-beta → 19.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -11,6 +11,10 @@ While these changes are inconvenient, the performance of the REST api is signifi
11
11
 
12
12
  To prepare for this change, we recommend you use the install the `19.0.0-rest-beta` version of this library and test your application with it.
13
13
 
14
+ ### 19.0.2
15
+
16
+ - Fix issue with reportCount() not giving the correct total results count.
17
+
14
18
  ## 19.0.1
15
19
 
16
20
  - Fix issue with type interface for search_settings not showing any valid fields.
@@ -7,7 +7,6 @@ export interface ClientOptions {
7
7
  client_secret: string;
8
8
  developer_token: string;
9
9
  disable_parsing?: boolean;
10
- max_reporting_rows?: number;
11
10
  }
12
11
  export declare class Client {
13
12
  private readonly options;
@@ -4,7 +4,6 @@ exports.Client = void 0;
4
4
  const customer_1 = require("./customer");
5
5
  const service_1 = require("./service");
6
6
  class Client {
7
- options;
8
7
  constructor(options) {
9
8
  this.options = options;
10
9
  }
@@ -52,10 +52,8 @@ export declare class Customer extends ServiceFactory {
52
52
  reportStreamRaw(reportOptions: Readonly<ReportOptions>): Promise<CancellableStream | void>;
53
53
  private search;
54
54
  private paginatedSearch;
55
- private useStreamToImitateRegularSearch;
56
55
  private querier;
57
56
  private streamer;
58
- private handleStreamError;
59
57
  /**
60
58
  * @description Creates, updates, or removes resources. This method supports atomic transactions
61
59
  * with multiple types of resources. For example, you can atomically create a campaign and a
@@ -64,8 +62,4 @@ export declare class Customer extends ServiceFactory {
64
62
  */
65
63
  mutateResources<T>(mutations: MutateOperation<T>[], mutateOptions?: MutateOptions): Promise<services.MutateGoogleAdsResponse>;
66
64
  private get googleAdsFields();
67
- private prepareGoogleAdsServicePostRequestArgs;
68
- private decamelizeKeysIfNeeded;
69
- private gaqlQueryStringIncludesLimit;
70
- private generateTooManyRowsError;
71
65
  }
@@ -4,19 +4,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.Customer = void 0;
7
- const axios_1 = __importDefault(require("axios"));
8
- const stream_chain_1 = require("stream-chain");
9
- const stream_json_1 = require("stream-json");
10
- const StreamArray_1 = require("stream-json/streamers/StreamArray");
11
- const parserRest_1 = require("./parserRest");
12
- const protos_1 = require("./protos");
7
+ const parser_1 = require("./parser");
13
8
  const serviceFactory_1 = __importDefault(require("./protos/autogen/serviceFactory"));
14
9
  const query_1 = require("./query");
15
- const version_1 = require("./version");
16
- const ROWS_PER_STREAMED_CHUNK = 10_000; // From experience, this is what can be expected from the API.
10
+ const utils_1 = require("./utils");
17
11
  class Customer extends serviceFactory_1.default {
18
12
  constructor(clientOptions, customerOptions, hooks) {
19
- super(clientOptions, customerOptions, hooks ?? {});
13
+ super(clientOptions, customerOptions, hooks !== null && hooks !== void 0 ? hooks : {});
20
14
  }
21
15
  /**
22
16
  @description Single query using a raw GAQL string.
@@ -118,123 +112,39 @@ class Customer extends serviceFactory_1.default {
118
112
  });
119
113
  }
120
114
  async search(gaqlQuery, requestOptions) {
121
- const accessToken = await this.getAccessToken();
122
- try {
123
- const rawResponse = await (0, axios_1.default)(this.prepareGoogleAdsServicePostRequestArgs("search", accessToken, {
124
- data: {
125
- query: gaqlQuery,
126
- ...requestOptions,
127
- },
128
- }));
129
- const searchResponse = rawResponse.data;
130
- const results = searchResponse.results ?? [];
131
- const response = results.map((row) => this.decamelizeKeysIfNeeded(row));
132
- const summaryRow = this.decamelizeKeysIfNeeded(searchResponse.summaryRow);
133
- const nextPageToken = searchResponse.nextPageToken;
134
- const totalResultsCount = searchResponse.totalResultsCount
135
- ? +searchResponse.totalResultsCount
136
- : undefined;
137
- return { response, nextPageToken, totalResultsCount, summaryRow };
138
- }
139
- catch (e) {
140
- if (e.response?.data.error.details[0]) {
141
- throw new protos_1.errors.GoogleAdsFailure(this.decamelizeKeysIfNeeded(e.response.data.error.details[0]));
142
- }
143
- throw e;
115
+ const { service, request } = this.buildSearchRequestAndService(gaqlQuery, requestOptions);
116
+ const searchResponse = await service.search(request, {
117
+ otherArgs: { headers: this.callHeaders },
118
+ autoPaginate: false, // autoPaginate doesn't work
119
+ });
120
+ const response = searchResponse[0];
121
+ const summaryRow = searchResponse[2].summary_row;
122
+ const nextPageToken = searchResponse[2].next_page_token;
123
+ const totalResultsCount = searchResponse[2].total_results_count
124
+ ? +searchResponse[2].total_results_count
125
+ : undefined;
126
+ if (summaryRow) {
127
+ response.unshift(summaryRow);
144
128
  }
129
+ return { response, nextPageToken, totalResultsCount };
145
130
  }
146
- async paginatedSearch(gaqlQuery, requestOptions) {
147
- /*
148
- When possible, use the searchStream method to avoid the overhead of pagination.
149
- */
150
- if (requestOptions.page_size === undefined &&
151
- !requestOptions.search_settings?.return_summary_row) {
152
- // If no pagination or summary options are set, we can use the non-paginated search method.
153
- const { response } = await this.useStreamToImitateRegularSearch(gaqlQuery, requestOptions);
154
- return { response };
155
- }
131
+ async paginatedSearch(gaqlQuery, requestOptions, parser) {
156
132
  const response = [];
157
133
  let nextPageToken = undefined;
158
134
  const initialSearch = await this.search(gaqlQuery, requestOptions);
159
- let totalResultsCount = initialSearch.totalResultsCount;
160
- // Sometimes (when no results?) the totalResultsCount field is not included in the response.
161
- // In this case, we set it to 0.
162
- if (requestOptions.search_settings?.return_total_results_count &&
163
- initialSearch.totalResultsCount === undefined) {
164
- totalResultsCount = 0;
165
- }
166
- let summaryRow = initialSearch.summaryRow;
167
- response.push(...initialSearch.response);
135
+ const totalResultsCount = initialSearch.totalResultsCount;
136
+ response.push(...parser(initialSearch.response));
168
137
  nextPageToken = initialSearch.nextPageToken;
169
138
  while (nextPageToken) {
170
139
  const nextSearch = await this.search(gaqlQuery, {
171
140
  ...requestOptions,
172
141
  page_token: nextPageToken,
173
142
  });
174
- response.push(...nextSearch.response);
143
+ response.push(...parser(nextSearch.response));
175
144
  nextPageToken = nextSearch.nextPageToken;
176
- if (nextSearch.summaryRow) {
177
- summaryRow = nextSearch.summaryRow;
178
- }
179
- }
180
- if (summaryRow) {
181
- response.unshift(summaryRow);
182
145
  }
183
146
  return { response, totalResultsCount };
184
147
  }
185
- // Google's searchStream method is faster than search, but it does not support all features.
186
- // When report() is called, we use searchStream if possible, otherwise we use paginatedSearch.
187
- // Note that just like `paginatedSearch`, this method accumulates results in memory. Use
188
- // `reportStream` for a more memory-efficient alternative (at the cost of more CPU usage).
189
- async useStreamToImitateRegularSearch(gaqlQuery, requestOptions) {
190
- const accessToken = await this.getAccessToken();
191
- try {
192
- const args = this.prepareGoogleAdsServicePostRequestArgs("searchStream", accessToken, {
193
- responseType: "stream",
194
- data: {
195
- query: gaqlQuery,
196
- ...requestOptions,
197
- },
198
- });
199
- const response = await (0, axios_1.default)(args);
200
- const stream = response.data;
201
- const buffers = [];
202
- let rowCount = -ROWS_PER_STREAMED_CHUNK;
203
- for await (const data of stream) {
204
- if (this.clientOptions.max_reporting_rows &&
205
- !this.gaqlQueryStringIncludesLimit(gaqlQuery)) {
206
- // This is a quick-and-dirty way to count rows, but it's good enough for our purposes.
207
- // We want to avoid using a proper JSON streamer here for performance reasons.
208
- if (data.toString("utf-8").includes(`results":`)) {
209
- rowCount += ROWS_PER_STREAMED_CHUNK;
210
- }
211
- if (rowCount > this.clientOptions.max_reporting_rows) {
212
- throw this.generateTooManyRowsError();
213
- }
214
- }
215
- buffers.push(data);
216
- }
217
- const asString = Buffer.concat(buffers).toString("utf-8");
218
- const accumulator = [];
219
- let foundSummaryRow;
220
- for (const { results, summaryRow } of JSON.parse(asString)) {
221
- if (summaryRow) {
222
- foundSummaryRow = this.decamelizeKeysIfNeeded(summaryRow);
223
- }
224
- accumulator.push(...(results ?? []).map((row) => {
225
- return this.decamelizeKeysIfNeeded(row);
226
- }));
227
- if (foundSummaryRow) {
228
- accumulator.unshift(foundSummaryRow);
229
- }
230
- }
231
- return { response: accumulator };
232
- }
233
- catch (e) {
234
- await this.handleStreamError(e);
235
- throw e; // The line above should always throw.
236
- }
237
- }
238
148
  async querier(gaqlQuery, requestOptions = {}, reportOptions, useHooks = true) {
239
149
  const baseHookArguments = {
240
150
  credentials: this.credentials,
@@ -261,7 +171,14 @@ class Customer extends serviceFactory_1.default {
261
171
  }
262
172
  }
263
173
  try {
264
- const { response, totalResultsCount } = await this.paginatedSearch(gaqlQuery, requestOptions);
174
+ const parsingWapper = (rows) => {
175
+ return this.clientOptions.disable_parsing
176
+ ? rows
177
+ : reportOptions
178
+ ? (0, parser_1.parse)({ results: rows, reportOptions })
179
+ : (0, parser_1.parse)({ results: rows, gaqlString: gaqlQuery });
180
+ };
181
+ const { response, totalResultsCount } = await this.paginatedSearch(gaqlQuery, requestOptions, parsingWapper);
265
182
  if (this.hooks.onQueryEnd && useHooks) {
266
183
  const queryResolution = { resolved: false };
267
184
  await this.hooks.onQueryEnd({
@@ -313,79 +230,58 @@ class Customer extends serviceFactory_1.default {
313
230
  return;
314
231
  }
315
232
  }
233
+ const { service, request } = this.buildSearchStreamRequestAndService(gaqlQuery, requestOptions);
234
+ const stream = service.searchStream(request, {
235
+ otherArgs: { headers: this.callHeaders },
236
+ });
237
+ let streamFinished = false;
238
+ const accumulator = [];
239
+ let nextChunk = (0, utils_1.createNextChunkArrivedPromise)();
240
+ stream.on("data", (chunk) => {
241
+ const results = chunk.summary_row ? [chunk.summary_row] : chunk.results;
242
+ const parsedResponse = this.clientOptions.disable_parsing
243
+ ? results
244
+ : reportOptions
245
+ ? (0, parser_1.parse)({ results, reportOptions })
246
+ : (0, parser_1.parse)({ results, gaqlString: gaqlQuery });
247
+ accumulator.push(...parsedResponse);
248
+ nextChunk.resolve();
249
+ nextChunk = (0, utils_1.createNextChunkArrivedPromise)();
250
+ });
251
+ stream.on("error", (searchError) => {
252
+ nextChunk.reject(searchError);
253
+ });
254
+ stream.on("end", () => {
255
+ streamFinished = true;
256
+ nextChunk.resolve();
257
+ });
316
258
  try {
317
- const accessToken = await this.getAccessToken();
318
- const args = this.prepareGoogleAdsServicePostRequestArgs("searchStream", accessToken, {
319
- responseType: "stream",
320
- data: {
321
- query: gaqlQuery,
322
- ...requestOptions,
323
- },
324
- });
325
- const response = await (0, axios_1.default)(args);
326
- const stream = response.data;
327
- // The options below help to make the stream less CPU intensive.
328
- const parser = new stream_json_1.Parser({
329
- streamValues: false,
330
- streamKeys: false,
331
- packValues: true,
332
- packKeys: true,
333
- });
334
- const pipeline = (0, stream_chain_1.chain)([stream, parser, (0, StreamArray_1.streamArray)()]);
335
- let count = 0;
336
- for await (const data of pipeline) {
337
- const results = data.value.results ?? [data.value.summaryRow];
338
- count += results.length;
339
- if (this.clientOptions.max_reporting_rows &&
340
- count > this.clientOptions.max_reporting_rows &&
341
- !this.gaqlQueryStringIncludesLimit(gaqlQuery)) {
342
- throw this.generateTooManyRowsError();
259
+ while (!streamFinished || accumulator.length) {
260
+ if (accumulator.length > 0) {
261
+ const item = accumulator.shift();
262
+ if (item === undefined) {
263
+ throw new Error("UNDEFINED_STREAM_ERROR");
264
+ }
265
+ yield item;
343
266
  }
344
- for (const row of results) {
345
- const parsed = this.decamelizeKeysIfNeeded(row);
346
- yield parsed;
267
+ else {
268
+ await nextChunk.newPromise;
347
269
  }
348
270
  }
349
- return;
350
271
  }
351
- catch (e) {
352
- try {
353
- await this.handleStreamError(e);
354
- }
355
- catch (_e) {
356
- if (this.hooks.onStreamError) {
357
- await this.hooks.onStreamError({
358
- ...baseHookArguments,
359
- error: _e,
360
- });
361
- }
362
- throw _e;
272
+ catch (searchError) {
273
+ const googleAdsError = this.getGoogleAdsError(searchError);
274
+ if (this.hooks.onStreamError) {
275
+ await this.hooks.onStreamError({
276
+ ...baseHookArguments,
277
+ error: googleAdsError,
278
+ });
363
279
  }
280
+ throw googleAdsError;
364
281
  }
365
- }
366
- async handleStreamError(e) {
367
- if (!e?.response?.data) {
368
- throw e;
282
+ finally {
283
+ stream.destroy();
369
284
  }
370
- // The error is a stream, so some effort is required to parse it.
371
- const stream = e.response.data;
372
- const pipeline = (0, stream_chain_1.chain)([stream, (0, stream_json_1.parser)(), (0, StreamArray_1.streamArray)()]);
373
- const defaultErrorMessage = "Unknown GoogleAdsFailure";
374
- let googleAdsFailure = new Error(defaultErrorMessage);
375
- // Only throw the first error.
376
- pipeline.once("data", (data) => {
377
- if (data?.value?.error?.details?.[0]) {
378
- googleAdsFailure = new protos_1.errors.GoogleAdsFailure(this.decamelizeKeysIfNeeded(data.value.error.details[0]));
379
- }
380
- else {
381
- googleAdsFailure = new Error(data?.value?.error?.message ?? defaultErrorMessage, { cause: data?.value?.error ?? data?.value });
382
- }
383
- });
384
- // Must always reject.
385
- await new Promise((_, reject) => {
386
- pipeline.on("end", () => reject(googleAdsFailure));
387
- pipeline.on("error", (err) => reject(err));
388
- });
389
285
  }
390
286
  /**
391
287
  * @description Creates, updates, or removes resources. This method supports atomic transactions
@@ -465,28 +361,5 @@ class Customer extends serviceFactory_1.default {
465
361
  },
466
362
  };
467
363
  }
468
- prepareGoogleAdsServicePostRequestArgs(functionName, accessToken, extra) {
469
- return {
470
- method: "POST",
471
- url: `https://googleads.googleapis.com/${version_1.googleAdsVersion}/customers/${this.customerOptions.customer_id}/googleAds:${functionName}`,
472
- headers: {
473
- Authorization: `Bearer ${accessToken}`,
474
- ...this.callHeaders,
475
- },
476
- ...extra,
477
- };
478
- }
479
- decamelizeKeysIfNeeded(input) {
480
- if (this.clientOptions.disable_parsing) {
481
- return input;
482
- }
483
- return (0, parserRest_1.decamelizeKeys)(input);
484
- }
485
- gaqlQueryStringIncludesLimit(gaqlQuery) {
486
- return gaqlQuery.toLowerCase().includes("limit ");
487
- }
488
- generateTooManyRowsError() {
489
- return new Error(`Exceeded the maximum number of rows set by "max_reporting_rows" (${this.clientOptions.max_reporting_rows}).`);
490
- }
491
364
  }
492
365
  exports.Customer = Customer;