adapty 0.3.0 → 0.4.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 +134 -5
- package/dist/commands/asa/ad-groups/create.d.ts +22 -0
- package/dist/commands/asa/ad-groups/create.js +54 -0
- package/dist/commands/asa/ad-groups/get.d.ts +11 -0
- package/dist/commands/asa/ad-groups/get.js +21 -0
- package/dist/commands/asa/ad-groups/list.d.ts +18 -0
- package/dist/commands/asa/ad-groups/list.js +24 -0
- package/dist/commands/asa/ad-groups/update.d.ts +23 -0
- package/dist/commands/asa/ad-groups/update.js +62 -0
- package/dist/commands/asa/ads/create.d.ts +16 -0
- package/dist/commands/asa/ads/create.js +41 -0
- package/dist/commands/asa/ads/get.d.ts +11 -0
- package/dist/commands/asa/ads/get.js +21 -0
- package/dist/commands/asa/ads/list.d.ts +18 -0
- package/dist/commands/asa/ads/list.js +24 -0
- package/dist/commands/asa/ads/update.d.ts +17 -0
- package/dist/commands/asa/ads/update.js +44 -0
- package/dist/commands/asa/apps/list.d.ts +13 -0
- package/dist/commands/asa/apps/list.js +18 -0
- package/dist/commands/asa/automations/create.d.ts +16 -0
- package/dist/commands/asa/automations/create.js +60 -0
- package/dist/commands/asa/automations/get.d.ts +11 -0
- package/dist/commands/asa/automations/get.js +21 -0
- package/dist/commands/asa/automations/list.d.ts +13 -0
- package/dist/commands/asa/automations/list.js +18 -0
- package/dist/commands/asa/automations/run.d.ts +16 -0
- package/dist/commands/asa/automations/run.js +41 -0
- package/dist/commands/asa/automations/runs.d.ts +16 -0
- package/dist/commands/asa/automations/runs.js +24 -0
- package/dist/commands/asa/automations/update.d.ts +21 -0
- package/dist/commands/asa/automations/update.js +73 -0
- package/dist/commands/asa/campaigns/create.d.ts +25 -0
- package/dist/commands/asa/campaigns/create.js +67 -0
- package/dist/commands/asa/campaigns/get.d.ts +11 -0
- package/dist/commands/asa/campaigns/get.js +21 -0
- package/dist/commands/asa/campaigns/list.d.ts +17 -0
- package/dist/commands/asa/campaigns/list.js +24 -0
- package/dist/commands/asa/campaigns/update.d.ts +23 -0
- package/dist/commands/asa/campaigns/update.js +66 -0
- package/dist/commands/asa/competitors/summary.d.ts +11 -0
- package/dist/commands/asa/competitors/summary.js +56 -0
- package/dist/commands/asa/connect.d.ts +12 -0
- package/dist/commands/asa/connect.js +39 -0
- package/dist/commands/asa/creatives/list.d.ts +15 -0
- package/dist/commands/asa/creatives/list.js +24 -0
- package/dist/commands/asa/keywords/add.d.ts +20 -0
- package/dist/commands/asa/keywords/add.js +73 -0
- package/dist/commands/asa/keywords/list.d.ts +19 -0
- package/dist/commands/asa/keywords/list.js +24 -0
- package/dist/commands/asa/keywords/update.d.ts +21 -0
- package/dist/commands/asa/keywords/update.js +64 -0
- package/dist/commands/asa/metrics/index.d.ts +21 -0
- package/dist/commands/asa/metrics/index.js +68 -0
- package/dist/commands/asa/metrics/overview.d.ts +15 -0
- package/dist/commands/asa/metrics/overview.js +51 -0
- package/dist/commands/asa/negative-keywords/add.d.ts +18 -0
- package/dist/commands/asa/negative-keywords/add.js +70 -0
- package/dist/commands/asa/negative-keywords/list.d.ts +19 -0
- package/dist/commands/asa/negative-keywords/list.js +29 -0
- package/dist/commands/asa/orgs/list.d.ts +13 -0
- package/dist/commands/asa/orgs/list.js +18 -0
- package/dist/commands/asa/product-pages/list.d.ts +15 -0
- package/dist/commands/asa/product-pages/list.js +24 -0
- package/dist/commands/asa/product-pages/sync.d.ts +13 -0
- package/dist/commands/asa/product-pages/sync.js +28 -0
- package/dist/commands/asa/search-terms/list.d.ts +20 -0
- package/dist/commands/asa/search-terms/list.js +25 -0
- package/dist/commands/asa/whoami.d.ts +8 -0
- package/dist/commands/asa/whoami.js +22 -0
- package/dist/lib/api-client.d.ts +15 -3
- package/dist/lib/api-client.js +49 -17
- package/dist/lib/asa-client.d.ts +16 -0
- package/dist/lib/asa-client.js +46 -0
- package/dist/lib/asa-confirm.d.ts +18 -0
- package/dist/lib/asa-confirm.js +41 -0
- package/dist/lib/asa-flags.d.ts +88 -0
- package/dist/lib/asa-flags.js +137 -0
- package/dist/lib/asa-schemas.d.ts +287 -0
- package/dist/lib/asa-schemas.js +1 -0
- package/dist/lib/errors.d.ts +25 -2
- package/dist/lib/errors.js +48 -7
- package/dist/lib/output.js +45 -15
- package/oclif.manifest.json +2802 -157
- package/package.json +40 -1
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Command, Flags } from '@oclif/core';
|
|
2
|
+
import { asaWrite, createAsaClient } from '../../../lib/asa-client.js';
|
|
3
|
+
import { printList, printResponse } from '../../../lib/output.js';
|
|
4
|
+
const MAX_APP_IDS = 5;
|
|
5
|
+
export default class AsaCompetitorsSummary extends Command {
|
|
6
|
+
static description = 'Competitor summary for up to five App Store apps; the server covers the last full month and every country';
|
|
7
|
+
static enableJsonFlag = true;
|
|
8
|
+
static examples = ['<%= config.bin %> asa competitors summary --app-ids 1668337467,6503873027'];
|
|
9
|
+
static flags = {
|
|
10
|
+
'app-ids': Flags.string({
|
|
11
|
+
description: `Apple App Store IDs (adam_id), comma-separated, 1-${MAX_APP_IDS} values`,
|
|
12
|
+
required: true,
|
|
13
|
+
}),
|
|
14
|
+
};
|
|
15
|
+
async run() {
|
|
16
|
+
const { flags } = await this.parse(AsaCompetitorsSummary);
|
|
17
|
+
const appIds = flags['app-ids']
|
|
18
|
+
.split(',')
|
|
19
|
+
.map((part) => part.trim())
|
|
20
|
+
.filter(Boolean);
|
|
21
|
+
if (appIds.length === 0 || appIds.length > MAX_APP_IDS) {
|
|
22
|
+
this.error(`Pass 1 to ${MAX_APP_IDS} App Store IDs, got ${appIds.length}.`, { exit: 2 });
|
|
23
|
+
}
|
|
24
|
+
if (appIds.some((id) => !/^\d+$/.test(id))) {
|
|
25
|
+
this.error('App Store IDs are numbers, e.g. --app-ids 1668337467,6503873027.', { exit: 2 });
|
|
26
|
+
}
|
|
27
|
+
const client = await createAsaClient(this.config);
|
|
28
|
+
const { result } = await asaWrite(client, 'post', '/competitors/summary', {
|
|
29
|
+
body: { app_ids: appIds.map(Number) },
|
|
30
|
+
});
|
|
31
|
+
const { total } = result;
|
|
32
|
+
printResponse({
|
|
33
|
+
competitors_count: total.competitorsCount,
|
|
34
|
+
countries_asa_count: total.countriesAsaCount,
|
|
35
|
+
countries_with_asa_terms: total.countriesWithAsaTerms,
|
|
36
|
+
total_unique_terms: total.totalUniqueTerms,
|
|
37
|
+
}, this.log.bind(this));
|
|
38
|
+
this.log('');
|
|
39
|
+
this.log('Top apps by performance:');
|
|
40
|
+
printList(total.topAppsByPerformance.map((app) => ({
|
|
41
|
+
adam_id: app.adamId,
|
|
42
|
+
avg_sov: app.avgSov,
|
|
43
|
+
countries: app.countries,
|
|
44
|
+
name: app.name,
|
|
45
|
+
terms_count: app.termsCount,
|
|
46
|
+
})), this.log.bind(this));
|
|
47
|
+
this.log('');
|
|
48
|
+
this.log('Most contested terms:');
|
|
49
|
+
printList(total.mostContestedTerms.map((term) => ({
|
|
50
|
+
competitor_count: term.competitorCount,
|
|
51
|
+
max_sov: term.maxSov,
|
|
52
|
+
term: term.term,
|
|
53
|
+
})), this.log.bind(this));
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
import type { AsaAppleOAuthDTO, AsaMeDTO } from '../../lib/asa-schemas.js';
|
|
3
|
+
export default class AsaConnect extends Command {
|
|
4
|
+
static description: string;
|
|
5
|
+
static enableJsonFlag: boolean;
|
|
6
|
+
static examples: string[];
|
|
7
|
+
static flags: {
|
|
8
|
+
timeout: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
wait: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
|
+
};
|
|
11
|
+
run(): Promise<AsaAppleOAuthDTO | AsaMeDTO>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Command, Flags } from '@oclif/core';
|
|
2
|
+
import open from 'open';
|
|
3
|
+
import { createAsaClient } from '../../lib/asa-client.js';
|
|
4
|
+
const POLL_INTERVAL_MS = 3000;
|
|
5
|
+
export default class AsaConnect extends Command {
|
|
6
|
+
static description = 'Connect an Apple Search Ads account to this company';
|
|
7
|
+
static enableJsonFlag = true;
|
|
8
|
+
static examples = ['<%= config.bin %> asa connect', '<%= config.bin %> asa connect --no-wait'];
|
|
9
|
+
static flags = {
|
|
10
|
+
timeout: Flags.integer({ default: 300, description: 'Seconds to wait for the browser step' }),
|
|
11
|
+
wait: Flags.boolean({ allowNo: true, default: true, description: 'Wait until Apple Ads reports as connected' }),
|
|
12
|
+
};
|
|
13
|
+
async run() {
|
|
14
|
+
const { flags } = await this.parse(AsaConnect);
|
|
15
|
+
const client = await createAsaClient(this.config);
|
|
16
|
+
const { auth_url: authUrl } = await client.get('/apple/oauth');
|
|
17
|
+
this.log(`If the browser doesn't open, visit: ${authUrl}\n`);
|
|
18
|
+
this.log('The link is valid for one hour. Sign in to the Adapty dashboard in that browser first — the last');
|
|
19
|
+
this.log('step is authorized by the dashboard session, not by this CLI.');
|
|
20
|
+
await open(authUrl).catch(() => false);
|
|
21
|
+
if (!flags.wait)
|
|
22
|
+
return { auth_url: authUrl };
|
|
23
|
+
const deadline = Date.now() + flags.timeout * 1000;
|
|
24
|
+
let status;
|
|
25
|
+
while (Date.now() < deadline) {
|
|
26
|
+
status = await client.get('/me');
|
|
27
|
+
if (status.apple_credentials_status === 'active') {
|
|
28
|
+
this.log('Apple Ads connected. The first metadata import starts automatically.');
|
|
29
|
+
return status;
|
|
30
|
+
}
|
|
31
|
+
await new Promise((resolve) => {
|
|
32
|
+
setTimeout(resolve, POLL_INTERVAL_MS);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
this.log(`Still not connected after ${flags.timeout}s. The usual cause is a browser that is not signed in to`);
|
|
36
|
+
this.log(`the Adapty dashboard. Finish the browser step, then check with: ${this.config.bin} asa whoami`);
|
|
37
|
+
return status ?? { auth_url: authUrl };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
import type { AsaCreativeDTO } from '../../../lib/asa-schemas.js';
|
|
3
|
+
import { type PaginatedResponse } from '../../../lib/flags.js';
|
|
4
|
+
export default class AsaCreativesList extends Command {
|
|
5
|
+
static description: string;
|
|
6
|
+
static enableJsonFlag: boolean;
|
|
7
|
+
static examples: string[];
|
|
8
|
+
static flags: {
|
|
9
|
+
app: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
'campaign-group': import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
page: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
'page-size': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
};
|
|
14
|
+
run(): Promise<PaginatedResponse<AsaCreativeDTO>>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
import { createAsaClient } from '../../../lib/asa-client.js';
|
|
3
|
+
import { asaPaginationFlags, assetScopeFlags, scopeParams } from '../../../lib/asa-flags.js';
|
|
4
|
+
import { paginationParams } from '../../../lib/flags.js';
|
|
5
|
+
import { printList } from '../../../lib/output.js';
|
|
6
|
+
export default class AsaCreativesList extends Command {
|
|
7
|
+
static description = 'List creatives; creative_id is what `asa ads create` needs';
|
|
8
|
+
static enableJsonFlag = true;
|
|
9
|
+
static examples = [
|
|
10
|
+
'<%= config.bin %> asa creatives list',
|
|
11
|
+
'<%= config.bin %> asa creatives list --app APP_UUID',
|
|
12
|
+
];
|
|
13
|
+
static flags = { ...asaPaginationFlags, ...assetScopeFlags };
|
|
14
|
+
async run() {
|
|
15
|
+
const { flags } = await this.parse(AsaCreativesList);
|
|
16
|
+
const client = await createAsaClient(this.config);
|
|
17
|
+
const result = await client.get('/creatives', {
|
|
18
|
+
...paginationParams(flags),
|
|
19
|
+
...scopeParams(flags),
|
|
20
|
+
});
|
|
21
|
+
printList(result.data, this.log.bind(this), result.meta.pagination);
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
import type { AsaKeywordMutationDTO } from '../../../lib/asa-schemas.js';
|
|
3
|
+
export default class AsaKeywordsAdd extends Command {
|
|
4
|
+
static description: string;
|
|
5
|
+
static enableJsonFlag: boolean;
|
|
6
|
+
static examples: string[];
|
|
7
|
+
static flags: {
|
|
8
|
+
'ad-group': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
bid: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
'from-file': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
'match-type': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
status: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
text: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
'idempotency-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
|
+
yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
16
|
+
currency: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
17
|
+
};
|
|
18
|
+
run(): Promise<AsaKeywordMutationDTO>;
|
|
19
|
+
private readTexts;
|
|
20
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Command, Flags } from '@oclif/core';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { asaWrite, createAsaClient, noteReplay } from '../../../lib/asa-client.js';
|
|
4
|
+
import { confirmFlags, confirmMutation } from '../../../lib/asa-confirm.js';
|
|
5
|
+
import { currencyFlag, idempotencyFlags, MAX_BULK_ITEMS, money, moneyFlag, reportBulkOutcome } from '../../../lib/asa-flags.js';
|
|
6
|
+
import { isValidUuid } from '../../../lib/flags.js';
|
|
7
|
+
export default class AsaKeywordsAdd extends Command {
|
|
8
|
+
static description = 'Add targeting keywords to an ad group';
|
|
9
|
+
static enableJsonFlag = true;
|
|
10
|
+
static examples = [
|
|
11
|
+
'<%= config.bin %> asa keywords add --ad-group UUID --text "running shoes" --text "trail shoes" --bid 1.20',
|
|
12
|
+
'<%= config.bin %> asa keywords add --ad-group UUID --from-file keywords.txt --match-type EXACT',
|
|
13
|
+
];
|
|
14
|
+
static flags = {
|
|
15
|
+
...currencyFlag,
|
|
16
|
+
...confirmFlags,
|
|
17
|
+
...idempotencyFlags,
|
|
18
|
+
'ad-group': Flags.string({ description: 'Ad group ID (UUID) — the campaign is resolved from it', required: true }),
|
|
19
|
+
bid: moneyFlag('Bid per keyword'),
|
|
20
|
+
'from-file': Flags.string({ description: 'File with one keyword per line, combined with any --text values' }),
|
|
21
|
+
'match-type': Flags.string({ default: 'BROAD', description: 'Match type', options: ['BROAD', 'EXACT'] }),
|
|
22
|
+
status: Flags.string({ default: 'ACTIVE', description: 'Keyword status', options: ['ACTIVE', 'PAUSED'] }),
|
|
23
|
+
text: Flags.string({ description: 'Keyword text, repeatable', multiple: true }),
|
|
24
|
+
};
|
|
25
|
+
async run() {
|
|
26
|
+
const { flags } = await this.parse(AsaKeywordsAdd);
|
|
27
|
+
if (!isValidUuid(flags['ad-group']))
|
|
28
|
+
this.error('Invalid ad group ID format.', { exit: 2 });
|
|
29
|
+
const texts = [...(flags.text ?? []), ...(await this.readTexts(flags['from-file']))];
|
|
30
|
+
if (texts.length === 0)
|
|
31
|
+
this.error('Pass at least one --text or a --from-file with keywords.', { exit: 2 });
|
|
32
|
+
if (texts.length > MAX_BULK_ITEMS) {
|
|
33
|
+
this.error(`A single call takes at most ${MAX_BULK_ITEMS} keywords, got ${texts.length}.`, { exit: 2 });
|
|
34
|
+
}
|
|
35
|
+
const body = {
|
|
36
|
+
keywords: texts.map((text) => ({
|
|
37
|
+
ad_group_id: flags['ad-group'],
|
|
38
|
+
bid_amount: money(flags.bid, flags.currency),
|
|
39
|
+
match_type: flags['match-type'],
|
|
40
|
+
status: flags.status,
|
|
41
|
+
text,
|
|
42
|
+
})),
|
|
43
|
+
};
|
|
44
|
+
await confirmMutation(this, { body, method: 'POST', path: '/keywords/', summary: `Add ${texts.length} keyword(s)` }, flags.yes);
|
|
45
|
+
const client = await createAsaClient(this.config);
|
|
46
|
+
const { replayed, result } = await asaWrite(client, 'post', '/keywords', {
|
|
47
|
+
body,
|
|
48
|
+
idempotencyKey: flags['idempotency-key'],
|
|
49
|
+
});
|
|
50
|
+
noteReplay(replayed, this.log.bind(this));
|
|
51
|
+
reportBulkOutcome({
|
|
52
|
+
applied: result.keywords,
|
|
53
|
+
errors: result.errors,
|
|
54
|
+
isValidationFailure: result.is_validation_failure,
|
|
55
|
+
kind: 'keywords',
|
|
56
|
+
}, this.log.bind(this));
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
async readTexts(path) {
|
|
60
|
+
if (!path)
|
|
61
|
+
return [];
|
|
62
|
+
try {
|
|
63
|
+
const raw = await readFile(path, 'utf8');
|
|
64
|
+
return raw
|
|
65
|
+
.split('\n')
|
|
66
|
+
.map((line) => line.trim())
|
|
67
|
+
.filter(Boolean);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
this.error(`Could not read ${path}.`, { exit: 2 });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
import type { AsaKeywordDTO } from '../../../lib/asa-schemas.js';
|
|
3
|
+
import { type PaginatedResponse } from '../../../lib/flags.js';
|
|
4
|
+
export default class AsaKeywordsList extends Command {
|
|
5
|
+
static description: string;
|
|
6
|
+
static enableJsonFlag: boolean;
|
|
7
|
+
static examples: string[];
|
|
8
|
+
static flags: {
|
|
9
|
+
status: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
'ad-group': import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
campaign: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
search: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
app: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
'campaign-group': import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
|
+
page: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
16
|
+
'page-size': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
17
|
+
};
|
|
18
|
+
run(): Promise<PaginatedResponse<AsaKeywordDTO>>;
|
|
19
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
import { createAsaClient } from '../../../lib/asa-client.js';
|
|
3
|
+
import { adGroupScopeFlags, asaPaginationFlags, scopeParams, statusFilter } from '../../../lib/asa-flags.js';
|
|
4
|
+
import { paginationParams } from '../../../lib/flags.js';
|
|
5
|
+
import { printList } from '../../../lib/output.js';
|
|
6
|
+
export default class AsaKeywordsList extends Command {
|
|
7
|
+
static description = 'List targeting keywords; read numbers with asa metrics';
|
|
8
|
+
static enableJsonFlag = true;
|
|
9
|
+
static examples = [
|
|
10
|
+
'<%= config.bin %> asa keywords list',
|
|
11
|
+
'<%= config.bin %> asa keywords list --ad-group AD_GROUP_UUID --status ACTIVE',
|
|
12
|
+
];
|
|
13
|
+
static flags = { ...asaPaginationFlags, ...adGroupScopeFlags, ...statusFilter(['ACTIVE', 'PAUSED']) };
|
|
14
|
+
async run() {
|
|
15
|
+
const { flags } = await this.parse(AsaKeywordsList);
|
|
16
|
+
const client = await createAsaClient(this.config);
|
|
17
|
+
const result = await client.get('/keywords', {
|
|
18
|
+
...paginationParams(flags),
|
|
19
|
+
...scopeParams(flags),
|
|
20
|
+
});
|
|
21
|
+
printList(result.data, this.log.bind(this), result.meta.pagination);
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
import type { AsaKeywordMutationDTO } from '../../../lib/asa-schemas.js';
|
|
3
|
+
export default class AsaKeywordsUpdate extends Command {
|
|
4
|
+
static args: {
|
|
5
|
+
keyword_id: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
6
|
+
};
|
|
7
|
+
static description: string;
|
|
8
|
+
static enableJsonFlag: boolean;
|
|
9
|
+
static examples: string[];
|
|
10
|
+
static flags: {
|
|
11
|
+
bid: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
'match-type': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
status: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
text: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
|
+
'idempotency-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
16
|
+
yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
17
|
+
currency: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
18
|
+
};
|
|
19
|
+
static strict: boolean;
|
|
20
|
+
run(): Promise<AsaKeywordMutationDTO>;
|
|
21
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Args, Command, Flags } from '@oclif/core';
|
|
2
|
+
import { asaWrite, createAsaClient, noteReplay } from '../../../lib/asa-client.js';
|
|
3
|
+
import { confirmFlags, confirmMutation } from '../../../lib/asa-confirm.js';
|
|
4
|
+
import { currencyFlag, idempotencyFlags, money, moneyFlag, reportBulkOutcome } from '../../../lib/asa-flags.js';
|
|
5
|
+
import { isValidUuid } from '../../../lib/flags.js';
|
|
6
|
+
export default class AsaKeywordsUpdate extends Command {
|
|
7
|
+
static args = {
|
|
8
|
+
keyword_id: Args.string({ description: 'Keyword ID (UUID), repeatable as extra arguments', required: true }),
|
|
9
|
+
};
|
|
10
|
+
static description = 'Change bid, status, text or match type of keywords';
|
|
11
|
+
static enableJsonFlag = true;
|
|
12
|
+
static examples = [
|
|
13
|
+
'<%= config.bin %> asa keywords update UUID --bid 2.00',
|
|
14
|
+
'<%= config.bin %> asa keywords update UUID_A UUID_B --status PAUSED',
|
|
15
|
+
];
|
|
16
|
+
static flags = {
|
|
17
|
+
...currencyFlag,
|
|
18
|
+
...confirmFlags,
|
|
19
|
+
...idempotencyFlags,
|
|
20
|
+
bid: moneyFlag('Bid'),
|
|
21
|
+
'match-type': Flags.string({ description: 'Match type', options: ['BROAD', 'EXACT'] }),
|
|
22
|
+
status: Flags.string({ description: 'Keyword status', options: ['ACTIVE', 'PAUSED'] }),
|
|
23
|
+
text: Flags.string({ description: 'Keyword text (only meaningful for a single keyword)' }),
|
|
24
|
+
};
|
|
25
|
+
static strict = false;
|
|
26
|
+
async run() {
|
|
27
|
+
const { argv, flags } = await this.parse(AsaKeywordsUpdate);
|
|
28
|
+
const keywordIds = argv;
|
|
29
|
+
for (const id of keywordIds) {
|
|
30
|
+
if (!isValidUuid(id))
|
|
31
|
+
this.error(`Invalid keyword ID format: ${id}`, { exit: 2 });
|
|
32
|
+
}
|
|
33
|
+
const change = {};
|
|
34
|
+
if (flags.bid !== undefined)
|
|
35
|
+
change.bid_amount = money(flags.bid, flags.currency);
|
|
36
|
+
if (flags.status !== undefined)
|
|
37
|
+
change.status = flags.status;
|
|
38
|
+
if (flags['match-type'] !== undefined)
|
|
39
|
+
change.match_type = flags['match-type'];
|
|
40
|
+
if (flags.text !== undefined)
|
|
41
|
+
change.text = flags.text;
|
|
42
|
+
if (Object.keys(change).length === 0) {
|
|
43
|
+
this.error('Nothing to change. Pass at least one field, e.g. --bid 2.00.', { exit: 2 });
|
|
44
|
+
}
|
|
45
|
+
if (flags.text !== undefined && keywordIds.length > 1) {
|
|
46
|
+
this.error('--text would give every keyword the same text. Update them one at a time.', { exit: 2 });
|
|
47
|
+
}
|
|
48
|
+
const body = { keywords: keywordIds.map((id) => ({ internal_id: id, ...change })) };
|
|
49
|
+
await confirmMutation(this, { body, method: 'PUT', path: '/keywords/', summary: `Update ${keywordIds.length} keyword(s)` }, flags.yes);
|
|
50
|
+
const client = await createAsaClient(this.config);
|
|
51
|
+
const { replayed, result } = await asaWrite(client, 'put', '/keywords', {
|
|
52
|
+
body,
|
|
53
|
+
idempotencyKey: flags['idempotency-key'],
|
|
54
|
+
});
|
|
55
|
+
noteReplay(replayed, this.log.bind(this));
|
|
56
|
+
reportBulkOutcome({
|
|
57
|
+
applied: result.keywords,
|
|
58
|
+
errors: result.errors,
|
|
59
|
+
isValidationFailure: result.is_validation_failure,
|
|
60
|
+
kind: 'keywords',
|
|
61
|
+
}, this.log.bind(this));
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
import { type PaginatedResponse } from '../../../lib/flags.js';
|
|
3
|
+
export default class AsaMetrics extends Command {
|
|
4
|
+
static description: string;
|
|
5
|
+
static enableJsonFlag: boolean;
|
|
6
|
+
static examples: string[];
|
|
7
|
+
static flags: {
|
|
8
|
+
'date-from': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
'date-to': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
entity: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
'group-by': import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
metric: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
order: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
'order-by': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
|
+
'order-by-day': import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
16
|
+
'by-days': import("@oclif/core/interfaces").OptionFlag<number[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
17
|
+
page: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
18
|
+
'page-size': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
19
|
+
};
|
|
20
|
+
run(): Promise<PaginatedResponse<Record<string, unknown>>>;
|
|
21
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Command, Flags } from '@oclif/core';
|
|
2
|
+
import { asaWrite, createAsaClient } from '../../../lib/asa-client.js';
|
|
3
|
+
import { ASA_GROUP_BY_DIMENSIONS, ASA_METRIC_ENTITIES, asaPaginationFlags, byDaysFlag, MAX_BY_DAYS } from '../../../lib/asa-flags.js';
|
|
4
|
+
import { paginationParams } from '../../../lib/flags.js';
|
|
5
|
+
import { printList } from '../../../lib/output.js';
|
|
6
|
+
export default class AsaMetrics extends Command {
|
|
7
|
+
static description = `Query metrics for any level of the account over a date range
|
|
8
|
+
|
|
9
|
+
One row per entity, already aggregated server-side and sorted by --order-by, so a top-N question is one
|
|
10
|
+
call with --order-by and --page-size N — never sum pages yourself. Account-level totals are one call to
|
|
11
|
+
asa metrics overview instead. The date window is capped by the coarsest --group-by period: 90 days for
|
|
12
|
+
day or no period grouping, 180 by week, 365 by month and coarser — widen the window by coarsening the
|
|
13
|
+
grouping, not by splitting into more calls. Budget: 5 metrics calls per minute, at most 2 per 10 seconds.`;
|
|
14
|
+
static enableJsonFlag = true;
|
|
15
|
+
static examples = [
|
|
16
|
+
'<%= config.bin %> asa metrics --entity campaign --date-from 2026-07-01 --date-to 2026-07-31',
|
|
17
|
+
'<%= config.bin %> asa metrics --entity campaign --date-from 2026-07-01 --date-to 2026-07-31 --order-by spend --page-size 5',
|
|
18
|
+
'<%= config.bin %> asa metrics --entity campaign --date-from 2026-07-01 --date-to 2026-07-31 --group-by country --page-size 1000',
|
|
19
|
+
'<%= config.bin %> asa metrics --entity keyword --date-from 2026-07-01 --date-to 2026-07-31 --metric spend --metric roas',
|
|
20
|
+
'<%= config.bin %> asa metrics --entity campaign --date-from 2026-07-01 --date-to 2026-07-31 --metric roas --by-days 7 --by-days 90',
|
|
21
|
+
];
|
|
22
|
+
static flags = {
|
|
23
|
+
...asaPaginationFlags,
|
|
24
|
+
...byDaysFlag,
|
|
25
|
+
'date-from': Flags.string({ description: 'Start of the period (YYYY-MM-DD)', required: true }),
|
|
26
|
+
'date-to': Flags.string({ description: 'End of the period (YYYY-MM-DD)', required: true }),
|
|
27
|
+
entity: Flags.string({ description: 'What to report on', options: ASA_METRIC_ENTITIES, required: true }),
|
|
28
|
+
'group-by': Flags.string({
|
|
29
|
+
description: 'Break the rows down by a dimension, repeatable',
|
|
30
|
+
multiple: true,
|
|
31
|
+
options: ASA_GROUP_BY_DIMENSIONS,
|
|
32
|
+
}),
|
|
33
|
+
metric: Flags.string({
|
|
34
|
+
description: 'Metric name (dashboard nomenclature, e.g. spend, taps, gross_roas), repeatable; omit for every metric; a wrong name fails listing all valid ones',
|
|
35
|
+
multiple: true,
|
|
36
|
+
}),
|
|
37
|
+
order: Flags.string({ default: 'desc', description: 'Sort direction', options: ['asc', 'desc'] }),
|
|
38
|
+
'order-by': Flags.string({
|
|
39
|
+
description: 'Metric or field to sort by; cohort metrics rank via their gross_/proceeds_/net_ names',
|
|
40
|
+
}),
|
|
41
|
+
'order-by-day': Flags.integer({
|
|
42
|
+
description: 'Rank by a cohort metric at this renewal window; must be one of the --by-days values',
|
|
43
|
+
}),
|
|
44
|
+
};
|
|
45
|
+
async run() {
|
|
46
|
+
const { flags } = await this.parse(AsaMetrics);
|
|
47
|
+
if (flags['by-days'] && flags['by-days'].length > MAX_BY_DAYS) {
|
|
48
|
+
this.error(`At most ${MAX_BY_DAYS} renewal windows per call, got ${flags['by-days'].length}.`, { exit: 2 });
|
|
49
|
+
}
|
|
50
|
+
const client = await createAsaClient(this.config);
|
|
51
|
+
const { result } = await asaWrite(client, 'post', '/metrics', {
|
|
52
|
+
body: {
|
|
53
|
+
date_from: flags['date-from'],
|
|
54
|
+
date_to: flags['date-to'],
|
|
55
|
+
entity: flags.entity,
|
|
56
|
+
order: flags.order,
|
|
57
|
+
...(flags.metric === undefined ? {} : { metrics: flags.metric }),
|
|
58
|
+
...(flags['by-days'] === undefined ? {} : { by_days: flags['by-days'] }),
|
|
59
|
+
...(flags['group-by'] === undefined ? {} : { group_by: flags['group-by'] }),
|
|
60
|
+
...(flags['order-by'] === undefined ? {} : { order_by: flags['order-by'] }),
|
|
61
|
+
...(flags['order-by-day'] === undefined ? {} : { order_by_day: flags['order-by-day'] }),
|
|
62
|
+
},
|
|
63
|
+
params: paginationParams(flags),
|
|
64
|
+
});
|
|
65
|
+
printList(result.data, this.log.bind(this), result.meta?.pagination);
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class AsaMetricsOverview extends Command {
|
|
3
|
+
static description: string;
|
|
4
|
+
static enableJsonFlag: boolean;
|
|
5
|
+
static examples: string[];
|
|
6
|
+
static flags: {
|
|
7
|
+
'date-from': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
'date-to': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
entity: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
metric: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
'period-unit': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
'by-days': import("@oclif/core/interfaces").OptionFlag<number[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
};
|
|
14
|
+
run(): Promise<Record<string, unknown>>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Command, Flags } from '@oclif/core';
|
|
2
|
+
import { asaWrite, createAsaClient } from '../../../lib/asa-client.js';
|
|
3
|
+
import { ASA_METRIC_ENTITIES, byDaysFlag, MAX_BY_DAYS } from '../../../lib/asa-flags.js';
|
|
4
|
+
import { printResponse } from '../../../lib/output.js';
|
|
5
|
+
export default class AsaMetricsOverview extends Command {
|
|
6
|
+
static description = `Totals for a period, optionally split into day, week or month buckets
|
|
7
|
+
|
|
8
|
+
The one-call answer to "how much did I spend / earn overall" and to any single-period trend question:
|
|
9
|
+
totals for the whole entity level plus a per-period series, no pagination, no client-side summing. The
|
|
10
|
+
date window is capped by --period-unit: 90 days at day, 180 by week, 365 by month and coarser. Shares
|
|
11
|
+
the 5-per-minute metrics budget with asa metrics.`;
|
|
12
|
+
static enableJsonFlag = true;
|
|
13
|
+
static examples = [
|
|
14
|
+
'<%= config.bin %> asa metrics overview --entity campaign --date-from 2026-07-01 --date-to 2026-07-31',
|
|
15
|
+
'<%= config.bin %> asa metrics overview --entity campaign --date-from 2026-07-01 --date-to 2026-07-31 --period-unit WEEK',
|
|
16
|
+
];
|
|
17
|
+
static flags = {
|
|
18
|
+
...byDaysFlag,
|
|
19
|
+
'date-from': Flags.string({ description: 'Start of the period (YYYY-MM-DD)', required: true }),
|
|
20
|
+
'date-to': Flags.string({ description: 'End of the period (YYYY-MM-DD)', required: true }),
|
|
21
|
+
entity: Flags.string({ description: 'What to report on', options: ASA_METRIC_ENTITIES, required: true }),
|
|
22
|
+
metric: Flags.string({
|
|
23
|
+
description: 'Metric name, repeatable; cohort roots only here (revenue, roas, arpu), not their gross_/proceeds_/net_ variants; omit for every metric',
|
|
24
|
+
multiple: true,
|
|
25
|
+
}),
|
|
26
|
+
'period-unit': Flags.string({
|
|
27
|
+
default: 'day',
|
|
28
|
+
description: 'Bucket size',
|
|
29
|
+
options: ['day', 'month', 'quarter', 'week', 'year'],
|
|
30
|
+
}),
|
|
31
|
+
};
|
|
32
|
+
async run() {
|
|
33
|
+
const { flags } = await this.parse(AsaMetricsOverview);
|
|
34
|
+
if (flags['by-days'] && flags['by-days'].length > MAX_BY_DAYS) {
|
|
35
|
+
this.error(`At most ${MAX_BY_DAYS} renewal windows per call, got ${flags['by-days'].length}.`, { exit: 2 });
|
|
36
|
+
}
|
|
37
|
+
const client = await createAsaClient(this.config);
|
|
38
|
+
const { result } = await asaWrite(client, 'post', '/metrics/overview', {
|
|
39
|
+
body: {
|
|
40
|
+
date_from: flags['date-from'],
|
|
41
|
+
date_to: flags['date-to'],
|
|
42
|
+
entity: flags.entity,
|
|
43
|
+
period_unit: flags['period-unit'],
|
|
44
|
+
...(flags.metric === undefined ? {} : { metrics: flags.metric }),
|
|
45
|
+
...(flags['by-days'] === undefined ? {} : { by_days: flags['by-days'] }),
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
printResponse(result, this.log.bind(this));
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
import type { AsaNegativeKeywordMutationDTO } from '../../../lib/asa-schemas.js';
|
|
3
|
+
export default class AsaNegativeKeywordsAdd extends Command {
|
|
4
|
+
static description: string;
|
|
5
|
+
static enableJsonFlag: boolean;
|
|
6
|
+
static examples: string[];
|
|
7
|
+
static flags: {
|
|
8
|
+
'ad-group': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
'all-ad-groups': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
|
+
campaign: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
'match-type': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
status: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
text: import("@oclif/core/interfaces").OptionFlag<string[], import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
'idempotency-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
|
+
yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
16
|
+
};
|
|
17
|
+
run(): Promise<AsaNegativeKeywordMutationDTO>;
|
|
18
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { Command, Flags } from '@oclif/core';
|
|
2
|
+
import { asaWrite, createAsaClient, noteReplay } from '../../../lib/asa-client.js';
|
|
3
|
+
import { confirmFlags, confirmMutation } from '../../../lib/asa-confirm.js';
|
|
4
|
+
import { idempotencyFlags, MAX_BULK_ITEMS, reportBulkOutcome } from '../../../lib/asa-flags.js';
|
|
5
|
+
import { isValidUuid } from '../../../lib/flags.js';
|
|
6
|
+
export default class AsaNegativeKeywordsAdd extends Command {
|
|
7
|
+
static description = 'Add negative keywords to an ad group or across a campaign';
|
|
8
|
+
static enableJsonFlag = true;
|
|
9
|
+
static examples = [
|
|
10
|
+
'<%= config.bin %> asa negative-keywords add --ad-group UUID --text "free"',
|
|
11
|
+
'<%= config.bin %> asa negative-keywords add --campaign UUID --text "free" --text "cheap"',
|
|
12
|
+
'<%= config.bin %> asa negative-keywords add --campaign UUID --all-ad-groups --text "free"',
|
|
13
|
+
];
|
|
14
|
+
static flags = {
|
|
15
|
+
...confirmFlags,
|
|
16
|
+
...idempotencyFlags,
|
|
17
|
+
'ad-group': Flags.string({ description: 'Ad group ID (UUID) — the campaign is resolved from it', exclusive: ['campaign'] }),
|
|
18
|
+
'all-ad-groups': Flags.boolean({
|
|
19
|
+
dependsOn: ['campaign'],
|
|
20
|
+
description: 'Apply to every ad group of the campaign instead of the campaign itself',
|
|
21
|
+
}),
|
|
22
|
+
campaign: Flags.string({ description: 'Campaign ID (UUID)', exclusive: ['ad-group'] }),
|
|
23
|
+
'match-type': Flags.string({ default: 'EXACT', description: 'Match type', options: ['BROAD', 'EXACT'] }),
|
|
24
|
+
status: Flags.string({ default: 'ACTIVE', description: 'Keyword status', options: ['ACTIVE', 'PAUSED'] }),
|
|
25
|
+
text: Flags.string({ description: 'Keyword text, repeatable', multiple: true, required: true }),
|
|
26
|
+
};
|
|
27
|
+
async run() {
|
|
28
|
+
const { flags } = await this.parse(AsaNegativeKeywordsAdd);
|
|
29
|
+
const target = flags['ad-group'] ?? flags.campaign;
|
|
30
|
+
if (!target)
|
|
31
|
+
this.error('Pass either --ad-group or --campaign.', { exit: 2 });
|
|
32
|
+
if (!isValidUuid(target))
|
|
33
|
+
this.error('Invalid ID format.', { exit: 2 });
|
|
34
|
+
if (flags.text.length > MAX_BULK_ITEMS) {
|
|
35
|
+
this.error(`A single call takes at most ${MAX_BULK_ITEMS} keywords, got ${flags.text.length}.`, { exit: 2 });
|
|
36
|
+
}
|
|
37
|
+
const scope = flags['ad-group'] ? 'AD_GROUP' : flags['all-ad-groups'] ? 'ALL_CAMPAIGN_AD_GROUPS' : 'CAMPAIGN';
|
|
38
|
+
const parent = flags['ad-group'] ? { ad_group_id: flags['ad-group'] } : { campaign_id: flags.campaign };
|
|
39
|
+
const body = {
|
|
40
|
+
negative_keywords: flags.text.map((text) => ({
|
|
41
|
+
...parent,
|
|
42
|
+
match_type: flags['match-type'],
|
|
43
|
+
status: flags.status,
|
|
44
|
+
text,
|
|
45
|
+
})),
|
|
46
|
+
scope,
|
|
47
|
+
};
|
|
48
|
+
await confirmMutation(this, {
|
|
49
|
+
body,
|
|
50
|
+
method: 'POST',
|
|
51
|
+
path: '/negative-keywords/',
|
|
52
|
+
summary: scope === 'ALL_CAMPAIGN_AD_GROUPS'
|
|
53
|
+
? `Add ${flags.text.length} negative keyword(s) to every ad group of the campaign`
|
|
54
|
+
: `Add ${flags.text.length} negative keyword(s) in ${scope} scope`,
|
|
55
|
+
}, flags.yes);
|
|
56
|
+
const client = await createAsaClient(this.config);
|
|
57
|
+
const { replayed, result } = await asaWrite(client, 'post', '/negative-keywords', {
|
|
58
|
+
body,
|
|
59
|
+
idempotencyKey: flags['idempotency-key'],
|
|
60
|
+
});
|
|
61
|
+
noteReplay(replayed, this.log.bind(this));
|
|
62
|
+
reportBulkOutcome({
|
|
63
|
+
applied: result.negative_keywords,
|
|
64
|
+
errors: result.errors,
|
|
65
|
+
isValidationFailure: result.is_validation_failure,
|
|
66
|
+
kind: 'negative keywords',
|
|
67
|
+
}, this.log.bind(this));
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
}
|