google-ads-api 20.0.0 → 21.0.0-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 +4 -4
- package/build/src/client.d.ts +1 -0
- package/build/src/client.js +1 -0
- package/build/src/customer.d.ts +8 -2
- package/build/src/customer.js +207 -74
- package/build/src/parserRest.d.ts +4 -0
- package/build/src/parserRest.js +106 -0
- package/build/src/protos/autogen/enums.d.ts +1194 -1043
- package/build/src/protos/autogen/enums.js +1196 -1034
- package/build/src/protos/autogen/fields.d.ts +1 -0
- package/build/src/protos/autogen/fields.js +3 -1
- package/build/src/protos/autogen/serviceFactory.js +1 -1
- package/build/src/protos/index.d.ts +7 -7
- package/build/src/protos/index.js +7 -6
- package/build/src/query.js +1 -1
- package/build/src/service.d.ts +2 -1
- package/build/src/service.js +37 -16
- package/build/src/testUtils.d.ts +38 -0
- package/build/src/testUtils.js +76 -5
- package/build/src/utils.d.ts +0 -5
- package/build/src/utils.js +0 -15
- package/build/src/version.d.ts +1 -1
- package/build/src/version.js +1 -1
- package/package.json +18 -7
package/README.md
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
# Features
|
|
27
27
|
|
|
28
28
|
- Simple and easy to use API
|
|
29
|
-
- Uses [
|
|
29
|
+
- Uses [REST](https://developers.google.com/google-ads/api/rest/design/overview) and [Protocol Buffers](https://developers.google.com/protocol-buffers/) internally
|
|
30
30
|
- Typescript definitions for all resources, enums, errors and services
|
|
31
31
|
- Provides all API functionality
|
|
32
32
|
|
|
@@ -93,7 +93,7 @@ const customer = client.Customer({
|
|
|
93
93
|
|
|
94
94
|
## List accessible customers
|
|
95
95
|
|
|
96
|
-
This is a special client method for listing the accessible customers for a given refresh token, and is equivalent to [CustomerService.listAccessibleCustomers](https://developers.google.com/google-ads/api/reference/rpc/
|
|
96
|
+
This is a special client method for listing the accessible customers for a given refresh token, and is equivalent to [CustomerService.listAccessibleCustomers](https://developers.google.com/google-ads/api/reference/rpc/v20/CustomerService#listaccessiblecustomers). It returns the resource names of available customer accounts.
|
|
97
97
|
|
|
98
98
|
```ts
|
|
99
99
|
const client = new GoogleAdsApi({
|
|
@@ -671,9 +671,9 @@ const customer = client.Customer(
|
|
|
671
671
|
|
|
672
672
|
## Error handling
|
|
673
673
|
|
|
674
|
-
All errors, apart from GRPC specific cases (such as a connection problem or timeout, [see more here](https://github.com/grpc/grpc/blob/master/doc/statuscodes.md)), are instances of a [GoogleAdsFailure](https://developers.google.com/google-ads/api/reference/rpc/
|
|
674
|
+
All errors, apart from GRPC specific cases (such as a connection problem or timeout, [see more here](https://github.com/grpc/grpc/blob/master/doc/statuscodes.md)), are instances of a [GoogleAdsFailure](https://developers.google.com/google-ads/api/reference/rpc/v20/GoogleAdsFailure).
|
|
675
675
|
|
|
676
|
-
You can find a list of all error types for a specific version in [the official documentation](https://developers.google.com/google-ads/api/reference/rpc/
|
|
676
|
+
You can find a list of all error types for a specific version in [the official documentation](https://developers.google.com/google-ads/api/reference/rpc/v20/AccessInvitationErrorEnum.AccessInvitationError), as well as more information about [handling errors here](https://developers.google.com/google-ads/api/docs/best-practices/error-types).
|
|
677
677
|
|
|
678
678
|
```ts
|
|
679
679
|
import { errors } from "google-ads-api";
|
package/build/src/client.d.ts
CHANGED
package/build/src/client.js
CHANGED
package/build/src/customer.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { ClientOptions } from "./client";
|
|
|
3
3
|
import { Hooks } from "./hooks";
|
|
4
4
|
import { services } from "./protos";
|
|
5
5
|
import ServiceFactory from "./protos/autogen/serviceFactory";
|
|
6
|
-
import { CustomerOptions, MutateOperation, MutateOptions, ReportOptions,
|
|
6
|
+
import { CustomerOptions, MutateOperation, MutateOptions, ReportOptions, RequestOptions } from "./types";
|
|
7
7
|
export declare class Customer extends ServiceFactory {
|
|
8
8
|
constructor(clientOptions: ClientOptions, customerOptions: CustomerOptions, hooks?: Hooks);
|
|
9
9
|
/**
|
|
@@ -30,7 +30,7 @@ export declare class Customer extends ServiceFactory {
|
|
|
30
30
|
@description Get the total row count of a report.
|
|
31
31
|
@hooks none
|
|
32
32
|
*/
|
|
33
|
-
reportCount(options: Readonly<
|
|
33
|
+
reportCount(options: Readonly<ReportOptions>): Promise<number | undefined>;
|
|
34
34
|
/**
|
|
35
35
|
@description Stream query using ReportOptions. If a generic type is provided, it must be the type of a single row.
|
|
36
36
|
If a summary row is requested then this will be the last emitted row of the stream.
|
|
@@ -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.
|
|
@@ -52,7 +58,10 @@ class Customer extends serviceFactory_1.default {
|
|
|
52
58
|
// must get at least one row
|
|
53
59
|
const { gaqlQuery, requestOptions } = (0, query_1.buildQuery)({ ...options, limit: 1 });
|
|
54
60
|
// We do not allow this field in reportOptions, however it is still a valid request option
|
|
55
|
-
requestOptions.search_settings = {
|
|
61
|
+
requestOptions.search_settings = {
|
|
62
|
+
return_total_results_count: true,
|
|
63
|
+
return_summary_row: false,
|
|
64
|
+
};
|
|
56
65
|
const useHooks = false; // to avoid cacheing conflicts
|
|
57
66
|
const { totalResultsCount } = await this.querier(gaqlQuery, requestOptions, options, useHooks);
|
|
58
67
|
return totalResultsCount;
|
|
@@ -112,39 +121,124 @@ class Customer extends serviceFactory_1.default {
|
|
|
112
121
|
});
|
|
113
122
|
}
|
|
114
123
|
async search(gaqlQuery, requestOptions) {
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
124
|
+
const accessToken = await this.getAccessToken();
|
|
125
|
+
try {
|
|
126
|
+
const rawResponse = await (0, axios_1.default)(this.prepareGoogleAdsServicePostRequestArgs("search", accessToken, {
|
|
127
|
+
data: {
|
|
128
|
+
query: gaqlQuery,
|
|
129
|
+
...requestOptions,
|
|
130
|
+
},
|
|
131
|
+
}));
|
|
132
|
+
const searchResponse = rawResponse.data;
|
|
133
|
+
const results = searchResponse.results ?? [];
|
|
134
|
+
const response = results.map((row) => this.decamelizeKeysIfNeeded(row));
|
|
135
|
+
const summaryRow = this.decamelizeKeysIfNeeded(searchResponse.summaryRow);
|
|
136
|
+
const nextPageToken = searchResponse.nextPageToken;
|
|
137
|
+
const totalResultsCount = searchResponse.totalResultsCount
|
|
138
|
+
? +searchResponse.totalResultsCount
|
|
139
|
+
: undefined;
|
|
140
|
+
return { response, nextPageToken, totalResultsCount, summaryRow };
|
|
141
|
+
}
|
|
142
|
+
catch (e) {
|
|
143
|
+
if (e.response?.data.error.details[0]) {
|
|
144
|
+
throw new protos_1.errors.GoogleAdsFailure(this.decamelizeKeysIfNeeded(e.response.data.error.details[0]));
|
|
145
|
+
}
|
|
146
|
+
throw e;
|
|
128
147
|
}
|
|
129
|
-
return { response, nextPageToken, totalResultsCount };
|
|
130
148
|
}
|
|
131
|
-
async paginatedSearch(gaqlQuery, requestOptions
|
|
149
|
+
async paginatedSearch(gaqlQuery, requestOptions) {
|
|
150
|
+
/*
|
|
151
|
+
When possible, use the searchStream method to avoid the overhead of pagination.
|
|
152
|
+
*/
|
|
153
|
+
if (requestOptions.page_size === undefined &&
|
|
154
|
+
requestOptions.search_settings === undefined // If search_settings is set, we can't use searchStream.
|
|
155
|
+
) {
|
|
156
|
+
// If no pagination or summary options are set, we can use the non-paginated search method.
|
|
157
|
+
const { response } = await this.useStreamToImitateRegularSearch(gaqlQuery, requestOptions);
|
|
158
|
+
return { response };
|
|
159
|
+
}
|
|
132
160
|
const response = [];
|
|
133
161
|
let nextPageToken = undefined;
|
|
134
162
|
const initialSearch = await this.search(gaqlQuery, requestOptions);
|
|
135
|
-
|
|
136
|
-
response.
|
|
163
|
+
let totalResultsCount = initialSearch.totalResultsCount;
|
|
164
|
+
// Sometimes (when no results?) the totalResultsCount field is not included in the response.
|
|
165
|
+
// In this case, we set it to 0.
|
|
166
|
+
if (requestOptions.search_settings?.return_total_results_count &&
|
|
167
|
+
initialSearch.totalResultsCount === undefined) {
|
|
168
|
+
totalResultsCount = 0;
|
|
169
|
+
}
|
|
170
|
+
let summaryRow = initialSearch.summaryRow;
|
|
171
|
+
response.push(...initialSearch.response);
|
|
137
172
|
nextPageToken = initialSearch.nextPageToken;
|
|
138
173
|
while (nextPageToken) {
|
|
139
174
|
const nextSearch = await this.search(gaqlQuery, {
|
|
140
175
|
...requestOptions,
|
|
141
176
|
page_token: nextPageToken,
|
|
142
177
|
});
|
|
143
|
-
response.push(...
|
|
178
|
+
response.push(...nextSearch.response);
|
|
144
179
|
nextPageToken = nextSearch.nextPageToken;
|
|
180
|
+
if (nextSearch.summaryRow) {
|
|
181
|
+
summaryRow = nextSearch.summaryRow;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (summaryRow) {
|
|
185
|
+
response.unshift(summaryRow);
|
|
145
186
|
}
|
|
146
187
|
return { response, totalResultsCount };
|
|
147
188
|
}
|
|
189
|
+
// Google's searchStream method is faster than search, but it does not support all features.
|
|
190
|
+
// When report() is called, we use searchStream if possible, otherwise we use paginatedSearch.
|
|
191
|
+
// Note that just like `paginatedSearch`, this method accumulates results in memory. Use
|
|
192
|
+
// `reportStream` for a more memory-efficient alternative (at the cost of more CPU usage).
|
|
193
|
+
async useStreamToImitateRegularSearch(gaqlQuery, requestOptions) {
|
|
194
|
+
const accessToken = await this.getAccessToken();
|
|
195
|
+
try {
|
|
196
|
+
const args = this.prepareGoogleAdsServicePostRequestArgs("searchStream", accessToken, {
|
|
197
|
+
responseType: "stream",
|
|
198
|
+
data: {
|
|
199
|
+
query: gaqlQuery,
|
|
200
|
+
...requestOptions,
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
const response = await (0, axios_1.default)(args);
|
|
204
|
+
const stream = response.data;
|
|
205
|
+
const buffers = [];
|
|
206
|
+
let rowCount = -ROWS_PER_STREAMED_CHUNK;
|
|
207
|
+
for await (const data of stream) {
|
|
208
|
+
if (this.clientOptions.max_reporting_rows &&
|
|
209
|
+
!this.gaqlQueryStringIncludesLimit(gaqlQuery)) {
|
|
210
|
+
// This is a quick-and-dirty way to count rows, but it's good enough for our purposes.
|
|
211
|
+
// We want to avoid using a proper JSON streamer here for performance reasons.
|
|
212
|
+
if (data.toString("utf-8").includes(`results":`)) {
|
|
213
|
+
rowCount += ROWS_PER_STREAMED_CHUNK;
|
|
214
|
+
}
|
|
215
|
+
if (rowCount > this.clientOptions.max_reporting_rows) {
|
|
216
|
+
throw this.generateTooManyRowsError();
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
buffers.push(data);
|
|
220
|
+
}
|
|
221
|
+
const asString = Buffer.concat(buffers).toString("utf-8");
|
|
222
|
+
const accumulator = [];
|
|
223
|
+
let foundSummaryRow;
|
|
224
|
+
for (const { results, summaryRow } of JSON.parse(asString)) {
|
|
225
|
+
if (summaryRow) {
|
|
226
|
+
foundSummaryRow = this.decamelizeKeysIfNeeded(summaryRow);
|
|
227
|
+
}
|
|
228
|
+
accumulator.push(...(results ?? []).map((row) => {
|
|
229
|
+
return this.decamelizeKeysIfNeeded(row);
|
|
230
|
+
}));
|
|
231
|
+
if (foundSummaryRow) {
|
|
232
|
+
accumulator.unshift(foundSummaryRow);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return { response: accumulator };
|
|
236
|
+
}
|
|
237
|
+
catch (e) {
|
|
238
|
+
await this.handleStreamError(e);
|
|
239
|
+
throw e; // The line above should always throw.
|
|
240
|
+
}
|
|
241
|
+
}
|
|
148
242
|
async querier(gaqlQuery, requestOptions = {}, reportOptions, useHooks = true) {
|
|
149
243
|
const baseHookArguments = {
|
|
150
244
|
credentials: this.credentials,
|
|
@@ -171,14 +265,7 @@ class Customer extends serviceFactory_1.default {
|
|
|
171
265
|
}
|
|
172
266
|
}
|
|
173
267
|
try {
|
|
174
|
-
const
|
|
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);
|
|
268
|
+
const { response, totalResultsCount } = await this.paginatedSearch(gaqlQuery, requestOptions);
|
|
182
269
|
if (this.hooks.onQueryEnd && useHooks) {
|
|
183
270
|
const queryResolution = { resolved: false };
|
|
184
271
|
await this.hooks.onQueryEnd({
|
|
@@ -230,58 +317,81 @@ class Customer extends serviceFactory_1.default {
|
|
|
230
317
|
return;
|
|
231
318
|
}
|
|
232
319
|
}
|
|
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
|
-
});
|
|
258
320
|
try {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
321
|
+
const accessToken = await this.getAccessToken();
|
|
322
|
+
const args = this.prepareGoogleAdsServicePostRequestArgs("searchStream", accessToken, {
|
|
323
|
+
responseType: "stream",
|
|
324
|
+
data: {
|
|
325
|
+
query: gaqlQuery,
|
|
326
|
+
...requestOptions,
|
|
327
|
+
},
|
|
328
|
+
});
|
|
329
|
+
const response = await (0, axios_1.default)(args);
|
|
330
|
+
const stream = response.data;
|
|
331
|
+
// The options below help to make the stream less CPU intensive.
|
|
332
|
+
const parser = new stream_json_1.Parser({
|
|
333
|
+
streamValues: false,
|
|
334
|
+
streamKeys: false,
|
|
335
|
+
packValues: true,
|
|
336
|
+
packKeys: true,
|
|
337
|
+
});
|
|
338
|
+
const pipeline = (0, stream_chain_1.chain)([stream, parser, (0, StreamArray_1.streamArray)()]);
|
|
339
|
+
let count = 0;
|
|
340
|
+
for await (const data of pipeline) {
|
|
341
|
+
const results = data.value.results ??
|
|
342
|
+
(data.value.summaryRow ? [data.value.summaryRow] : undefined) ??
|
|
343
|
+
[];
|
|
344
|
+
count += results.length;
|
|
345
|
+
if (this.clientOptions.max_reporting_rows &&
|
|
346
|
+
count > this.clientOptions.max_reporting_rows &&
|
|
347
|
+
!this.gaqlQueryStringIncludesLimit(gaqlQuery)) {
|
|
348
|
+
throw this.generateTooManyRowsError();
|
|
266
349
|
}
|
|
267
|
-
|
|
268
|
-
|
|
350
|
+
for (const row of results) {
|
|
351
|
+
const parsed = this.decamelizeKeysIfNeeded(row);
|
|
352
|
+
yield parsed;
|
|
269
353
|
}
|
|
270
354
|
}
|
|
355
|
+
return;
|
|
271
356
|
}
|
|
272
|
-
catch (
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
357
|
+
catch (e) {
|
|
358
|
+
try {
|
|
359
|
+
await this.handleStreamError(e);
|
|
360
|
+
}
|
|
361
|
+
catch (_e) {
|
|
362
|
+
if (this.hooks.onStreamError) {
|
|
363
|
+
await this.hooks.onStreamError({
|
|
364
|
+
...baseHookArguments,
|
|
365
|
+
error: _e,
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
throw _e;
|
|
279
369
|
}
|
|
280
|
-
throw googleAdsError;
|
|
281
370
|
}
|
|
282
|
-
|
|
283
|
-
|
|
371
|
+
}
|
|
372
|
+
async handleStreamError(e) {
|
|
373
|
+
if (!e?.response?.data) {
|
|
374
|
+
throw e;
|
|
284
375
|
}
|
|
376
|
+
// The error is a stream, so some effort is required to parse it.
|
|
377
|
+
const stream = e.response.data;
|
|
378
|
+
const pipeline = (0, stream_chain_1.chain)([stream, (0, stream_json_1.parser)(), (0, StreamArray_1.streamArray)()]);
|
|
379
|
+
const defaultErrorMessage = "Unknown GoogleAdsFailure";
|
|
380
|
+
let googleAdsFailure = new Error(defaultErrorMessage);
|
|
381
|
+
// Only throw the first error.
|
|
382
|
+
pipeline.once("data", (data) => {
|
|
383
|
+
if (data?.value?.error?.details?.[0]) {
|
|
384
|
+
googleAdsFailure = new protos_1.errors.GoogleAdsFailure(this.decamelizeKeysIfNeeded(data.value.error.details[0]));
|
|
385
|
+
}
|
|
386
|
+
else {
|
|
387
|
+
googleAdsFailure = new Error(data?.value?.error?.message ?? defaultErrorMessage, { cause: data?.value?.error ?? data?.value });
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
// Must always reject.
|
|
391
|
+
await new Promise((_, reject) => {
|
|
392
|
+
pipeline.on("end", () => reject(googleAdsFailure));
|
|
393
|
+
pipeline.on("error", (err) => reject(err));
|
|
394
|
+
});
|
|
285
395
|
}
|
|
286
396
|
/**
|
|
287
397
|
* @description Creates, updates, or removes resources. This method supports atomic transactions
|
|
@@ -361,5 +471,28 @@ class Customer extends serviceFactory_1.default {
|
|
|
361
471
|
},
|
|
362
472
|
};
|
|
363
473
|
}
|
|
474
|
+
prepareGoogleAdsServicePostRequestArgs(functionName, accessToken, extra) {
|
|
475
|
+
return {
|
|
476
|
+
method: "POST",
|
|
477
|
+
url: `https://googleads.googleapis.com/${version_1.googleAdsVersion}/customers/${this.customerOptions.customer_id}/googleAds:${functionName}`,
|
|
478
|
+
headers: {
|
|
479
|
+
Authorization: `Bearer ${accessToken}`,
|
|
480
|
+
...this.callHeaders,
|
|
481
|
+
},
|
|
482
|
+
...extra,
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
decamelizeKeysIfNeeded(input) {
|
|
486
|
+
if (this.clientOptions.disable_parsing) {
|
|
487
|
+
return input;
|
|
488
|
+
}
|
|
489
|
+
return (0, parserRest_1.decamelizeKeys)(input);
|
|
490
|
+
}
|
|
491
|
+
gaqlQueryStringIncludesLimit(gaqlQuery) {
|
|
492
|
+
return gaqlQuery.toLowerCase().includes("limit ");
|
|
493
|
+
}
|
|
494
|
+
generateTooManyRowsError() {
|
|
495
|
+
return new Error(`Exceeded the maximum number of rows set by "max_reporting_rows" (${this.clientOptions.max_reporting_rows}).`);
|
|
496
|
+
}
|
|
364
497
|
}
|
|
365
498
|
exports.Customer = Customer;
|
|
@@ -0,0 +1,106 @@
|
|
|
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
|
+
// Special handling for FieldMask types - REST API returns them as comma-separated strings
|
|
57
|
+
if (megaDataType.paths === "STRING" && typeof value === "string") {
|
|
58
|
+
// This is a FieldMask field, convert the comma-separated string to the expected format
|
|
59
|
+
// Also convert each path from camelCase to snake_case
|
|
60
|
+
newValue = {
|
|
61
|
+
paths: value.split(",").map((p) => {
|
|
62
|
+
// Handle nested paths like "ipBlock.ipAddress"
|
|
63
|
+
return p
|
|
64
|
+
.trim()
|
|
65
|
+
.split(".")
|
|
66
|
+
.map((segment) => (0, utils_1.toSnakeCase)(segment))
|
|
67
|
+
.join(".");
|
|
68
|
+
}),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
// Normal enum handling
|
|
73
|
+
newValue = megaDataType[value];
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
else if (megaDataType === "INT64") {
|
|
77
|
+
newValue = Number(value);
|
|
78
|
+
}
|
|
79
|
+
else if (megaDataType === "ENUM") {
|
|
80
|
+
// Some enums aren't embedded in megaDataType, so we need this fallback.
|
|
81
|
+
// @ts-expect-error typescript doesn't like accessing items in a namespace with a string
|
|
82
|
+
newValue = enums_1.enums[fields_1.fields.enumFields[fullPath]][value]; // e.g. enums['CampaignStatus'][ENABLED] = "2"
|
|
83
|
+
}
|
|
84
|
+
return newValue;
|
|
85
|
+
};
|
|
86
|
+
const getTypeFromPath = (path) => {
|
|
87
|
+
const cachedResult = fieldTypeCache.get(path);
|
|
88
|
+
if (cachedResult) {
|
|
89
|
+
return cachedResult;
|
|
90
|
+
}
|
|
91
|
+
const t = get(fieldDataTypes, path);
|
|
92
|
+
fieldTypeCache.set(path, t);
|
|
93
|
+
return t;
|
|
94
|
+
};
|
|
95
|
+
// Copied from youmightnotneed.com
|
|
96
|
+
const get = (obj, path) => {
|
|
97
|
+
if (!path)
|
|
98
|
+
return undefined;
|
|
99
|
+
// Check if path is string or array. Regex : ensure that we do not have '.' and brackets.
|
|
100
|
+
// Regex explained: https://regexr.com/58j0k
|
|
101
|
+
const pathArray = path.match(/([^[.\]])+/g);
|
|
102
|
+
if (!pathArray)
|
|
103
|
+
return undefined;
|
|
104
|
+
// Find value
|
|
105
|
+
return pathArray.reduce((prevObj, key) => prevObj && prevObj[key], obj);
|
|
106
|
+
};
|