google-ads-api
Version:
Google Ads API Client Library for Node.js
212 lines (211 loc) • 8.85 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Service = exports.FAILURE_KEY = void 0;
const google_gax_1 = require("google-gax");
const google_auth_library_1 = require("google-auth-library");
const protos_1 = require("./protos");
const utils_1 = require("./utils");
const version_1 = require("./version");
const ttlcache_1 = __importDefault(require("@isaacs/ttlcache"));
// Make sure to update this version number when upgrading
exports.FAILURE_KEY = `google.ads.googleads.${version_1.googleAdsVersion}.errors.googleadsfailure-bin`;
// A global service cache to avoid re-initialising services
const serviceCache = new ttlcache_1.default({
max: 1_000,
ttl: 10 * 60 * 1000, // 10 minutes
dispose: async (service) => {
// Close connections when services are removed from the cache
await service.close();
},
});
// A global access token cache used by REST calls. Issued tokens expire after 1 hour, so we cache them for 50 minutes.
const accessTokenCache = new ttlcache_1.default({
max: 100_000,
ttl: 50 * 60 * 1000, // 50 minutes
});
class Service {
clientOptions;
customerOptions;
hooks;
constructor(clientOptions, customerOptions, hooks) {
this.clientOptions = clientOptions;
this.customerOptions = customerOptions;
this.hooks = hooks ?? {};
// @ts-expect-error All fields don't need to be set here
this.serviceCache = {};
}
get credentials() {
return {
customer_id: this.customerOptions.customer_id,
login_customer_id: this.customerOptions.login_customer_id,
linked_customer_id: this.customerOptions.linked_customer_id,
};
}
get callHeaders() {
const headers = {
"developer-token": this.clientOptions.developer_token,
};
if (this.customerOptions.login_customer_id) {
headers["login-customer-id"] = this.customerOptions.login_customer_id;
}
if (this.customerOptions.linked_customer_id) {
headers["linked-customer-id"] = this.customerOptions.linked_customer_id;
}
return headers;
}
// Used only by gRPC calls
getCredentials() {
const sslCreds = google_gax_1.grpc.credentials.createSsl();
const authClient = new google_auth_library_1.UserRefreshClient(this.clientOptions.client_id, this.clientOptions.client_secret, this.customerOptions.refresh_token);
const credentials = google_gax_1.grpc.credentials.combineChannelCredentials(sslCreds, google_gax_1.grpc.credentials.createFromGoogleCredential(authClient));
return credentials;
}
// Used only by REST calls
async getAccessToken() {
const cachedToken = accessTokenCache.get(this.customerOptions.refresh_token);
if (cachedToken) {
return cachedToken;
}
const oAuth2Client = new google_auth_library_1.OAuth2Client(this.clientOptions.client_id, this.clientOptions.client_secret);
oAuth2Client.setCredentials({
refresh_token: this.customerOptions.refresh_token,
});
const { token } = await oAuth2Client.getAccessToken();
if (typeof token !== "string") {
throw new Error("Failed to retrieve access token");
}
accessTokenCache.set(this.customerOptions.refresh_token, token);
return token;
}
loadService(service) {
const serviceCacheKey = `${service}_${this.customerOptions.refresh_token}`;
if (serviceCache.has(serviceCacheKey)) {
return serviceCache.get(serviceCacheKey);
}
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { [service]: protoService } = require("google-ads-node");
if (typeof protoService === "undefined") {
throw new Error(`Service "${service}" could not be found`);
}
// Initialising services can take a few ms, so we cache when possible.
const client = new protoService({
sslCreds: this.getCredentials(),
});
serviceCache.set(serviceCacheKey, client);
return client;
}
getGoogleAdsError(error) {
// @ts-expect-error No type exists for GA query error
if (typeof error?.metadata?.internalRepr.get(exports.FAILURE_KEY) === "undefined") {
return error;
}
// @ts-expect-error No type exists for GA query error
const [buffer] = error.metadata.internalRepr.get(exports.FAILURE_KEY);
return this.decodeGoogleAdsFailureBuffer(buffer);
}
decodeGoogleAdsFailureBuffer(buffer) {
const googleAdsFailure = protos_1.errors.GoogleAdsFailure.decode(buffer);
return googleAdsFailure;
}
decodePartialFailureError(response) {
if (typeof response?.partial_failure_error === "undefined" ||
!response?.partial_failure_error) {
return response;
}
const { details } = response.partial_failure_error;
const buffer = details?.find((d) => d.type_url.includes("errors.GoogleAdsFailure"))?.value;
if (typeof buffer === "undefined") {
return response;
}
// Update the partial failure field with the decoded error details
return {
...response,
partial_failure_error: this.decodeGoogleAdsFailureBuffer(buffer),
};
}
buildSearchRequestAndService(gaql, options) {
const service = this.loadService("GoogleAdsServiceClient");
const request = new protos_1.services.SearchGoogleAdsRequest({
customer_id: this.customerOptions.customer_id,
query: gaql,
...options,
});
return { service, request };
}
buildSearchStreamRequestAndService(gaql, options) {
const service = this.loadService("GoogleAdsServiceClient");
const request = new protos_1.services.SearchGoogleAdsStreamRequest({
customer_id: this.customerOptions.customer_id,
query: gaql,
...options,
});
return { service, request };
}
buildMutationRequestAndService(mutations, options) {
const service = this.loadService("GoogleAdsServiceClient");
const mutateOperations = mutations.map((mutation) => {
const opKey = (0, utils_1.toSnakeCase)(`${mutation.entity}Operation`);
const operation = {
[mutation.operation ?? "create"]: mutation.resource,
};
if (mutation.operation === "create" &&
//@ts-ignore
mutation?.exempt_policy_violation_keys?.length) {
//@ts-ignore
operation.exempt_policy_violation_keys =
mutation.exempt_policy_violation_keys;
}
else if (mutation.operation === "update") {
// @ts-expect-error Resource operations should have updateMask defined
operation.update_mask = (0, utils_1.getFieldMask)(mutation.resource);
}
const mutateOperation = new protos_1.services.MutateOperation({
[opKey]: operation,
});
return mutateOperation;
});
const request = new protos_1.services.MutateGoogleAdsRequest({
customer_id: this.customerOptions.customer_id,
mutate_operations: mutateOperations,
...options,
});
return { service, request };
}
buildOperations(type, entities, message) {
const ops = entities.map((e) => {
const op = {
[type]: e,
operation: type,
};
//@ts-ignore
if (type === "create" && e?.exempt_policy_violation_keys?.length) {
// @ts-expect-error Field required for policy violation exemptions
op.exempt_policy_violation_keys = e.exempt_policy_violation_keys;
//@ts-ignore
delete e.exempt_policy_violation_keys;
}
else if (type === "update") {
// @ts-expect-error Field required for updates
op.update_mask = (0, utils_1.getFieldMask)(
// @ts-expect-error Message types have a toObject method
message.toObject(e, {
defaults: false,
}));
}
return op;
});
return ops;
}
buildRequest(operations, options) {
const request = {
customer_id: this.customerOptions.customer_id,
operations,
...options,
};
return request;
}
}
exports.Service = Service;