google-ads-api 17.1.0 → 19.0.0-rest-beta
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 +14 -14
- package/build/src/client.d.ts +1 -0
- package/build/src/client.js +1 -0
- package/build/src/customer.d.ts +6 -0
- package/build/src/customer.js +202 -75
- package/build/src/parserRest.d.ts +4 -0
- package/build/src/parserRest.js +88 -0
- package/build/src/protos/autogen/enums.d.ts +431 -1076
- package/build/src/protos/autogen/enums.js +436 -1119
- package/build/src/protos/autogen/fields.d.ts +123 -181
- package/build/src/protos/autogen/fields.js +12 -57
- package/build/src/protos/autogen/resourceNames.d.ts +26 -124
- package/build/src/protos/autogen/resourceNames.js +32 -152
- package/build/src/protos/autogen/serviceFactory.d.ts +197 -404
- package/build/src/protos/autogen/serviceFactory.js +2121 -4200
- package/build/src/protos/index.d.ts +7 -7
- package/build/src/protos/index.js +7 -6
- package/build/src/query.js +3 -3
- package/build/src/service.d.ts +2 -1
- package/build/src/service.js +35 -14
- package/build/src/testUtils.d.ts +38 -0
- package/build/src/testUtils.js +76 -5
- package/build/src/types.d.ts +3 -1
- package/build/src/utils.d.ts +0 -5
- package/build/src/utils.js +1 -16
- package/build/src/version.d.ts +1 -1
- package/build/src/version.js +1 -1
- package/package.json +21 -8
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
</p>
|
|
8
8
|
<p align="center">
|
|
9
9
|
<a href="https://developers.google.com/google-ads/api/docs/release-notes">
|
|
10
|
-
<img src="https://img.shields.io/badge/google%20ads-
|
|
10
|
+
<img src="https://img.shields.io/badge/google%20ads-v17.1.0-009688.svg?style=flat-square">
|
|
11
11
|
</a>
|
|
12
12
|
<a href="https://www.npmjs.com/package/google-ads-api">
|
|
13
13
|
<img src="https://img.shields.io/npm/v/google-ads-api.svg?style=flat-square">
|
|
@@ -350,7 +350,7 @@ const operations: MutateOperation<
|
|
|
350
350
|
advertising_channel_type: enums.AdvertisingChannelType.SEARCH,
|
|
351
351
|
status: enums.CampaignStatus.PAUSED,
|
|
352
352
|
manual_cpc: {
|
|
353
|
-
enhanced_cpc_enabled:
|
|
353
|
+
enhanced_cpc_enabled: false,
|
|
354
354
|
},
|
|
355
355
|
// Use the temporary resource id which will be created in the previous operation
|
|
356
356
|
campaign_budget: budgetResourceName,
|
|
@@ -385,7 +385,7 @@ const keyword = "24 hour locksmith harlem";
|
|
|
385
385
|
|
|
386
386
|
const operations: MutateOperation<
|
|
387
387
|
resources.IAdGroupCriterion & {
|
|
388
|
-
exempt_policy_violation_keys?: google.ads.googleads.
|
|
388
|
+
exempt_policy_violation_keys?: google.ads.googleads.v17.common.IPolicyViolationKey[];
|
|
389
389
|
}
|
|
390
390
|
>[] = [
|
|
391
391
|
{
|
|
@@ -443,7 +443,9 @@ If a summary row is requested in the `report` method, it will be included as the
|
|
|
443
443
|
const [summaryRow, ...response] = await customer.report({
|
|
444
444
|
entity: "campaign",
|
|
445
445
|
metrics: ["metrics.clicks", "metrics.all_conversions"],
|
|
446
|
-
|
|
446
|
+
search_settings: {
|
|
447
|
+
return_summary_row: enums.SummaryRowSetting.SUMMARY_ROW_WITH_RESULTS,
|
|
448
|
+
},
|
|
447
449
|
});
|
|
448
450
|
```
|
|
449
451
|
|
|
@@ -453,7 +455,9 @@ If a summery row is requested in the `reportStream` method, it will be included
|
|
|
453
455
|
const stream = customer.reportStream({
|
|
454
456
|
entity: "campaign",
|
|
455
457
|
metrics: ["metrics.clicks", "metrics.all_conversions"],
|
|
456
|
-
|
|
458
|
+
search_settings: {
|
|
459
|
+
return_summary_row: enums.SummaryRowSetting.SUMMARY_ROW_WITH_RESULTS,
|
|
460
|
+
},
|
|
457
461
|
});
|
|
458
462
|
|
|
459
463
|
const accumulator = [];
|
|
@@ -685,7 +689,7 @@ try {
|
|
|
685
689
|
// Get the first one and explicitly check for a certain error type
|
|
686
690
|
const [firstError] = err.errors;
|
|
687
691
|
if (
|
|
688
|
-
firstError.error_code ===
|
|
692
|
+
firstError.error_code.query_error ===
|
|
689
693
|
errors.QueryErrorEnum.QueryError.UNRECOGNIZED_FIELD
|
|
690
694
|
) {
|
|
691
695
|
console.log(
|
|
@@ -701,15 +705,11 @@ try {
|
|
|
701
705
|
1. Update google-ads-node & publish to NPM. Check that package for instructions.
|
|
702
706
|
2. Update google-ads-node & google-gax in package.json. google-gax must be the same version as google-ads-node,
|
|
703
707
|
Otherwise you are likely to get a `TypeError: Channel credentials must be a ChannelCredentials object` error.
|
|
704
|
-
3. Update the version in
|
|
705
|
-
4. Update the versions in src/protos/index.ts
|
|
706
|
-
5. Run `yarn compile` to update the generated files.
|
|
707
|
-
The `tsc` command might fail -- if so, temporarily add @ts-nocheck
|
|
708
|
-
to the top of problematic files so that the compile script can continue.
|
|
709
|
-
After the compile script has run, those @ts-nocheck lines should no longer be required.
|
|
710
|
-
The compile step depends on some environment variables being set (see scripts/fields.ts), so make sure to set those.
|
|
708
|
+
3. Update the version in `version.ts`.
|
|
709
|
+
4. Update the versions in `src/protos/index.ts`.
|
|
710
|
+
5. Run `yarn compile` to update the generated files. The `tsc` command might fail -- if so, temporarily add @ts-nocheck to the top of problematic files so that the compile script can continue. After the compile script has run, those @ts-nocheck lines should no longer be required. The compile step depends on some environment variables being set (see scripts/fields.ts), so make sure to set those.
|
|
711
711
|
6. Prettify the generated files so the the diffs are easier to read.
|
|
712
712
|
7. Check that the diffs are expected and that the generated files are correct. New Gads versions will often add new services, fields, and enums.
|
|
713
|
-
8. Test with `yarn test
|
|
713
|
+
8. Test with `yarn test`.
|
|
714
714
|
9. Test by linking to a real project and making some real requests.
|
|
715
715
|
10. Once confident, bump the major version & publish to NPM.
|
package/build/src/client.d.ts
CHANGED
package/build/src/client.js
CHANGED
package/build/src/customer.d.ts
CHANGED
|
@@ -52,8 +52,10 @@ 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;
|
|
55
56
|
private querier;
|
|
56
57
|
private streamer;
|
|
58
|
+
private handleStreamError;
|
|
57
59
|
/**
|
|
58
60
|
* @description Creates, updates, or removes resources. This method supports atomic transactions
|
|
59
61
|
* with multiple types of resources. For example, you can atomically create a campaign and a
|
|
@@ -62,4 +64,8 @@ export declare class Customer extends ServiceFactory {
|
|
|
62
64
|
*/
|
|
63
65
|
mutateResources<T>(mutations: MutateOperation<T>[], mutateOptions?: MutateOptions): Promise<services.MutateGoogleAdsResponse>;
|
|
64
66
|
private get googleAdsFields();
|
|
67
|
+
private prepareGoogleAdsServicePostRequestArgs;
|
|
68
|
+
private decamelizeKeysIfNeeded;
|
|
69
|
+
private gaqlQueryStringIncludesLimit;
|
|
70
|
+
private generateTooManyRowsError;
|
|
65
71
|
}
|
package/build/src/customer.js
CHANGED
|
@@ -4,13 +4,19 @@ 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
|
|
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");
|
|
8
13
|
const serviceFactory_1 = __importDefault(require("./protos/autogen/serviceFactory"));
|
|
9
14
|
const query_1 = require("./query");
|
|
10
|
-
const
|
|
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.
|
|
11
17
|
class Customer extends serviceFactory_1.default {
|
|
12
18
|
constructor(clientOptions, customerOptions, hooks) {
|
|
13
|
-
super(clientOptions, customerOptions, hooks
|
|
19
|
+
super(clientOptions, customerOptions, hooks ?? {});
|
|
14
20
|
}
|
|
15
21
|
/**
|
|
16
22
|
@description Single query using a raw GAQL string.
|
|
@@ -50,8 +56,8 @@ class Customer extends serviceFactory_1.default {
|
|
|
50
56
|
*/
|
|
51
57
|
async reportCount(options) {
|
|
52
58
|
const { gaqlQuery, requestOptions } = (0, query_1.buildQuery)({ ...options, limit: 1 }); // must get at least one row
|
|
53
|
-
//
|
|
54
|
-
requestOptions.
|
|
59
|
+
// We do not allow this field in reportOptions, however it is still a valid request option
|
|
60
|
+
requestOptions.search_settings = { return_total_results_count: true };
|
|
55
61
|
const useHooks = false; // to avoid cacheing conflicts
|
|
56
62
|
const { totalResultsCount } = await this.querier(gaqlQuery, requestOptions, options, useHooks);
|
|
57
63
|
return totalResultsCount;
|
|
@@ -111,39 +117,123 @@ class Customer extends serviceFactory_1.default {
|
|
|
111
117
|
});
|
|
112
118
|
}
|
|
113
119
|
async search(gaqlQuery, requestOptions) {
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
120
|
+
const accessToken = await this.getAccessToken();
|
|
121
|
+
try {
|
|
122
|
+
const rawResponse = await (0, axios_1.default)(this.prepareGoogleAdsServicePostRequestArgs("search", accessToken, {
|
|
123
|
+
data: {
|
|
124
|
+
query: gaqlQuery,
|
|
125
|
+
...requestOptions,
|
|
126
|
+
},
|
|
127
|
+
}));
|
|
128
|
+
const searchResponse = rawResponse.data;
|
|
129
|
+
const results = searchResponse.results ?? [];
|
|
130
|
+
const response = results.map((row) => this.decamelizeKeysIfNeeded(row));
|
|
131
|
+
const summaryRow = this.decamelizeKeysIfNeeded(searchResponse.summaryRow);
|
|
132
|
+
const nextPageToken = searchResponse.nextPageToken;
|
|
133
|
+
const totalResultsCount = searchResponse.totalResultsCount
|
|
134
|
+
? +searchResponse.totalResultsCount
|
|
135
|
+
: undefined;
|
|
136
|
+
return { response, nextPageToken, totalResultsCount, summaryRow };
|
|
137
|
+
}
|
|
138
|
+
catch (e) {
|
|
139
|
+
if (e.response?.data.error.details[0]) {
|
|
140
|
+
throw new protos_1.errors.GoogleAdsFailure(this.decamelizeKeysIfNeeded(e.response.data.error.details[0]));
|
|
141
|
+
}
|
|
142
|
+
throw e;
|
|
127
143
|
}
|
|
128
|
-
return { response, nextPageToken, totalResultsCount };
|
|
129
144
|
}
|
|
130
|
-
async paginatedSearch(gaqlQuery, requestOptions
|
|
145
|
+
async paginatedSearch(gaqlQuery, requestOptions) {
|
|
146
|
+
/*
|
|
147
|
+
When possible, use the searchStream method to avoid the overhead of pagination.
|
|
148
|
+
*/
|
|
149
|
+
if (requestOptions.page_size === undefined &&
|
|
150
|
+
requestOptions.search_settings?.return_total_results_count === undefined) {
|
|
151
|
+
// If no pagination or summary options are set, we can use the non-paginated search method.
|
|
152
|
+
const { response } = await this.useStreamToImitateRegularSearch(gaqlQuery, requestOptions);
|
|
153
|
+
return { response };
|
|
154
|
+
}
|
|
131
155
|
const response = [];
|
|
132
156
|
let nextPageToken = undefined;
|
|
133
157
|
const initialSearch = await this.search(gaqlQuery, requestOptions);
|
|
134
|
-
|
|
135
|
-
response.
|
|
158
|
+
let totalResultsCount = initialSearch.totalResultsCount;
|
|
159
|
+
// Sometimes (when no results?) the totalResultsCount field is not included in the response.
|
|
160
|
+
// In this case, we set it to 0.
|
|
161
|
+
if (requestOptions.search_settings?.return_total_results_count &&
|
|
162
|
+
initialSearch.totalResultsCount === undefined) {
|
|
163
|
+
totalResultsCount = 0;
|
|
164
|
+
}
|
|
165
|
+
let summaryRow = initialSearch.summaryRow;
|
|
166
|
+
response.push(...initialSearch.response);
|
|
136
167
|
nextPageToken = initialSearch.nextPageToken;
|
|
137
168
|
while (nextPageToken) {
|
|
138
169
|
const nextSearch = await this.search(gaqlQuery, {
|
|
139
170
|
...requestOptions,
|
|
140
171
|
page_token: nextPageToken,
|
|
141
172
|
});
|
|
142
|
-
response.push(...
|
|
173
|
+
response.push(...nextSearch.response);
|
|
143
174
|
nextPageToken = nextSearch.nextPageToken;
|
|
175
|
+
if (nextSearch.summaryRow) {
|
|
176
|
+
summaryRow = nextSearch.summaryRow;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (summaryRow) {
|
|
180
|
+
response.unshift(summaryRow);
|
|
144
181
|
}
|
|
145
182
|
return { response, totalResultsCount };
|
|
146
183
|
}
|
|
184
|
+
// Google's searchStream method is faster than search, but it does not support all features.
|
|
185
|
+
// When report() is called, we use searchStream if possible, otherwise we use paginatedSearch.
|
|
186
|
+
// Note that just like `paginatedSearch`, this method accumulates results in memory. Use
|
|
187
|
+
// `reportStream` for a more memory-efficient alternative (at the cost of more CPU usage).
|
|
188
|
+
async useStreamToImitateRegularSearch(gaqlQuery, requestOptions) {
|
|
189
|
+
const accessToken = await this.getAccessToken();
|
|
190
|
+
try {
|
|
191
|
+
const args = this.prepareGoogleAdsServicePostRequestArgs("searchStream", accessToken, {
|
|
192
|
+
responseType: "stream",
|
|
193
|
+
data: {
|
|
194
|
+
query: gaqlQuery,
|
|
195
|
+
...requestOptions,
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
const response = await (0, axios_1.default)(args);
|
|
199
|
+
const stream = response.data;
|
|
200
|
+
const buffers = [];
|
|
201
|
+
let rowCount = -ROWS_PER_STREAMED_CHUNK;
|
|
202
|
+
for await (const data of stream) {
|
|
203
|
+
if (this.clientOptions.max_reporting_rows &&
|
|
204
|
+
!this.gaqlQueryStringIncludesLimit(gaqlQuery)) {
|
|
205
|
+
// This is a quick-and-dirty way to count rows, but it's good enough for our purposes.
|
|
206
|
+
// We want to avoid using a proper JSON streamer here for performance reasons.
|
|
207
|
+
if (data.toString("utf-8").includes(`results":`)) {
|
|
208
|
+
rowCount += ROWS_PER_STREAMED_CHUNK;
|
|
209
|
+
}
|
|
210
|
+
if (rowCount > this.clientOptions.max_reporting_rows) {
|
|
211
|
+
throw this.generateTooManyRowsError();
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
buffers.push(data);
|
|
215
|
+
}
|
|
216
|
+
const asString = Buffer.concat(buffers).toString("utf-8");
|
|
217
|
+
const accumulator = [];
|
|
218
|
+
let foundSummaryRow;
|
|
219
|
+
for (const { results, summaryRow } of JSON.parse(asString)) {
|
|
220
|
+
if (summaryRow) {
|
|
221
|
+
foundSummaryRow = this.decamelizeKeysIfNeeded(summaryRow);
|
|
222
|
+
}
|
|
223
|
+
accumulator.push(...(results ?? []).map((row) => {
|
|
224
|
+
return this.decamelizeKeysIfNeeded(row);
|
|
225
|
+
}));
|
|
226
|
+
if (foundSummaryRow) {
|
|
227
|
+
accumulator.unshift(foundSummaryRow);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return { response: accumulator };
|
|
231
|
+
}
|
|
232
|
+
catch (e) {
|
|
233
|
+
await this.handleStreamError(e);
|
|
234
|
+
throw e; // The line above should always throw.
|
|
235
|
+
}
|
|
236
|
+
}
|
|
147
237
|
async querier(gaqlQuery, requestOptions = {}, reportOptions, useHooks = true) {
|
|
148
238
|
const baseHookArguments = {
|
|
149
239
|
credentials: this.credentials,
|
|
@@ -170,14 +260,7 @@ class Customer extends serviceFactory_1.default {
|
|
|
170
260
|
}
|
|
171
261
|
}
|
|
172
262
|
try {
|
|
173
|
-
const
|
|
174
|
-
return this.clientOptions.disable_parsing
|
|
175
|
-
? rows
|
|
176
|
-
: reportOptions
|
|
177
|
-
? (0, parser_1.parse)({ results: rows, reportOptions })
|
|
178
|
-
: (0, parser_1.parse)({ results: rows, gaqlString: gaqlQuery });
|
|
179
|
-
};
|
|
180
|
-
const { response, totalResultsCount } = await this.paginatedSearch(gaqlQuery, requestOptions, parsingWapper);
|
|
263
|
+
const { response, totalResultsCount } = await this.paginatedSearch(gaqlQuery, requestOptions);
|
|
181
264
|
if (this.hooks.onQueryEnd && useHooks) {
|
|
182
265
|
const queryResolution = { resolved: false };
|
|
183
266
|
await this.hooks.onQueryEnd({
|
|
@@ -229,58 +312,79 @@ class Customer extends serviceFactory_1.default {
|
|
|
229
312
|
return;
|
|
230
313
|
}
|
|
231
314
|
}
|
|
232
|
-
const { service, request } = this.buildSearchStreamRequestAndService(gaqlQuery, requestOptions);
|
|
233
|
-
const stream = service.searchStream(request, {
|
|
234
|
-
otherArgs: { headers: this.callHeaders },
|
|
235
|
-
});
|
|
236
|
-
let streamFinished = false;
|
|
237
|
-
const accumulator = [];
|
|
238
|
-
let nextChunk = (0, utils_1.createNextChunkArrivedPromise)();
|
|
239
|
-
stream.on("data", (chunk) => {
|
|
240
|
-
const results = chunk.summary_row ? [chunk.summary_row] : chunk.results;
|
|
241
|
-
const parsedResponse = this.clientOptions.disable_parsing
|
|
242
|
-
? results
|
|
243
|
-
: reportOptions
|
|
244
|
-
? (0, parser_1.parse)({ results, reportOptions })
|
|
245
|
-
: (0, parser_1.parse)({ results, gaqlString: gaqlQuery });
|
|
246
|
-
accumulator.push(...parsedResponse);
|
|
247
|
-
nextChunk.resolve();
|
|
248
|
-
nextChunk = (0, utils_1.createNextChunkArrivedPromise)();
|
|
249
|
-
});
|
|
250
|
-
stream.on("error", (searchError) => {
|
|
251
|
-
nextChunk.reject(searchError);
|
|
252
|
-
});
|
|
253
|
-
stream.on("end", () => {
|
|
254
|
-
streamFinished = true;
|
|
255
|
-
nextChunk.resolve();
|
|
256
|
-
});
|
|
257
315
|
try {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
316
|
+
const accessToken = await this.getAccessToken();
|
|
317
|
+
const args = this.prepareGoogleAdsServicePostRequestArgs("searchStream", accessToken, {
|
|
318
|
+
responseType: "stream",
|
|
319
|
+
data: {
|
|
320
|
+
query: gaqlQuery,
|
|
321
|
+
...requestOptions,
|
|
322
|
+
},
|
|
323
|
+
});
|
|
324
|
+
const response = await (0, axios_1.default)(args);
|
|
325
|
+
const stream = response.data;
|
|
326
|
+
// The options below help to make the stream less CPU intensive.
|
|
327
|
+
const parser = new stream_json_1.Parser({
|
|
328
|
+
streamValues: false,
|
|
329
|
+
streamKeys: false,
|
|
330
|
+
packValues: true,
|
|
331
|
+
packKeys: true,
|
|
332
|
+
});
|
|
333
|
+
const pipeline = (0, stream_chain_1.chain)([stream, parser, (0, StreamArray_1.streamArray)()]);
|
|
334
|
+
let count = 0;
|
|
335
|
+
for await (const data of pipeline) {
|
|
336
|
+
const results = data.value.results ?? [data.value.summaryRow];
|
|
337
|
+
count += results.length;
|
|
338
|
+
if (this.clientOptions.max_reporting_rows &&
|
|
339
|
+
count > this.clientOptions.max_reporting_rows &&
|
|
340
|
+
!this.gaqlQueryStringIncludesLimit(gaqlQuery)) {
|
|
341
|
+
throw this.generateTooManyRowsError();
|
|
265
342
|
}
|
|
266
|
-
|
|
267
|
-
|
|
343
|
+
for (const row of results) {
|
|
344
|
+
const parsed = this.decamelizeKeysIfNeeded(row);
|
|
345
|
+
yield parsed;
|
|
268
346
|
}
|
|
269
347
|
}
|
|
348
|
+
return;
|
|
270
349
|
}
|
|
271
|
-
catch (
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
350
|
+
catch (e) {
|
|
351
|
+
try {
|
|
352
|
+
await this.handleStreamError(e);
|
|
353
|
+
}
|
|
354
|
+
catch (_e) {
|
|
355
|
+
if (this.hooks.onStreamError) {
|
|
356
|
+
await this.hooks.onStreamError({
|
|
357
|
+
...baseHookArguments,
|
|
358
|
+
error: _e,
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
throw _e;
|
|
278
362
|
}
|
|
279
|
-
throw googleAdsError;
|
|
280
363
|
}
|
|
281
|
-
|
|
282
|
-
|
|
364
|
+
}
|
|
365
|
+
async handleStreamError(e) {
|
|
366
|
+
if (!e?.response?.data) {
|
|
367
|
+
throw e;
|
|
283
368
|
}
|
|
369
|
+
// The error is a stream, so some effort is required to parse it.
|
|
370
|
+
const stream = e.response.data;
|
|
371
|
+
const pipeline = (0, stream_chain_1.chain)([stream, (0, stream_json_1.parser)(), (0, StreamArray_1.streamArray)()]);
|
|
372
|
+
const defaultErrorMessage = "Unknown GoogleAdsFailure";
|
|
373
|
+
let googleAdsFailure = new Error(defaultErrorMessage);
|
|
374
|
+
// Only throw the first error.
|
|
375
|
+
pipeline.once("data", (data) => {
|
|
376
|
+
if (data?.value?.error?.details?.[0]) {
|
|
377
|
+
googleAdsFailure = new protos_1.errors.GoogleAdsFailure(this.decamelizeKeysIfNeeded(data.value.error.details[0]));
|
|
378
|
+
}
|
|
379
|
+
else {
|
|
380
|
+
googleAdsFailure = new Error(data?.value?.error?.message ?? defaultErrorMessage, { cause: data?.value?.error ?? data?.value });
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
// Must always reject.
|
|
384
|
+
await new Promise((_, reject) => {
|
|
385
|
+
pipeline.on("end", () => reject(googleAdsFailure));
|
|
386
|
+
pipeline.on("error", (err) => reject(err));
|
|
387
|
+
});
|
|
284
388
|
}
|
|
285
389
|
/**
|
|
286
390
|
* @description Creates, updates, or removes resources. This method supports atomic transactions
|
|
@@ -360,5 +464,28 @@ class Customer extends serviceFactory_1.default {
|
|
|
360
464
|
},
|
|
361
465
|
};
|
|
362
466
|
}
|
|
467
|
+
prepareGoogleAdsServicePostRequestArgs(functionName, accessToken, extra) {
|
|
468
|
+
return {
|
|
469
|
+
method: "POST",
|
|
470
|
+
url: `https://googleads.googleapis.com/${version_1.googleAdsVersion}/customers/${this.customerOptions.customer_id}/googleAds:${functionName}`,
|
|
471
|
+
headers: {
|
|
472
|
+
Authorization: `Bearer ${accessToken}`,
|
|
473
|
+
...this.callHeaders,
|
|
474
|
+
},
|
|
475
|
+
...extra,
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
decamelizeKeysIfNeeded(input) {
|
|
479
|
+
if (this.clientOptions.disable_parsing) {
|
|
480
|
+
return input;
|
|
481
|
+
}
|
|
482
|
+
return (0, parserRest_1.decamelizeKeys)(input);
|
|
483
|
+
}
|
|
484
|
+
gaqlQueryStringIncludesLimit(gaqlQuery) {
|
|
485
|
+
return gaqlQuery.toLowerCase().includes("limit ");
|
|
486
|
+
}
|
|
487
|
+
generateTooManyRowsError() {
|
|
488
|
+
return new Error(`Exceeded the maximum number of rows set by "max_reporting_rows" (${this.clientOptions.max_reporting_rows}).`);
|
|
489
|
+
}
|
|
363
490
|
}
|
|
364
491
|
exports.Customer = Customer;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* JSON Rest parsing
|
|
4
|
+
*/
|
|
5
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
6
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
7
|
+
};
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.decamelizeKeys = void 0;
|
|
10
|
+
const map_obj_1 = __importDefault(require("map-obj"));
|
|
11
|
+
const circ_json_1 = require("circ-json");
|
|
12
|
+
const utils_1 = require("./utils");
|
|
13
|
+
const fields_1 = require("./protos/autogen/fields");
|
|
14
|
+
const enums_1 = require("./protos/autogen/enums");
|
|
15
|
+
const fieldDataTypes = (0, circ_json_1.parse)(fields_1.fieldDataTypes);
|
|
16
|
+
const decamelizeCache = new Map();
|
|
17
|
+
const fieldTypeCache = new Map();
|
|
18
|
+
const isObject = (value) => typeof value === "object" && value !== null;
|
|
19
|
+
const decamelizeKeys = (input) => {
|
|
20
|
+
if (!isObject(input)) {
|
|
21
|
+
return input;
|
|
22
|
+
}
|
|
23
|
+
const makeMapper = (parentPath) => (key, value) => {
|
|
24
|
+
key = cachedDecamelize(key);
|
|
25
|
+
if (isObject(value)) {
|
|
26
|
+
const path = parentPath === undefined ? key : `${parentPath}.${key}`;
|
|
27
|
+
// @ts-ignore
|
|
28
|
+
value = (0, map_obj_1.default)(value, makeMapper(path));
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
value = cachedValueParser(key, parentPath, value);
|
|
32
|
+
}
|
|
33
|
+
return [key, value];
|
|
34
|
+
};
|
|
35
|
+
// @ts-ignore
|
|
36
|
+
return (0, map_obj_1.default)(input, makeMapper());
|
|
37
|
+
};
|
|
38
|
+
exports.decamelizeKeys = decamelizeKeys;
|
|
39
|
+
const cachedDecamelize = (key) => {
|
|
40
|
+
const cachedResult = decamelizeCache.get(key);
|
|
41
|
+
if (cachedResult) {
|
|
42
|
+
return cachedResult;
|
|
43
|
+
}
|
|
44
|
+
const newKey = (0, utils_1.toSnakeCase)(key);
|
|
45
|
+
decamelizeCache.set(key, newKey);
|
|
46
|
+
return newKey;
|
|
47
|
+
};
|
|
48
|
+
const cachedValueParser = (key, parentPath, value) => {
|
|
49
|
+
let newValue = value;
|
|
50
|
+
const fullPath = parentPath ? `${parentPath}.${key}` : key;
|
|
51
|
+
const megaDataType = getTypeFromPath(fullPath);
|
|
52
|
+
if (megaDataType === undefined && !fullPath.startsWith("@")) {
|
|
53
|
+
console.warn(`No data type found for ${fullPath}`);
|
|
54
|
+
}
|
|
55
|
+
else if (typeof megaDataType === "object") {
|
|
56
|
+
newValue = megaDataType[value];
|
|
57
|
+
}
|
|
58
|
+
else if (megaDataType === "INT64") {
|
|
59
|
+
newValue = Number(value);
|
|
60
|
+
}
|
|
61
|
+
else if (megaDataType === "ENUM") {
|
|
62
|
+
// Some enums aren't embedded in megaDataType, so we need this fallback.
|
|
63
|
+
// @ts-expect-error typescript doesn't like accessing items in a namespace with a string
|
|
64
|
+
newValue = enums_1.enums[fields_1.fields.enumFields[fullPath]][value]; // e.g. enums['CampaignStatus'][ENABLED] = "2"
|
|
65
|
+
}
|
|
66
|
+
return newValue;
|
|
67
|
+
};
|
|
68
|
+
const getTypeFromPath = (path) => {
|
|
69
|
+
const cachedResult = fieldTypeCache.get(path);
|
|
70
|
+
if (cachedResult) {
|
|
71
|
+
return cachedResult;
|
|
72
|
+
}
|
|
73
|
+
const t = get(fieldDataTypes, path);
|
|
74
|
+
fieldTypeCache.set(path, t);
|
|
75
|
+
return t;
|
|
76
|
+
};
|
|
77
|
+
// Copied from youmightnotneed.com
|
|
78
|
+
const get = (obj, path) => {
|
|
79
|
+
if (!path)
|
|
80
|
+
return undefined;
|
|
81
|
+
// Check if path is string or array. Regex : ensure that we do not have '.' and brackets.
|
|
82
|
+
// Regex explained: https://regexr.com/58j0k
|
|
83
|
+
const pathArray = path.match(/([^[.\]])+/g);
|
|
84
|
+
if (!pathArray)
|
|
85
|
+
return undefined;
|
|
86
|
+
// Find value
|
|
87
|
+
return pathArray.reduce((prevObj, key) => prevObj && prevObj[key], obj);
|
|
88
|
+
};
|