Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement service account authentication #2

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5,585 changes: 5,585 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,18 @@
"author": "Opteo",
"license": "MIT",
"dependencies": {
"@faker-js/faker": "^8.4.1",
"@isaacs/ttlcache": "^1.2.2",
"@prisma/client": "^5.13.0",
"google-ads-api": "^16.0.0-rest-beta3",
"google-ads-node": "^13.0.0",
"google-auth-library": "^7.1.0",
"google-auth-library": "^7.14.1",
"google-gax": "^4.3.1",
"google-protobuf": "^3.21.2",
"long": "^4.0.0"
},
"devDependencies": {
"@types/google-protobuf": "^3.15.12",
"@types/jest": "^29.0.1",
"@types/pluralize": "^0.0.29",
"@typescript-eslint/eslint-plugin": "^4.8.2",
Expand Down
221,171 changes: 221,171 additions & 0 deletions protos.d.ts

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export interface ClientOptions {
client_secret: string;
developer_token: string;
disable_parsing?: boolean;
service_account_key_file?: string;
}

export class Client {
Expand Down
12 changes: 6 additions & 6 deletions src/customer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export class Customer extends ServiceFactory {
return response;
}

/**
/**
@description Stream query using a raw GAQL string. If a generic type is provided, it must be the type of a single row.
If a summary row is requested then this will be the last emitted row of the stream.
@hooks onStreamStart, onStreamError
Expand All @@ -60,7 +60,7 @@ export class Customer extends ServiceFactory {
}
}

/**
/**
@description Single query using ReportOptions.
If a summary row is requested then this will be the first row of the results.
@hooks onQueryStart, onQueryError, onQueryEnd
Expand Down Expand Up @@ -98,7 +98,7 @@ export class Customer extends ServiceFactory {
return totalResultsCount;
}

/**
/**
@description Stream query using ReportOptions. If a generic type is provided, it must be the type of a single row.
If a summary row is requested then this will be the last emitted row of the stream.
@hooks onStreamStart, onStreamError
Expand All @@ -116,7 +116,7 @@ export class Customer extends ServiceFactory {
}
}

/**
/**
@description Retreive the raw stream using ReportOptions.
@hooks onStreamStart
@example
Expand Down Expand Up @@ -400,7 +400,7 @@ export class Customer extends ServiceFactory {
* campaign budget, or perform up to thousands of mutates atomically.
* @hooks onMutationStart, onMutationError, onMutationEnd
*/
public async mutateResources<T>(
public async mutateResources<T extends Record<string, any>>(
mutations: MutateOperation<T>[],
mutateOptions: MutateOptions = {}
): Promise<services.MutateGoogleAdsResponse> {
Expand Down Expand Up @@ -444,7 +444,7 @@ export class Customer extends ServiceFactory {
)[0] as services.MutateGoogleAdsResponse;

const parsedResponse = request.partial_failure
? this.decodePartialFailureError(response)
? this.decodePartialFailureError(response as services.MutateGoogleAdsResponse & { partial_failure_error?: { details?: { type_url: string; value: Buffer; }[] | undefined; } })
: response;

if (this.hooks.onMutationEnd) {
Expand Down
28 changes: 28 additions & 0 deletions src/field_mask_pb.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Type definitions for google-protobuf FieldMask
declare module 'google-protobuf/google/protobuf/field_mask_pb' {
import * as jspb from 'google-protobuf';

export class FieldMask extends jspb.Message {
constructor(opt_data?: any);

getPathsList(): string[];
setPathsList(value: string[]): FieldMask;
addPaths(value: string, opt_index?: number): FieldMask;
clearPathsList(): FieldMask;

toObject(includeInstance?: boolean): FieldMask.AsObject;
serializeBinary(): Uint8Array;
static deserializeBinary(bytes: Uint8Array): FieldMask;
static serializeBinaryToWriter(message: FieldMask, writer: jspb.BinaryWriter): void;
static toObject(includeInstance: boolean, msg: FieldMask): FieldMask.AsObject;

// Add any additional method or property signatures needed by FieldMask
}

namespace FieldMask {
type AsObject = {
pathsList: string[];
// Add any additional field signatures needed by FieldMask.AsObject
}
}
}
5 changes: 3 additions & 2 deletions src/query.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
QueryError,
buildSelectClause,
buildFromClause,
getFromClause,
validateConstraintKeyAndValue,
convertNumericEnumToString,
extractConstraintConditions,
Expand Down Expand Up @@ -94,7 +95,7 @@ describe("buildFromClause", () => {
});

it("correctly parses entity", () => {
const fromClause = buildFromClause(options.entity);
const fromClause = buildFromClause(getFromClause(options.entity));
expect(fromClause).toEqual(` FROM ad_group`);
});
});
Expand Down Expand Up @@ -718,7 +719,7 @@ describe("buildQuery", () => {
ad_group.name, ad_group.id, metrics.impressions, metrics.cost_micros, segments.date
FROM
ad_group
WHERE
WHERE
ad_group.status = "PAUSED"
AND campaign.advertising_channel_type = "SEARCH"
AND metrics.clicks > 10
Expand Down
40 changes: 25 additions & 15 deletions src/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,12 +91,27 @@ export function buildSelectClause(
return `${QueryKeywords.SELECT} ${selections}` as const;
}

export function buildFromClause(entity: ReportOptions["entity"]): FromClause {
const entityToFromClauseMap: Record<string, FromClause> = {
'campaign': ' FROM campaign' as FromClause,
'ad_group': ' FROM ad_group' as FromClause,
'ad_schedule_view': ' FROM ad_schedule_view' as FromClause,
// ... additional entities and their corresponding FromClause literals
};

export function getFromClause(entity: string): FromClause {
const fromClause = entityToFromClauseMap[entity];
if (!fromClause) {
throw new Error(`Invalid entity: ${entity}`);
}
return fromClause;
}

export function buildFromClause(entity: FromClause): FromClause {
if (typeof entity === "undefined") {
throw new Error(QueryError.UNDEFINED_ENTITY);
}

return ` ${QueryKeywords.FROM} ${entity}` as const;
return entity;
}

export function validateConstraintKeyAndValue(
Expand All @@ -108,7 +123,7 @@ export function validateConstraintKeyAndValue(
val: ParsedConstraintValue;
} {
if (typeof val === "number" || typeof val === "boolean") {
return { op: "=", val: convertNumericEnumToString(key, val) };
return { op, val: convertNumericEnumToString(key, val) };
}

if (typeof val === "string") {
Expand All @@ -117,7 +132,7 @@ export function validateConstraintKeyAndValue(
}

return {
op: "=",
op,
val: new RegExp(/^'.*'$|^".*"$/g).test(val) ? val : `"${val}"`,
}; // must start and end in either single or double quotation marks
}
Expand All @@ -133,7 +148,7 @@ export function validateConstraintKeyAndValue(
})
.join(`, `);

return { op: "IN", val: `(${stringifiedValue})` };
return { op: op === "IN" ? op : "=", val: `(${stringifiedValue})` };
}

throw new Error(QueryError.INVALID_CONSTRAINT_VALUE(key, val));
Expand Down Expand Up @@ -164,31 +179,26 @@ export function extractConstraintConditions(
} else if (Array.isArray(constraints)) {
return constraints.map((con: Constraint) => {
if (typeof con === "object" && !Array.isArray(con) && con !== null) {
// @ts-ignore
if (con.key && con.op && typeof con.val !== "undefined") {
const { key, op, val }: ConstraintType1 = con as ConstraintType1;

if (typeof key !== "string") {
throw new Error(QueryError.INVALID_CONSTRAINT_KEY);
}
const validatedValue = validateConstraintKeyAndValue(key, op, val);
// @ts-ignore
return `${key} ${op} ${validatedValue.val}` as const;
return `${key} ${validatedValue.op} ${validatedValue.val}` as ConstraintString;
} else if (Object.keys(con).length === 1) {
const [[key, val]] = Object.entries(con);

const validatedValue = validateConstraintKeyAndValue(
key as ConstraintKey,
"=",
val as ConstraintValue
);

return `${key} ${validatedValue.op} ${validatedValue.val}` as const;
return `${key} ${validatedValue.op} ${validatedValue.val}` as ConstraintString;
} else {
throw new Error(QueryError.INVALID_CONSTRAINT_OBJECT_FORMAT);
}
} else if (typeof con === "string") {
return con;
return con as ConstraintString;
} else {
throw new Error(QueryError.INVALID_CONSTRAINT_OBJECT_FORMAT);
}
Expand All @@ -201,7 +211,7 @@ export function extractConstraintConditions(
val as ConstraintValue
);

return `${key} ${validatedValue.op} ${validatedValue.val}` as const;
return `${key} ${validatedValue.op} ${validatedValue.val}` as ConstraintString;
});
} else {
throw new Error(QueryError.INVALID_CONSTRAINTS_FORMAT);
Expand Down Expand Up @@ -406,7 +416,7 @@ export function buildQuery(reportOptions: Readonly<ReportOptions>): {
reportOptions.metrics,
reportOptions.segments
);
const FROM: FromClause = buildFromClause(reportOptions.entity);
const FROM: FromClause = buildFromClause(getFromClause(reportOptions.entity));
const WHERE: WhereClause = buildWhereClause(
reportOptions.constraints,
reportOptions.date_constant,
Expand Down
43 changes: 17 additions & 26 deletions src/service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { GoogleAdsServiceClient } from "google-ads-node";
import { google } from "google-gax/build/protos/operations";
import { errors, services } from "./protos";
import { FAILURE_KEY } from "./service";
import {
Expand Down Expand Up @@ -42,11 +41,10 @@ describe("Service", () => {
});

describe("getCredentials", () => {
it("should create grpc channel credentials with customer auth", () => {
it("should create grpc channel credentials with customer auth", async () => {
const customer = newCustomer();
// @ts-expect-error Accessing private method for test purposes
const creds = customer.getCredentials();
// This could be better
const creds = await customer.getCredentials();
expect(creds._isSecure()).toEqual(true);
});
});
Expand Down Expand Up @@ -164,37 +162,30 @@ describe("Service", () => {
const failureBuffer =
errors.GoogleAdsFailure.encode(failureMessage).finish();

const response = new services.MutateGoogleAdsResponse({
partial_failure_error: new google.rpc.Status({
details: [
{
type_url: `google.ads.googleads.${googleAdsVersion}.errors.GoogleAdsFailure`,
value: failureBuffer,
},
],
}),
});
const response = {
partial_failure_error: {
details: [{
type_url: `type.googleapis.com/google.ads.googleads.${googleAdsVersion}.errors.GoogleAdsFailure`,
value: Buffer.from(failureBuffer),
}],
},
};

const customer = newCustomer();

const parsedPartialFailureResponse =
// @ts-expect-error Accessing private method for test purposes
customer.decodePartialFailureError(response);

const parsedPartialFailureResponse = customer.decodePartialFailureError(response as unknown as services.MutateGoogleAdsResponse & { partial_failure_error?: { details?: { type_url: string; value: Buffer; }[] | undefined; } });
expect(parsedPartialFailureResponse).toEqual({
mutate_operation_responses: [],
partial_failure_error: failureMessage,
});
});

it("should do nothing if no partial failures exist", () => {
const customer = newCustomer();
// @ts-expect-error Accessing private method for test purposes
const parsedPartialFailureResponse = customer.decodePartialFailureError(
new services.MutateGoogleAdsResponse({
partial_failure_error: undefined,
})
);
const response = {
partial_failure_error: {
details: [],
},
};
const parsedPartialFailureResponse = customer.decodePartialFailureError(response as unknown as services.MutateGoogleAdsResponse & { partial_failure_error?: { details?: { type_url: string; value: Buffer; }[] | undefined; } });
expect(parsedPartialFailureResponse).toEqual({
mutate_operation_responses: [],
});
Expand Down
Loading