google-ads-api 24.1.0 → 25.1.0-beta.0
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 +9 -1
- package/build/{src → cjs}/client.d.ts +6 -4
- package/build/{src → cjs}/client.js +4 -4
- package/build/{src → cjs}/customer.d.ts +5 -5
- package/build/{src → cjs}/customer.js +24 -20
- package/build/{src → cjs}/hooks.d.ts +2 -2
- package/build/cjs/index.d.ts +9 -0
- package/build/{src → cjs}/index.js +22 -20
- package/build/{src → cjs}/parser.d.ts +2 -2
- package/build/{src → cjs}/parser.js +5 -5
- package/build/{src → cjs}/parserRest.js +7 -7
- package/build/{src → cjs}/protos/autogen/enums.d.ts +1679 -1180
- package/build/{src → cjs}/protos/autogen/enums.js +1696 -1177
- package/build/cjs/protos/autogen/fields.d.ts +1191 -0
- package/build/cjs/protos/autogen/fields.js +205 -0
- package/build/cjs/protos/autogen/resourceNames.d.ts +1668 -0
- package/build/cjs/protos/autogen/resourceNames.js +1669 -0
- package/build/{src → cjs}/protos/autogen/serviceFactory.d.ts +206 -215
- package/build/{src → cjs}/protos/autogen/serviceFactory.js +483 -554
- package/build/cjs/protos/index.d.ts +14 -0
- package/build/{src → cjs}/protos/index.js +12 -12
- package/build/{src → cjs}/query.d.ts +2 -2
- package/build/{src → cjs}/query.js +5 -5
- package/build/{src → cjs}/service.d.ts +5 -5
- package/build/{src → cjs}/service.js +64 -22
- package/build/{src → cjs}/types.d.ts +1 -1
- package/build/{src → cjs}/utils.d.ts +1 -1
- package/build/{src → cjs}/utils.js +2 -2
- package/build/cjs/version.d.ts +1 -0
- package/build/{src → cjs}/version.js +1 -1
- package/build/esm/client.d.ts +19 -0
- package/build/esm/client.js +40 -0
- package/build/esm/customer.d.ts +71 -0
- package/build/esm/customer.js +502 -0
- package/build/esm/hooks.d.ts +160 -0
- package/build/esm/hooks.js +1 -0
- package/build/esm/index.d.ts +9 -0
- package/build/esm/index.js +10 -0
- package/build/esm/package.json +1 -0
- package/build/esm/parser.d.ts +21 -0
- package/build/esm/parser.js +113 -0
- package/build/esm/parserRest.d.ts +1 -0
- package/build/esm/parserRest.js +112 -0
- package/build/esm/protos/autogen/enums.d.ts +6002 -0
- package/build/esm/protos/autogen/enums.js +6401 -0
- package/build/esm/protos/autogen/fields.d.ts +1191 -0
- package/build/esm/protos/autogen/fields.js +202 -0
- package/build/esm/protos/autogen/resourceNames.d.ts +1668 -0
- package/build/esm/protos/autogen/resourceNames.js +1475 -0
- package/build/esm/protos/autogen/serviceFactory.d.ts +1825 -0
- package/build/esm/protos/autogen/serviceFactory.js +18795 -0
- package/build/esm/protos/index.d.ts +14 -0
- package/build/esm/protos/index.js +19 -0
- package/build/esm/query.d.ts +62 -0
- package/build/esm/query.js +304 -0
- package/build/esm/service.d.ts +47 -0
- package/build/esm/service.js +222 -0
- package/build/esm/types.d.ts +75 -0
- package/build/esm/types.js +14 -0
- package/build/esm/utils.d.ts +49 -0
- package/build/esm/utils.js +108 -0
- package/build/esm/version.d.ts +1 -0
- package/build/esm/version.js +1 -0
- package/package.json +50 -11
- package/build/src/index.d.ts +0 -8
- package/build/src/protos/autogen/fields.d.ts +0 -1122
- package/build/src/protos/autogen/fields.js +0 -644
- package/build/src/protos/autogen/resourceNames.d.ts +0 -1609
- package/build/src/protos/autogen/resourceNames.js +0 -1982
- package/build/src/protos/index.d.ts +0 -14
- package/build/src/testUtils.d.ts +0 -107
- package/build/src/testUtils.js +0 -333
- package/build/src/version.d.ts +0 -1
- /package/build/{src → cjs}/hooks.js +0 -0
- /package/build/{src → cjs}/parserRest.d.ts +0 -0
- /package/build/{src → cjs}/types.js +0 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import allProtos, { protos } from "google-ads-node";
|
|
2
|
+
export declare const VERSION: "v25";
|
|
3
|
+
export import common = protos.google.ads.googleads.v25.common;
|
|
4
|
+
export import errors = protos.google.ads.googleads.v25.errors;
|
|
5
|
+
export import resources = protos.google.ads.googleads.v25.resources;
|
|
6
|
+
export import services = protos.google.ads.googleads.v25.services;
|
|
7
|
+
export import internalEnums = protos.google.ads.googleads.v25.enums;
|
|
8
|
+
export { enums } from "./autogen/enums.js";
|
|
9
|
+
export { fields, fieldDataTypes } from "./autogen/fields.js";
|
|
10
|
+
export { GoogleAdsServiceClient } from "google-ads-node";
|
|
11
|
+
export type AllServices = Omit<typeof allProtos, typeof VERSION>;
|
|
12
|
+
export type ServiceName = keyof Omit<typeof allProtos, typeof VERSION>;
|
|
13
|
+
export import protobuf = protos.google.protobuf;
|
|
14
|
+
export import longrunning = protos.google.longrunning;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { protos } from "google-ads-node";
|
|
2
|
+
import { googleAdsVersion } from "../version.js";
|
|
3
|
+
// "as vN" is required to avoid type issues later on
|
|
4
|
+
export const VERSION = googleAdsVersion;
|
|
5
|
+
// -- Expose the googleads generated types --
|
|
6
|
+
// The namespace path is shortened for ease of use
|
|
7
|
+
export var common = protos.google.ads.googleads.v25.common;
|
|
8
|
+
export var errors = protos.google.ads.googleads.v25.errors;
|
|
9
|
+
export var resources = protos.google.ads.googleads.v25.resources;
|
|
10
|
+
export var services = protos.google.ads.googleads.v25.services;
|
|
11
|
+
// We compile enums ourselves, the Google ones should be used internally only
|
|
12
|
+
export var internalEnums = protos.google.ads.googleads.v25.enums;
|
|
13
|
+
export { enums } from "./autogen/enums.js";
|
|
14
|
+
export { fields, fieldDataTypes } from "./autogen/fields.js";
|
|
15
|
+
// Common service used for report/query methods
|
|
16
|
+
export { GoogleAdsServiceClient } from "google-ads-node";
|
|
17
|
+
// -- Expose some google protobuf helpers --
|
|
18
|
+
export var protobuf = protos.google.protobuf;
|
|
19
|
+
export var longrunning = protos.google.longrunning;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { fields } from "./protos/index.js";
|
|
2
|
+
import { ConstraintKey, ConstraintOperation, ConstraintValue, ReportOptions, RequestOptions, RequestOptionsWithTotalResults, SortOrder } from "./types.js";
|
|
3
|
+
declare enum QueryKeywords {
|
|
4
|
+
SELECT = "SELECT",
|
|
5
|
+
FROM = "FROM",
|
|
6
|
+
WHERE = "WHERE",
|
|
7
|
+
ORDER_BY = "ORDER BY",
|
|
8
|
+
LIMIT = "LIMIT",
|
|
9
|
+
PARAMETERS = "PARAMETERS",
|
|
10
|
+
AND = "AND",
|
|
11
|
+
OR = "OR"
|
|
12
|
+
}
|
|
13
|
+
type ParsedConstraintValue = string | number | boolean;
|
|
14
|
+
type ConstraintString = `${string} ${ConstraintOperation} ${ParsedConstraintValue}`;
|
|
15
|
+
type SelectClause = `${QueryKeywords.SELECT} ${string}`;
|
|
16
|
+
type FromClause = ` ${QueryKeywords.FROM} ${fields.Resource}`;
|
|
17
|
+
type WhereClause = ` ${QueryKeywords.WHERE} ${string}` | ``;
|
|
18
|
+
export type OrderClause = ` ${QueryKeywords.ORDER_BY} ${string} ${SortOrder}` | ` ${QueryKeywords.ORDER_BY} ${string}` | ``;
|
|
19
|
+
type LimitClause = ` ${QueryKeywords.LIMIT} ${number}` | ``;
|
|
20
|
+
type ParametersClause = ` ${QueryKeywords.PARAMETERS} ${string}` | ``;
|
|
21
|
+
type Query = `${SelectClause}${FromClause}${WhereClause}${OrderClause}${LimitClause}${ParametersClause}`;
|
|
22
|
+
export declare const QueryError: {
|
|
23
|
+
INVALID_CONSTRAINTS_FORMAT: string;
|
|
24
|
+
INVALID_CONSTRAINT_KEY: string;
|
|
25
|
+
INVALID_CONSTRAINT_VALUE: (key: ConstraintKey, val: ConstraintValue) => string;
|
|
26
|
+
INVALID_CONSTRAINT_OBJECT_FORMAT: string;
|
|
27
|
+
INVALID_DATE_CONSTANT_TYPE: (dateConstant: ReportOptions["date_constant"]) => string;
|
|
28
|
+
INVALID_FROM_DATE_TYPE: (fromDate: ReportOptions["from_date"]) => string;
|
|
29
|
+
INVALID_TO_DATE_TYPE: (toDate: ReportOptions["to_date"]) => string;
|
|
30
|
+
INVALID_LIMIT: string;
|
|
31
|
+
INVALID_PARAMETERS: string;
|
|
32
|
+
INVALID_ORDER: string;
|
|
33
|
+
INVALID_ORDERLY: string;
|
|
34
|
+
INVALID_ORDERBY: string;
|
|
35
|
+
INVALID_SORT_ORDER: string;
|
|
36
|
+
MISSING_FIELDS: string;
|
|
37
|
+
MISSING_FROM_DATE: string;
|
|
38
|
+
UNDEFINED_ENTITY: string;
|
|
39
|
+
};
|
|
40
|
+
export declare function buildSelectClause(attributes: ReportOptions["attributes"], metrics: ReportOptions["metrics"], segments: ReportOptions["segments"]): SelectClause;
|
|
41
|
+
export declare function buildFromClause(entity: ReportOptions["entity"]): FromClause;
|
|
42
|
+
export declare function validateConstraintKeyAndValue(key: ConstraintKey, op: ConstraintOperation, val: ConstraintValue): {
|
|
43
|
+
op: ConstraintOperation;
|
|
44
|
+
val: ParsedConstraintValue;
|
|
45
|
+
};
|
|
46
|
+
export declare function convertNumericEnumToString(key: ConstraintKey, val: ParsedConstraintValue): ParsedConstraintValue;
|
|
47
|
+
export declare function extractConstraintConditions(constraints: Readonly<ReportOptions["constraints"]>): ConstraintString[];
|
|
48
|
+
export declare function extractDateConstantConditions(dateConstant: ReportOptions["date_constant"]): ConstraintString[];
|
|
49
|
+
export declare function extractDateConditions(fromDate: ReportOptions["from_date"], toDate: ReportOptions["to_date"]): ConstraintString[];
|
|
50
|
+
export declare function buildWhereClause(constraints: Readonly<ReportOptions["constraints"]>, dateConstant: Readonly<ReportOptions["date_constant"]>, fromDate: Readonly<ReportOptions["from_date"]>, toDate: Readonly<ReportOptions["to_date"]>): WhereClause;
|
|
51
|
+
export declare function buildLimitClause(limit: ReportOptions["limit"]): LimitClause;
|
|
52
|
+
export declare function buildParametersClause(parameters: Readonly<ReportOptions["parameters"]>): ParametersClause;
|
|
53
|
+
export declare function completeOrderly(orderly: string, entity: ReportOptions["entity"]): string;
|
|
54
|
+
export declare function buildOrderClauseOld(orderBy: Readonly<ReportOptions["order_by"]>, sortOrder: Readonly<ReportOptions["sort_order"]>, entity: Readonly<ReportOptions["entity"]>): OrderClause;
|
|
55
|
+
export declare function buildOrderClauseNew(order: Readonly<Required<ReportOptions["order"]>>, entity: Readonly<ReportOptions["entity"]>): OrderClause;
|
|
56
|
+
export declare function buildOrderClause(order: Readonly<ReportOptions["order"]>, orderBy: Readonly<ReportOptions["order_by"]>, sortOrder: Readonly<ReportOptions["sort_order"]>, entity: Readonly<ReportOptions["entity"]>): OrderClause;
|
|
57
|
+
export declare function buildRequestOptions(reportOptions: Readonly<ReportOptions>): RequestOptions;
|
|
58
|
+
export declare function buildQuery(reportOptions: Readonly<ReportOptions>): {
|
|
59
|
+
gaqlQuery: Query;
|
|
60
|
+
requestOptions: RequestOptionsWithTotalResults;
|
|
61
|
+
};
|
|
62
|
+
export {};
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import { enums, fields } from "./protos/index.js";
|
|
2
|
+
import { dateConstants, } from "./types.js";
|
|
3
|
+
var QueryKeywords;
|
|
4
|
+
(function (QueryKeywords) {
|
|
5
|
+
QueryKeywords["SELECT"] = "SELECT";
|
|
6
|
+
QueryKeywords["FROM"] = "FROM";
|
|
7
|
+
QueryKeywords["WHERE"] = "WHERE";
|
|
8
|
+
QueryKeywords["ORDER_BY"] = "ORDER BY";
|
|
9
|
+
QueryKeywords["LIMIT"] = "LIMIT";
|
|
10
|
+
QueryKeywords["PARAMETERS"] = "PARAMETERS";
|
|
11
|
+
QueryKeywords["AND"] = "AND";
|
|
12
|
+
QueryKeywords["OR"] = "OR";
|
|
13
|
+
})(QueryKeywords || (QueryKeywords = {}));
|
|
14
|
+
export const QueryError = {
|
|
15
|
+
INVALID_CONSTRAINTS_FORMAT: "Constraints must be an array of objects or a singular object.",
|
|
16
|
+
INVALID_CONSTRAINT_KEY: "A constraint key must have a string value.",
|
|
17
|
+
INVALID_CONSTRAINT_VALUE: (key, val) =>
|
|
18
|
+
// @ts-ignore
|
|
19
|
+
`The value of the constraint ${key} must be a string, number, boolean, or an array of these types. Here, typeof ${key} is ${typeof val}.`,
|
|
20
|
+
INVALID_CONSTRAINT_OBJECT_FORMAT: "Must specify { key, op, val } or { key: value } when using object-style constraints.",
|
|
21
|
+
INVALID_DATE_CONSTANT_TYPE: (dateConstant) => `Date constant must be a string. Here, typeof date constant is ${typeof dateConstant}`,
|
|
22
|
+
INVALID_FROM_DATE_TYPE: (fromDate) => `From date must be a string. Here, typeof from date is ${typeof fromDate}`,
|
|
23
|
+
INVALID_TO_DATE_TYPE: (toDate) => `To date must be a string. Here, typeof to date is ${typeof toDate}`,
|
|
24
|
+
INVALID_LIMIT: "Limit must be a positive integer.",
|
|
25
|
+
INVALID_PARAMETERS: "Parameters must be a string.",
|
|
26
|
+
INVALID_ORDER: "Order must be an array.",
|
|
27
|
+
INVALID_ORDERLY: "OrderBy arrays must only contain strings.",
|
|
28
|
+
INVALID_ORDERBY: "OrderBy must be a string or an array of strings.",
|
|
29
|
+
INVALID_SORT_ORDER: `Sort order must be "ASC" or "DESC".`,
|
|
30
|
+
MISSING_FIELDS: 'Must specify at least one field in ("attributes","metrics","segments").',
|
|
31
|
+
MISSING_FROM_DATE: 'Expected start date range is missing - "from_date".',
|
|
32
|
+
UNDEFINED_ENTITY: "The entity of the query must be defined.",
|
|
33
|
+
};
|
|
34
|
+
export function buildSelectClause(attributes, metrics, segments) {
|
|
35
|
+
if (!attributes?.length && !metrics?.length && !segments?.length) {
|
|
36
|
+
throw new Error(QueryError.MISSING_FIELDS);
|
|
37
|
+
}
|
|
38
|
+
const selections = [
|
|
39
|
+
...(attributes || []),
|
|
40
|
+
...(metrics || []),
|
|
41
|
+
...(segments || []),
|
|
42
|
+
].join(", ");
|
|
43
|
+
return `${QueryKeywords.SELECT} ${selections}`;
|
|
44
|
+
}
|
|
45
|
+
export function buildFromClause(entity) {
|
|
46
|
+
if (typeof entity === "undefined") {
|
|
47
|
+
throw new Error(QueryError.UNDEFINED_ENTITY);
|
|
48
|
+
}
|
|
49
|
+
return ` ${QueryKeywords.FROM} ${entity}`;
|
|
50
|
+
}
|
|
51
|
+
function formatGaqlString(val) {
|
|
52
|
+
const len = val.length;
|
|
53
|
+
const startsWithSingle = val.startsWith("'");
|
|
54
|
+
const endsWithSingle = val.endsWith("'");
|
|
55
|
+
const startsWithDouble = val.startsWith('"');
|
|
56
|
+
const endsWithDouble = val.endsWith('"');
|
|
57
|
+
// Returns the original string if it seems correctly quoted already.
|
|
58
|
+
if (len >= 2) {
|
|
59
|
+
if (startsWithSingle && endsWithSingle && !val.slice(1, -1).includes("'")) {
|
|
60
|
+
return val;
|
|
61
|
+
}
|
|
62
|
+
if (startsWithDouble && endsWithDouble && !val.slice(1, -1).includes('"')) {
|
|
63
|
+
return val;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// Always use double quotes and escape internal double quotes and backslashes
|
|
67
|
+
if (val.includes('"') || val.includes("\\")) {
|
|
68
|
+
const escapedVal = val.replace(/\\/g, "\\\\").replace(/"/g, '\\"'); // Escape backslashes first, then double quotes
|
|
69
|
+
return `"${escapedVal}"`;
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
// No escaping needed, just wrap in double quotes
|
|
73
|
+
return `"${val}"`;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
export function validateConstraintKeyAndValue(key, op, val) {
|
|
77
|
+
if (typeof val === "number" || typeof val === "boolean") {
|
|
78
|
+
return { op: "=", val: convertNumericEnumToString(key, val) };
|
|
79
|
+
}
|
|
80
|
+
if (typeof val === "string") {
|
|
81
|
+
if (dateConstants.includes(val)) {
|
|
82
|
+
return { op, val };
|
|
83
|
+
}
|
|
84
|
+
return { op: "=", val: formatGaqlString(val) };
|
|
85
|
+
}
|
|
86
|
+
if (Array.isArray(val)) {
|
|
87
|
+
const stringifiedValue = val
|
|
88
|
+
.map((v) => {
|
|
89
|
+
if (typeof v === "string") {
|
|
90
|
+
return formatGaqlString(v);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
return convertNumericEnumToString(key, v);
|
|
94
|
+
}
|
|
95
|
+
})
|
|
96
|
+
.join(`, `);
|
|
97
|
+
return { op: "IN", val: `(${stringifiedValue})` };
|
|
98
|
+
}
|
|
99
|
+
throw new Error(QueryError.INVALID_CONSTRAINT_VALUE(key, val));
|
|
100
|
+
}
|
|
101
|
+
export function convertNumericEnumToString(key, val) {
|
|
102
|
+
// @ts-expect-error key does not always match an enum field
|
|
103
|
+
if (fields.enumFields[key] && typeof val === "number") {
|
|
104
|
+
// @ts-expect-error typescript doesn't like accessing items in a namespace with a string
|
|
105
|
+
const enumStringValue = enums[fields.enumFields[key]][val]; // e.g. enums['CampaignStatus'][2] = "ENABLED"
|
|
106
|
+
if (enumStringValue) {
|
|
107
|
+
return `"${enumStringValue}"`;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return val;
|
|
111
|
+
}
|
|
112
|
+
export function extractConstraintConditions(constraints) {
|
|
113
|
+
if (typeof constraints === "undefined") {
|
|
114
|
+
return [];
|
|
115
|
+
}
|
|
116
|
+
else if (Array.isArray(constraints)) {
|
|
117
|
+
return constraints.map((con) => {
|
|
118
|
+
if (typeof con === "object" && !Array.isArray(con) && con !== null) {
|
|
119
|
+
// @ts-ignore
|
|
120
|
+
if (con.key && con.op && typeof con.val !== "undefined") {
|
|
121
|
+
const { key, op, val } = con;
|
|
122
|
+
if (typeof key !== "string") {
|
|
123
|
+
throw new Error(QueryError.INVALID_CONSTRAINT_KEY);
|
|
124
|
+
}
|
|
125
|
+
const validatedValue = validateConstraintKeyAndValue(key, op, val);
|
|
126
|
+
// @ts-ignore
|
|
127
|
+
return `${key} ${op} ${validatedValue.val}`;
|
|
128
|
+
}
|
|
129
|
+
else if (Object.keys(con).length === 1) {
|
|
130
|
+
const [[key, val]] = Object.entries(con);
|
|
131
|
+
const validatedValue = validateConstraintKeyAndValue(key, "=", val);
|
|
132
|
+
return `${key} ${validatedValue.op} ${validatedValue.val}`;
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
throw new Error(QueryError.INVALID_CONSTRAINT_OBJECT_FORMAT);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
else if (typeof con === "string") {
|
|
139
|
+
return con;
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
throw new Error(QueryError.INVALID_CONSTRAINT_OBJECT_FORMAT);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
else if (typeof constraints === "object" && constraints !== null) {
|
|
147
|
+
return Object.entries(constraints).map(([key, val]) => {
|
|
148
|
+
const validatedValue = validateConstraintKeyAndValue(key, "=", val);
|
|
149
|
+
return `${key} ${validatedValue.op} ${validatedValue.val}`;
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
throw new Error(QueryError.INVALID_CONSTRAINTS_FORMAT);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
export function extractDateConstantConditions(dateConstant) {
|
|
157
|
+
if (typeof dateConstant === "undefined") {
|
|
158
|
+
return [];
|
|
159
|
+
}
|
|
160
|
+
else if (typeof dateConstant !== "string") {
|
|
161
|
+
throw new Error(QueryError.INVALID_DATE_CONSTANT_TYPE(dateConstant));
|
|
162
|
+
}
|
|
163
|
+
return [`segments.date DURING ${dateConstant}`];
|
|
164
|
+
}
|
|
165
|
+
export function extractDateConditions(fromDate, toDate) {
|
|
166
|
+
if (typeof fromDate === "undefined" && typeof toDate === "undefined") {
|
|
167
|
+
return [];
|
|
168
|
+
}
|
|
169
|
+
if (typeof fromDate === "undefined" && typeof toDate !== "undefined") {
|
|
170
|
+
throw new Error(QueryError.MISSING_FROM_DATE);
|
|
171
|
+
}
|
|
172
|
+
if (typeof fromDate !== "undefined" && typeof toDate === "undefined") {
|
|
173
|
+
const d = new Date();
|
|
174
|
+
toDate = `${d.getFullYear()}-${("0" + (d.getMonth() + 1)).slice(-2)}-${("0" + d.getDate()).slice(-2)}`;
|
|
175
|
+
}
|
|
176
|
+
if (typeof fromDate !== "string") {
|
|
177
|
+
throw new Error(QueryError.INVALID_FROM_DATE_TYPE(fromDate));
|
|
178
|
+
}
|
|
179
|
+
if (typeof toDate !== "string") {
|
|
180
|
+
throw new Error(QueryError.INVALID_TO_DATE_TYPE(toDate));
|
|
181
|
+
}
|
|
182
|
+
return [
|
|
183
|
+
`segments.date >= "${fromDate}"`,
|
|
184
|
+
`segments.date <= "${toDate}"`,
|
|
185
|
+
];
|
|
186
|
+
}
|
|
187
|
+
export function buildWhereClause(constraints, dateConstant, fromDate, toDate) {
|
|
188
|
+
const constraintClauses = extractConstraintConditions(constraints);
|
|
189
|
+
const dateConstantClauses = extractDateConstantConditions(dateConstant);
|
|
190
|
+
const dateClauses = extractDateConditions(fromDate, toDate);
|
|
191
|
+
const whereClauses = [
|
|
192
|
+
...constraintClauses,
|
|
193
|
+
...dateConstantClauses,
|
|
194
|
+
...dateClauses,
|
|
195
|
+
].join(` ${QueryKeywords.AND} `);
|
|
196
|
+
return whereClauses.length
|
|
197
|
+
? ` ${QueryKeywords.WHERE} ${whereClauses}`
|
|
198
|
+
: ``;
|
|
199
|
+
}
|
|
200
|
+
export function buildLimitClause(limit) {
|
|
201
|
+
if (typeof limit === "undefined") {
|
|
202
|
+
return ``;
|
|
203
|
+
}
|
|
204
|
+
if (typeof limit !== "number" || limit < 1 || !Number.isInteger(limit)) {
|
|
205
|
+
throw new Error(QueryError.INVALID_LIMIT);
|
|
206
|
+
}
|
|
207
|
+
return ` ${QueryKeywords.LIMIT} ${limit}`;
|
|
208
|
+
}
|
|
209
|
+
export function buildParametersClause(parameters) {
|
|
210
|
+
if (typeof parameters === "undefined") {
|
|
211
|
+
return ``;
|
|
212
|
+
}
|
|
213
|
+
if (typeof parameters !== "string") {
|
|
214
|
+
throw new Error(QueryError.INVALID_PARAMETERS);
|
|
215
|
+
}
|
|
216
|
+
return ` ${QueryKeywords.PARAMETERS} ${parameters}`;
|
|
217
|
+
}
|
|
218
|
+
export function completeOrderly(orderly, entity) {
|
|
219
|
+
if (!orderly.length) {
|
|
220
|
+
throw new Error(QueryError.INVALID_ORDERLY);
|
|
221
|
+
}
|
|
222
|
+
else if (new RegExp(/^[^.\s]+(\.[^.\s]+)+$/g).test(orderly)) {
|
|
223
|
+
// text containing full stops
|
|
224
|
+
return orderly;
|
|
225
|
+
}
|
|
226
|
+
else if (new RegExp(/^[^.\s]+$/g).test(orderly)) {
|
|
227
|
+
// text without a full stop (e.g. resource_name)
|
|
228
|
+
return `${entity}.${orderly}`;
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
throw new Error(QueryError.INVALID_ORDERLY);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
export function buildOrderClauseOld(orderBy, sortOrder, entity) {
|
|
235
|
+
if (typeof orderBy === "undefined") {
|
|
236
|
+
return ``;
|
|
237
|
+
}
|
|
238
|
+
else if (typeof sortOrder === "undefined") {
|
|
239
|
+
sortOrder = "DESC";
|
|
240
|
+
}
|
|
241
|
+
else if (sortOrder !== "ASC" && sortOrder !== "DESC") {
|
|
242
|
+
throw new Error(QueryError.INVALID_SORT_ORDER);
|
|
243
|
+
}
|
|
244
|
+
if (Array.isArray(orderBy)) {
|
|
245
|
+
const orders = orderBy
|
|
246
|
+
.map((orderly) => {
|
|
247
|
+
if (typeof orderly !== "string") {
|
|
248
|
+
throw new Error(QueryError.INVALID_ORDERLY);
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
return completeOrderly(orderly, entity);
|
|
252
|
+
}
|
|
253
|
+
})
|
|
254
|
+
.join(", ");
|
|
255
|
+
return ` ${QueryKeywords.ORDER_BY} ${orders} ${sortOrder}`;
|
|
256
|
+
}
|
|
257
|
+
else if (typeof orderBy === "string") {
|
|
258
|
+
return ` ${QueryKeywords.ORDER_BY} ${completeOrderly(orderBy, entity)} ${sortOrder}`;
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
throw new Error(QueryError.INVALID_ORDERBY);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
export function buildOrderClauseNew(order, entity) {
|
|
265
|
+
if (!order || !Array.isArray(order)) {
|
|
266
|
+
throw new Error(QueryError.INVALID_ORDER);
|
|
267
|
+
}
|
|
268
|
+
if (!order.length) {
|
|
269
|
+
return "";
|
|
270
|
+
}
|
|
271
|
+
const orders = order
|
|
272
|
+
.map((o) => {
|
|
273
|
+
const orderly = completeOrderly(o.field, entity);
|
|
274
|
+
const sortOrder = o.sort_order ? o.sort_order : "DESC";
|
|
275
|
+
return `${orderly} ${sortOrder}`;
|
|
276
|
+
})
|
|
277
|
+
.join(", ");
|
|
278
|
+
return ` ${QueryKeywords.ORDER_BY} ${orders}`;
|
|
279
|
+
}
|
|
280
|
+
export function buildOrderClause(order, orderBy, sortOrder, entity) {
|
|
281
|
+
if (order) {
|
|
282
|
+
return buildOrderClauseNew(order, entity);
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
return buildOrderClauseOld(orderBy, sortOrder, entity);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
export function buildRequestOptions(reportOptions) {
|
|
289
|
+
const { page_size, page_token, validate_only, search_settings } = reportOptions;
|
|
290
|
+
return { page_size, page_token, validate_only, search_settings };
|
|
291
|
+
}
|
|
292
|
+
export function buildQuery(reportOptions) {
|
|
293
|
+
const SELECT = buildSelectClause(reportOptions.attributes, reportOptions.metrics, reportOptions.segments);
|
|
294
|
+
const FROM = buildFromClause(reportOptions.entity);
|
|
295
|
+
const WHERE = buildWhereClause(reportOptions.constraints, reportOptions.date_constant, reportOptions.from_date, reportOptions.to_date);
|
|
296
|
+
const ORDER = buildOrderClause(reportOptions.order, reportOptions.order_by, reportOptions.sort_order, reportOptions.entity);
|
|
297
|
+
const LIMIT = buildLimitClause(reportOptions.limit);
|
|
298
|
+
const PARAMETERS = buildParametersClause(reportOptions.parameters);
|
|
299
|
+
const requestOptions = buildRequestOptions(reportOptions);
|
|
300
|
+
return {
|
|
301
|
+
gaqlQuery: `${SELECT}${FROM}${WHERE}${ORDER}${LIMIT}${PARAMETERS}`,
|
|
302
|
+
requestOptions,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { ClientOptions } from "./client.js";
|
|
2
|
+
import { AllServices, errors, GoogleAdsServiceClient, ServiceName, services } from "./protos/index.js";
|
|
3
|
+
import { CustomerOptions, CustomerCredentials, RequestOptions, MutateOperation, MutateOptions } from "./types.js";
|
|
4
|
+
import { Hooks } from "./hooks.js";
|
|
5
|
+
import TTLCache from "@isaacs/ttlcache";
|
|
6
|
+
export declare const FAILURE_KEY = "google.ads.googleads.v25.errors.googleadsfailure-bin";
|
|
7
|
+
export interface CallHeaders {
|
|
8
|
+
"developer-token": string;
|
|
9
|
+
"login-customer-id"?: string;
|
|
10
|
+
"linked-customer-id"?: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function disposeService(service: {
|
|
13
|
+
close: () => Promise<void>;
|
|
14
|
+
}): void;
|
|
15
|
+
export declare const serviceCache: TTLCache<unknown, {
|
|
16
|
+
close: () => Promise<void>;
|
|
17
|
+
}>;
|
|
18
|
+
export declare class Service {
|
|
19
|
+
protected readonly clientOptions: ClientOptions;
|
|
20
|
+
protected readonly customerOptions: CustomerOptions;
|
|
21
|
+
protected readonly hooks: Hooks;
|
|
22
|
+
constructor(clientOptions: ClientOptions, customerOptions: CustomerOptions, hooks?: Hooks);
|
|
23
|
+
get credentials(): CustomerCredentials;
|
|
24
|
+
protected get callHeaders(): CallHeaders;
|
|
25
|
+
private getCredentials;
|
|
26
|
+
getAccessToken(): Promise<string>;
|
|
27
|
+
protected loadService<T = AllServices>(service: ServiceName, options?: {
|
|
28
|
+
skipCache?: boolean;
|
|
29
|
+
}): T;
|
|
30
|
+
protected getGoogleAdsError(error: Error): errors.GoogleAdsFailure | Error;
|
|
31
|
+
private decodeGoogleAdsFailureBuffer;
|
|
32
|
+
protected decodePartialFailureError<T>(response: T): T;
|
|
33
|
+
protected buildSearchRequestAndService(gaql: string, options?: RequestOptions): {
|
|
34
|
+
service: GoogleAdsServiceClient;
|
|
35
|
+
request: services.SearchGoogleAdsRequest;
|
|
36
|
+
};
|
|
37
|
+
protected buildSearchStreamRequestAndService(gaql: string, options?: RequestOptions): {
|
|
38
|
+
service: GoogleAdsServiceClient;
|
|
39
|
+
request: services.SearchGoogleAdsStreamRequest;
|
|
40
|
+
};
|
|
41
|
+
protected buildMutationRequestAndService<T>(mutations: MutateOperation<T>[], options?: MutateOptions): {
|
|
42
|
+
service: GoogleAdsServiceClient;
|
|
43
|
+
request: services.MutateGoogleAdsRequest;
|
|
44
|
+
};
|
|
45
|
+
protected buildOperations<Op, Ent>(type: "create" | "update" | "remove", entities: Ent[], message?: Ent): Op[];
|
|
46
|
+
protected buildRequest<Op, Req, Options>(operations: Op[], options?: Options): Req;
|
|
47
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { grpc } from "google-gax";
|
|
2
|
+
import * as googleAdsNode from "google-ads-node";
|
|
3
|
+
import { UserRefreshClient, OAuth2Client } from "google-auth-library";
|
|
4
|
+
import { errors, services, } from "./protos/index.js";
|
|
5
|
+
import { getFieldMask, toSnakeCase } from "./utils.js";
|
|
6
|
+
import { googleAdsVersion } from "./version.js";
|
|
7
|
+
import TTLCache from "@isaacs/ttlcache";
|
|
8
|
+
// Make sure to update this version number when upgrading
|
|
9
|
+
export const FAILURE_KEY = `google.ads.googleads.${googleAdsVersion}.errors.googleadsfailure-bin`;
|
|
10
|
+
// Close connections when services are removed from the cache
|
|
11
|
+
export function disposeService(service) {
|
|
12
|
+
Promise.resolve()
|
|
13
|
+
.then(() => service.close())
|
|
14
|
+
.catch(() => null);
|
|
15
|
+
}
|
|
16
|
+
// A global service cache to avoid re-initialising services
|
|
17
|
+
export const serviceCache = new TTLCache({
|
|
18
|
+
max: 1_000,
|
|
19
|
+
ttl: 10 * 60 * 1000, // 10 minutes
|
|
20
|
+
checkAgeOnGet: true,
|
|
21
|
+
dispose: disposeService,
|
|
22
|
+
});
|
|
23
|
+
// A global access token cache used by REST calls. Issued tokens expire after 1 hour, so we cache them for 50 minutes.
|
|
24
|
+
const accessTokenCache = new TTLCache({
|
|
25
|
+
max: 100_000,
|
|
26
|
+
ttl: 50 * 60 * 1000, // 50 minutes
|
|
27
|
+
});
|
|
28
|
+
export class Service {
|
|
29
|
+
clientOptions;
|
|
30
|
+
customerOptions;
|
|
31
|
+
hooks;
|
|
32
|
+
constructor(clientOptions, customerOptions, hooks) {
|
|
33
|
+
this.clientOptions = clientOptions;
|
|
34
|
+
this.customerOptions = customerOptions;
|
|
35
|
+
this.hooks = hooks ?? {};
|
|
36
|
+
// @ts-expect-error All fields don't need to be set here
|
|
37
|
+
this.serviceCache = {};
|
|
38
|
+
}
|
|
39
|
+
get credentials() {
|
|
40
|
+
return {
|
|
41
|
+
customer_id: this.customerOptions.customer_id,
|
|
42
|
+
login_customer_id: this.customerOptions.login_customer_id,
|
|
43
|
+
linked_customer_id: this.customerOptions.linked_customer_id,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
get callHeaders() {
|
|
47
|
+
const headers = {
|
|
48
|
+
"developer-token": this.clientOptions.developer_token,
|
|
49
|
+
};
|
|
50
|
+
if (this.customerOptions.login_customer_id) {
|
|
51
|
+
headers["login-customer-id"] = this.customerOptions.login_customer_id;
|
|
52
|
+
}
|
|
53
|
+
if (this.customerOptions.linked_customer_id) {
|
|
54
|
+
headers["linked-customer-id"] = this.customerOptions.linked_customer_id;
|
|
55
|
+
}
|
|
56
|
+
return headers;
|
|
57
|
+
}
|
|
58
|
+
// Used only by gRPC calls
|
|
59
|
+
getCredentials() {
|
|
60
|
+
const sslCreds = grpc.credentials.createSsl();
|
|
61
|
+
const authClient = new UserRefreshClient({
|
|
62
|
+
clientId: this.clientOptions.client_id,
|
|
63
|
+
clientSecret: this.clientOptions.client_secret,
|
|
64
|
+
refreshToken: this.customerOptions.refresh_token,
|
|
65
|
+
});
|
|
66
|
+
const credentials = grpc.credentials.combineChannelCredentials(sslCreds, grpc.credentials.createFromGoogleCredential({
|
|
67
|
+
getRequestHeaders: async (url) => Object.fromEntries(await authClient.getRequestHeaders(url)),
|
|
68
|
+
}));
|
|
69
|
+
return credentials;
|
|
70
|
+
}
|
|
71
|
+
// Used only by REST calls
|
|
72
|
+
async getAccessToken() {
|
|
73
|
+
const cachedToken = accessTokenCache.get(this.customerOptions.refresh_token);
|
|
74
|
+
if (cachedToken) {
|
|
75
|
+
return cachedToken;
|
|
76
|
+
}
|
|
77
|
+
const oAuth2Client = new OAuth2Client({
|
|
78
|
+
clientId: this.clientOptions.client_id,
|
|
79
|
+
clientSecret: this.clientOptions.client_secret,
|
|
80
|
+
});
|
|
81
|
+
oAuth2Client.setCredentials({
|
|
82
|
+
refresh_token: this.customerOptions.refresh_token,
|
|
83
|
+
});
|
|
84
|
+
const { token } = await oAuth2Client.getAccessToken();
|
|
85
|
+
if (typeof token !== "string") {
|
|
86
|
+
throw new Error("Failed to retrieve access token");
|
|
87
|
+
}
|
|
88
|
+
accessTokenCache.set(this.customerOptions.refresh_token, token);
|
|
89
|
+
return token;
|
|
90
|
+
}
|
|
91
|
+
loadService(service, options) {
|
|
92
|
+
const serviceCacheKey = `${service}_${this.clientOptions.client_id}_${this.customerOptions.refresh_token}`;
|
|
93
|
+
if (!options?.skipCache) {
|
|
94
|
+
const cachedService = serviceCache.get(serviceCacheKey);
|
|
95
|
+
if (cachedService) {
|
|
96
|
+
return cachedService;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const protoService = googleAdsNode[service];
|
|
100
|
+
if (typeof protoService === "undefined") {
|
|
101
|
+
throw new Error(`Service "${String(service)}" could not be found`);
|
|
102
|
+
}
|
|
103
|
+
// Initialising services can take a few ms, so we cache when possible.
|
|
104
|
+
const client = new protoService({
|
|
105
|
+
sslCreds: this.getCredentials(),
|
|
106
|
+
universeDomain: "googleapis.com",
|
|
107
|
+
...this.clientOptions.grpc_channel_options,
|
|
108
|
+
});
|
|
109
|
+
if (!options?.skipCache) {
|
|
110
|
+
serviceCache.set(serviceCacheKey, client);
|
|
111
|
+
}
|
|
112
|
+
return client;
|
|
113
|
+
}
|
|
114
|
+
getGoogleAdsError(error) {
|
|
115
|
+
const trailer = error?.metadata?.internalRepr?.get?.(FAILURE_KEY);
|
|
116
|
+
if (typeof trailer?.[0] === "undefined") {
|
|
117
|
+
return error;
|
|
118
|
+
}
|
|
119
|
+
return this.decodeGoogleAdsFailureBuffer(trailer[0]);
|
|
120
|
+
}
|
|
121
|
+
decodeGoogleAdsFailureBuffer(buffer) {
|
|
122
|
+
const googleAdsFailure = errors.GoogleAdsFailure.decode(buffer);
|
|
123
|
+
return googleAdsFailure;
|
|
124
|
+
}
|
|
125
|
+
decodePartialFailureError(response) {
|
|
126
|
+
if (typeof response?.partial_failure_error === "undefined" ||
|
|
127
|
+
!response?.partial_failure_error) {
|
|
128
|
+
return response;
|
|
129
|
+
}
|
|
130
|
+
const { details } = response.partial_failure_error;
|
|
131
|
+
const buffer = details?.find((d) => d.type_url.includes("errors.GoogleAdsFailure"))?.value;
|
|
132
|
+
if (typeof buffer === "undefined") {
|
|
133
|
+
return response;
|
|
134
|
+
}
|
|
135
|
+
// Update the partial failure field with the decoded error details
|
|
136
|
+
return {
|
|
137
|
+
...response,
|
|
138
|
+
partial_failure_error: this.decodeGoogleAdsFailureBuffer(buffer),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
buildSearchRequestAndService(gaql, options) {
|
|
142
|
+
const service = this.loadService("GoogleAdsServiceClient");
|
|
143
|
+
const request = new services.SearchGoogleAdsRequest({
|
|
144
|
+
customer_id: this.customerOptions.customer_id,
|
|
145
|
+
query: gaql,
|
|
146
|
+
...options,
|
|
147
|
+
});
|
|
148
|
+
return { service, request };
|
|
149
|
+
}
|
|
150
|
+
buildSearchStreamRequestAndService(gaql, options) {
|
|
151
|
+
const service = this.loadService("GoogleAdsServiceClient");
|
|
152
|
+
const request = new services.SearchGoogleAdsStreamRequest({
|
|
153
|
+
customer_id: this.customerOptions.customer_id,
|
|
154
|
+
query: gaql,
|
|
155
|
+
...options,
|
|
156
|
+
});
|
|
157
|
+
return { service, request };
|
|
158
|
+
}
|
|
159
|
+
buildMutationRequestAndService(mutations, options) {
|
|
160
|
+
const service = this.loadService("GoogleAdsServiceClient");
|
|
161
|
+
const mutateOperations = mutations.map((mutation) => {
|
|
162
|
+
const opKey = toSnakeCase(`${mutation.entity}Operation`);
|
|
163
|
+
const operation = {
|
|
164
|
+
[mutation.operation ?? "create"]: mutation.resource,
|
|
165
|
+
};
|
|
166
|
+
if (mutation.operation === "create" &&
|
|
167
|
+
//@ts-ignore
|
|
168
|
+
mutation?.exempt_policy_violation_keys?.length) {
|
|
169
|
+
//@ts-ignore
|
|
170
|
+
operation.exempt_policy_violation_keys =
|
|
171
|
+
mutation.exempt_policy_violation_keys;
|
|
172
|
+
}
|
|
173
|
+
else if (mutation.operation === "update") {
|
|
174
|
+
// @ts-expect-error Resource operations should have updateMask defined
|
|
175
|
+
operation.update_mask = getFieldMask(mutation.resource);
|
|
176
|
+
}
|
|
177
|
+
const mutateOperation = new services.MutateOperation({
|
|
178
|
+
[opKey]: operation,
|
|
179
|
+
});
|
|
180
|
+
return mutateOperation;
|
|
181
|
+
});
|
|
182
|
+
const request = new services.MutateGoogleAdsRequest({
|
|
183
|
+
customer_id: this.customerOptions.customer_id,
|
|
184
|
+
mutate_operations: mutateOperations,
|
|
185
|
+
...options,
|
|
186
|
+
});
|
|
187
|
+
return { service, request };
|
|
188
|
+
}
|
|
189
|
+
buildOperations(type, entities, message) {
|
|
190
|
+
const ops = entities.map((e) => {
|
|
191
|
+
const op = {
|
|
192
|
+
[type]: e,
|
|
193
|
+
operation: type,
|
|
194
|
+
};
|
|
195
|
+
//@ts-ignore
|
|
196
|
+
if (type === "create" && e?.exempt_policy_violation_keys?.length) {
|
|
197
|
+
// @ts-expect-error Field required for policy violation exemptions
|
|
198
|
+
op.exempt_policy_violation_keys = e.exempt_policy_violation_keys;
|
|
199
|
+
//@ts-ignore
|
|
200
|
+
delete e.exempt_policy_violation_keys;
|
|
201
|
+
}
|
|
202
|
+
else if (type === "update") {
|
|
203
|
+
// @ts-expect-error Field required for updates
|
|
204
|
+
op.update_mask = getFieldMask(
|
|
205
|
+
// @ts-expect-error Message types have a toObject method
|
|
206
|
+
message.toObject(e, {
|
|
207
|
+
defaults: false,
|
|
208
|
+
}));
|
|
209
|
+
}
|
|
210
|
+
return op;
|
|
211
|
+
});
|
|
212
|
+
return ops;
|
|
213
|
+
}
|
|
214
|
+
buildRequest(operations, options) {
|
|
215
|
+
const request = {
|
|
216
|
+
customer_id: this.customerOptions.customer_id,
|
|
217
|
+
operations,
|
|
218
|
+
...options,
|
|
219
|
+
};
|
|
220
|
+
return request;
|
|
221
|
+
}
|
|
222
|
+
}
|