@channel47/google-ads-mcp 1.0.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.
@@ -0,0 +1,417 @@
1
+ import { validateQuery, logValidation } from './query-validator.js';
2
+
3
+ /**
4
+ * GAQL query templates with placeholder substitution
5
+ */
6
+ export const TEMPLATES = {
7
+ // List accounts query
8
+ LIST_ACCOUNTS: `
9
+ SELECT
10
+ customer_client.id,
11
+ customer_client.descriptive_name,
12
+ customer_client.manager,
13
+ customer_client.currency_code,
14
+ customer_client.time_zone,
15
+ customer_client.status
16
+ FROM customer_client
17
+ {{FILTERS}}
18
+ `,
19
+
20
+ // Campaign performance
21
+ CAMPAIGN_PERFORMANCE: `
22
+ SELECT
23
+ campaign.id,
24
+ campaign.name,
25
+ campaign.status,
26
+ campaign.advertising_channel_type,
27
+ {{METRICS}}
28
+ FROM campaign
29
+ WHERE segments.date BETWEEN '{{START_DATE}}' AND '{{END_DATE}}'
30
+ {{FILTERS}}
31
+ ORDER BY {{ORDER_BY}} DESC
32
+ LIMIT {{LIMIT}}
33
+ `,
34
+
35
+ // Ad group performance
36
+ AD_GROUP_PERFORMANCE: `
37
+ SELECT
38
+ ad_group.id,
39
+ ad_group.name,
40
+ ad_group.status,
41
+ campaign.id,
42
+ campaign.name,
43
+ {{METRICS}}
44
+ FROM ad_group
45
+ WHERE segments.date BETWEEN '{{START_DATE}}' AND '{{END_DATE}}'
46
+ {{FILTERS}}
47
+ ORDER BY {{ORDER_BY}} DESC
48
+ LIMIT {{LIMIT}}
49
+ `,
50
+
51
+ // Keyword performance
52
+ KEYWORD_PERFORMANCE: `
53
+ SELECT
54
+ ad_group_criterion.criterion_id,
55
+ ad_group_criterion.keyword.text,
56
+ ad_group_criterion.keyword.match_type,
57
+ ad_group_criterion.status,
58
+ ad_group.id,
59
+ ad_group.name,
60
+ campaign.id,
61
+ campaign.name,
62
+ {{METRICS}}
63
+ FROM keyword_view
64
+ WHERE segments.date BETWEEN '{{START_DATE}}' AND '{{END_DATE}}'
65
+ {{FILTERS}}
66
+ ORDER BY {{ORDER_BY}} DESC
67
+ LIMIT {{LIMIT}}
68
+ `,
69
+
70
+ // Product group performance
71
+ PRODUCT_GROUP_PERFORMANCE: `
72
+ SELECT
73
+ shopping_performance_view.product_item_id,
74
+ shopping_performance_view.product_title,
75
+ shopping_performance_view.product_brand,
76
+ campaign.id,
77
+ campaign.name,
78
+ {{METRICS}}
79
+ FROM shopping_performance_view
80
+ WHERE segments.date BETWEEN '{{START_DATE}}' AND '{{END_DATE}}'
81
+ {{FILTERS}}
82
+ ORDER BY {{ORDER_BY}} DESC
83
+ LIMIT {{LIMIT}}
84
+ `,
85
+
86
+ // Search terms report
87
+ SEARCH_TERMS: `
88
+ SELECT
89
+ search_term_view.search_term,
90
+ search_term_view.status,
91
+ ad_group.id,
92
+ ad_group.name,
93
+ campaign.id,
94
+ campaign.name,
95
+ {{METRICS}}
96
+ FROM search_term_view
97
+ WHERE segments.date BETWEEN '{{START_DATE}}' AND '{{END_DATE}}'
98
+ AND metrics.impressions >= {{MIN_IMPRESSIONS}}
99
+ {{FILTERS}}
100
+ ORDER BY {{ORDER_BY}} DESC
101
+ LIMIT {{LIMIT}}
102
+ `,
103
+
104
+ // Wasted spend - search terms
105
+ WASTED_SPEND_SEARCH_TERMS: `
106
+ SELECT
107
+ search_term_view.search_term,
108
+ ad_group.id,
109
+ ad_group.name,
110
+ campaign.id,
111
+ campaign.name,
112
+ metrics.cost_micros,
113
+ metrics.conversions,
114
+ metrics.clicks,
115
+ metrics.impressions
116
+ FROM search_term_view
117
+ WHERE segments.date BETWEEN '{{START_DATE}}' AND '{{END_DATE}}'
118
+ AND metrics.cost_micros >= {{MIN_COST_MICROS}}
119
+ AND metrics.conversions = {{MAX_CONVERSIONS}}
120
+ {{FILTERS}}
121
+ ORDER BY metrics.cost_micros DESC
122
+ LIMIT {{LIMIT}}
123
+ `,
124
+
125
+ // Wasted spend - keywords
126
+ WASTED_SPEND_KEYWORDS: `
127
+ SELECT
128
+ ad_group_criterion.criterion_id,
129
+ ad_group_criterion.keyword.text,
130
+ ad_group_criterion.keyword.match_type,
131
+ ad_group.id,
132
+ ad_group.name,
133
+ campaign.id,
134
+ campaign.name,
135
+ metrics.cost_micros,
136
+ metrics.conversions,
137
+ metrics.clicks,
138
+ metrics.impressions
139
+ FROM keyword_view
140
+ WHERE segments.date BETWEEN '{{START_DATE}}' AND '{{END_DATE}}'
141
+ AND metrics.cost_micros >= {{MIN_COST_MICROS}}
142
+ AND metrics.conversions = {{MAX_CONVERSIONS}}
143
+ {{FILTERS}}
144
+ ORDER BY metrics.cost_micros DESC
145
+ LIMIT {{LIMIT}}
146
+ `,
147
+
148
+ // Account average CPA (for smart threshold)
149
+ ACCOUNT_CPA: `
150
+ SELECT
151
+ metrics.cost_per_conversion
152
+ FROM customer
153
+ WHERE segments.date DURING LAST_30_DAYS
154
+ `,
155
+
156
+ // ============================================
157
+ // PHASE 2: Quality & Budget Analysis Templates
158
+ // ============================================
159
+
160
+ // Quality Score Analysis
161
+ QUALITY_SCORE_ANALYSIS: `
162
+ SELECT
163
+ ad_group_criterion.criterion_id,
164
+ ad_group_criterion.keyword.text,
165
+ ad_group_criterion.keyword.match_type,
166
+ ad_group_criterion.status,
167
+ ad_group_criterion.quality_info.quality_score,
168
+ ad_group_criterion.quality_info.creative_quality_score,
169
+ ad_group_criterion.quality_info.post_click_quality_score,
170
+ ad_group_criterion.quality_info.search_predicted_ctr,
171
+ ad_group.id,
172
+ ad_group.name,
173
+ campaign.id,
174
+ campaign.name,
175
+ metrics.impressions,
176
+ metrics.clicks,
177
+ metrics.cost_micros,
178
+ metrics.conversions
179
+ FROM keyword_view
180
+ WHERE segments.date BETWEEN '{{START_DATE}}' AND '{{END_DATE}}'
181
+ AND ad_group_criterion.status = 'ENABLED'
182
+ AND metrics.impressions >= {{MIN_IMPRESSIONS}}
183
+ {{FILTERS}}
184
+ ORDER BY metrics.cost_micros DESC
185
+ LIMIT {{LIMIT}}
186
+ `,
187
+
188
+ // Budget Pacing - Campaign Budgets
189
+ CAMPAIGN_BUDGET: `
190
+ SELECT
191
+ campaign.id,
192
+ campaign.name,
193
+ campaign.status,
194
+ campaign.advertising_channel_type,
195
+ campaign_budget.id,
196
+ campaign_budget.name,
197
+ campaign_budget.amount_micros,
198
+ campaign_budget.period,
199
+ campaign_budget.delivery_method,
200
+ campaign_budget.explicitly_shared,
201
+ campaign_budget.has_recommended_budget,
202
+ campaign_budget.recommended_budget_amount_micros,
203
+ metrics.cost_micros,
204
+ metrics.impressions,
205
+ metrics.clicks,
206
+ metrics.conversions
207
+ FROM campaign
208
+ WHERE campaign.status = 'ENABLED'
209
+ AND segments.date BETWEEN '{{START_DATE}}' AND '{{END_DATE}}'
210
+ {{FILTERS}}
211
+ `,
212
+
213
+ // Budget Pacing - Yesterday's spend for daily rate
214
+ CAMPAIGN_DAILY_SPEND: `
215
+ SELECT
216
+ campaign.id,
217
+ campaign.name,
218
+ metrics.cost_micros
219
+ FROM campaign
220
+ WHERE campaign.status = 'ENABLED'
221
+ AND segments.date = '{{DATE}}'
222
+ {{FILTERS}}
223
+ `,
224
+
225
+ // ============================================
226
+ // PHASE 3: Shopping & Segmentation Templates
227
+ // ============================================
228
+
229
+ // Shopping Product Status
230
+ SHOPPING_PRODUCT_STATUS: `
231
+ SELECT
232
+ shopping_product.resource_name,
233
+ shopping_product.merchant_center_id,
234
+ shopping_product.channel,
235
+ shopping_product.language_code,
236
+ shopping_product.feed_label,
237
+ shopping_product.item_id,
238
+ shopping_product.title,
239
+ shopping_product.brand,
240
+ shopping_product.price_micros,
241
+ shopping_product.currency_code,
242
+ shopping_product.status,
243
+ shopping_product.issues
244
+ FROM shopping_product
245
+ WHERE 1=1
246
+ {{FILTERS}}
247
+ LIMIT {{LIMIT}}
248
+ `,
249
+
250
+ // Shopping Performance by Segment
251
+ SHOPPING_PERFORMANCE: `
252
+ SELECT
253
+ {{SEGMENT_FIELD}},
254
+ campaign.id,
255
+ campaign.name,
256
+ campaign.advertising_channel_type,
257
+ {{METRICS}}
258
+ FROM shopping_performance_view
259
+ WHERE segments.date BETWEEN '{{START_DATE}}' AND '{{END_DATE}}'
260
+ AND metrics.impressions >= {{MIN_IMPRESSIONS}}
261
+ {{FILTERS}}
262
+ ORDER BY {{ORDER_BY}} DESC
263
+ LIMIT {{LIMIT}}
264
+ `,
265
+
266
+ // Performance by Hour of Day
267
+ PERFORMANCE_BY_HOUR: `
268
+ SELECT
269
+ segments.hour,
270
+ {{METRICS}}
271
+ FROM campaign
272
+ WHERE segments.date BETWEEN '{{START_DATE}}' AND '{{END_DATE}}'
273
+ {{FILTERS}}
274
+ `,
275
+
276
+ // Performance by Day of Week
277
+ PERFORMANCE_BY_DAY_OF_WEEK: `
278
+ SELECT
279
+ segments.day_of_week,
280
+ {{METRICS}}
281
+ FROM campaign
282
+ WHERE segments.date BETWEEN '{{START_DATE}}' AND '{{END_DATE}}'
283
+ {{FILTERS}}
284
+ `,
285
+
286
+ // Performance by Device
287
+ PERFORMANCE_BY_DEVICE: `
288
+ SELECT
289
+ segments.device,
290
+ {{METRICS}}
291
+ FROM campaign
292
+ WHERE segments.date BETWEEN '{{START_DATE}}' AND '{{END_DATE}}'
293
+ {{FILTERS}}
294
+ `,
295
+
296
+ // Performance by Geographic Location
297
+ PERFORMANCE_BY_GEO: `
298
+ SELECT
299
+ geographic_view.country_criterion_id,
300
+ geographic_view.location_type,
301
+ {{METRICS}}
302
+ FROM geographic_view
303
+ WHERE segments.date BETWEEN '{{START_DATE}}' AND '{{END_DATE}}'
304
+ {{FILTERS}}
305
+ ORDER BY {{ORDER_BY}} DESC
306
+ LIMIT {{LIMIT}}
307
+ `,
308
+
309
+ // Performance by Audience
310
+ PERFORMANCE_BY_AUDIENCE: `
311
+ SELECT
312
+ user_list.id,
313
+ user_list.name,
314
+ user_list.type,
315
+ {{METRICS}}
316
+ FROM user_list
317
+ WHERE segments.date BETWEEN '{{START_DATE}}' AND '{{END_DATE}}'
318
+ {{FILTERS}}
319
+ ORDER BY {{ORDER_BY}} DESC
320
+ LIMIT {{LIMIT}}
321
+ `
322
+ };
323
+
324
+ /**
325
+ * Build GAQL query from template with parameter substitution
326
+ * @param {string} template - Template string with placeholders
327
+ * @param {Object} params - Substitution parameters
328
+ * @returns {string} Complete GAQL query
329
+ */
330
+ export function buildQuery(template, params = {}) {
331
+ let query = template;
332
+
333
+ // Replace metrics (array → comma-separated string)
334
+ if (params.metrics) {
335
+ const metricsStr = params.metrics.map(m => {
336
+ // Add metrics. prefix if not present
337
+ return m.startsWith('metrics.') ? m : `metrics.${m}`;
338
+ }).join(', ');
339
+ query = query.replace('{{METRICS}}', metricsStr);
340
+ }
341
+
342
+ // Replace dates
343
+ if (params.dates) {
344
+ query = query.replace('{{START_DATE}}', params.dates.start);
345
+ query = query.replace('{{END_DATE}}', params.dates.end);
346
+ }
347
+
348
+ // Replace filters
349
+ const filters = params.filters || '';
350
+ query = query.replace('{{FILTERS}}', filters);
351
+
352
+ // Replace order by
353
+ const orderBy = params.orderBy || 'metrics.cost_micros';
354
+ query = query.replace('{{ORDER_BY}}', orderBy);
355
+
356
+ // Replace limit
357
+ const limit = params.limit || 50;
358
+ query = query.replace('{{LIMIT}}', limit);
359
+
360
+ // Replace min impressions
361
+ const minImpressions = params.minImpressions || 10;
362
+ query = query.replace('{{MIN_IMPRESSIONS}}', minImpressions);
363
+
364
+ // Replace min cost (convert dollars to micros)
365
+ if (params.minCost !== undefined) {
366
+ const minCostMicros = Math.round(params.minCost * 1000000);
367
+ query = query.replace('{{MIN_COST_MICROS}}', minCostMicros);
368
+ }
369
+
370
+ // Replace max conversions
371
+ const maxConversions = params.maxConversions !== undefined ? params.maxConversions : 0;
372
+ query = query.replace('{{MAX_CONVERSIONS}}', maxConversions);
373
+
374
+ // Replace resource ID (for auction insights)
375
+ if (params.resourceId !== undefined) {
376
+ query = query.replace('{{RESOURCE_ID}}', params.resourceId);
377
+ }
378
+
379
+ // Replace single date (for daily spend queries)
380
+ if (params.date) {
381
+ query = query.replace('{{DATE}}', params.date);
382
+ }
383
+
384
+ // Replace max quality score filter
385
+ if (params.maxQualityScore !== undefined) {
386
+ query = query.replace('{{MAX_QUALITY_SCORE}}', params.maxQualityScore);
387
+ }
388
+
389
+ // Replace segment field (for shopping performance)
390
+ if (params.segmentField) {
391
+ query = query.replace('{{SEGMENT_FIELD}}', params.segmentField);
392
+ }
393
+
394
+ // Replace status filter (for shopping product status)
395
+ if (params.statusFilter) {
396
+ query = query.replace('{{STATUS_FILTER}}', `= '${params.statusFilter}'`);
397
+ } else {
398
+ // If no status filter, match all statuses
399
+ query = query.replace('{{STATUS_FILTER}}', 'IS NOT NULL');
400
+ }
401
+
402
+ // Replace geo level (for geographic queries)
403
+ if (params.geoLevel) {
404
+ query = query.replace('{{GEO_LEVEL}}', params.geoLevel);
405
+ }
406
+
407
+ // Clean up whitespace
408
+ const finalQuery = query.trim().replace(/\s+/g, ' ');
409
+
410
+ // Validate the query (logs warnings/errors if any)
411
+ const validation = validateQuery(finalQuery);
412
+ if (!validation.valid) {
413
+ logValidation(finalQuery, validation);
414
+ }
415
+
416
+ return finalQuery;
417
+ }