adapty 0.3.0 → 0.5.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 +156 -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/bulk-create.d.ts +25 -0
- package/dist/commands/asa/campaigns/bulk-create.js +177 -0
- package/dist/commands/asa/campaigns/bulk-status.d.ts +15 -0
- package/dist/commands/asa/campaigns/bulk-status.js +26 -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 +16 -3
- package/dist/lib/api-client.js +53 -18
- 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 +327 -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 +2956 -157
- package/package.json +41 -1
package/dist/lib/api-client.js
CHANGED
|
@@ -1,51 +1,75 @@
|
|
|
1
1
|
import { ApiError, NetworkError, parseApiError } from './errors.js';
|
|
2
2
|
const DEFAULT_API_URL = 'https://api-admin.adapty.io/api/v1/developer';
|
|
3
|
+
const MAX_RETRY_AFTER_SECONDS = 60;
|
|
3
4
|
function ensureTrailingSlash(path) {
|
|
4
5
|
return path.endsWith('/') ? path : `${path}/`;
|
|
5
6
|
}
|
|
6
7
|
export class ApiClient {
|
|
7
8
|
baseUrl;
|
|
9
|
+
errorFormat;
|
|
8
10
|
token;
|
|
9
11
|
userAgent;
|
|
10
12
|
constructor(opts = {}) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
+
const defaultBaseUrl = opts.defaultBaseUrl ?? DEFAULT_API_URL;
|
|
14
|
+
const envBaseUrl = process.env[opts.urlEnvVar ?? 'ADAPTY_API_URL'];
|
|
15
|
+
this.baseUrl = (opts.baseUrl ?? envBaseUrl ?? defaultBaseUrl).replace(/\/$/, '');
|
|
16
|
+
if (this.baseUrl !== defaultBaseUrl) {
|
|
13
17
|
process.stderr.write(`Warning: using non-default API URL: ${this.baseUrl}\n`);
|
|
14
18
|
}
|
|
19
|
+
this.errorFormat = opts.errorFormat ?? 'developer';
|
|
15
20
|
this.token = opts.token ?? null;
|
|
16
21
|
this.userAgent = opts.userAgent ?? 'adapty-cli';
|
|
17
22
|
}
|
|
18
23
|
async get(path, params) {
|
|
19
|
-
|
|
20
|
-
if (params) {
|
|
21
|
-
const qs = new URLSearchParams(params);
|
|
22
|
-
url += `?${qs.toString()}`;
|
|
23
|
-
}
|
|
24
|
-
return this.request(url, { method: 'GET' });
|
|
24
|
+
return this.request(this.buildUrl(path, params), { method: 'GET' });
|
|
25
25
|
}
|
|
26
|
-
async post(path, body) {
|
|
27
|
-
return this.request(
|
|
26
|
+
async post(path, body, params, opts) {
|
|
27
|
+
return this.request(this.buildUrl(path, params), {
|
|
28
28
|
body: body ? JSON.stringify(body) : undefined,
|
|
29
29
|
method: 'POST',
|
|
30
|
-
});
|
|
30
|
+
}, opts);
|
|
31
|
+
}
|
|
32
|
+
async postForm(path, form, params, opts) {
|
|
33
|
+
return this.request(this.buildUrl(path, params), { body: form, method: 'POST' }, opts);
|
|
31
34
|
}
|
|
32
|
-
async put(path, body) {
|
|
33
|
-
return this.request(
|
|
35
|
+
async put(path, body, params, opts) {
|
|
36
|
+
return this.request(this.buildUrl(path, params), {
|
|
34
37
|
body: body ? JSON.stringify(body) : undefined,
|
|
35
38
|
method: 'PUT',
|
|
36
|
-
});
|
|
39
|
+
}, opts);
|
|
40
|
+
}
|
|
41
|
+
buildUrl(path, params) {
|
|
42
|
+
const url = `${this.baseUrl}${ensureTrailingSlash(path)}`;
|
|
43
|
+
if (!params)
|
|
44
|
+
return url;
|
|
45
|
+
const search = new URLSearchParams();
|
|
46
|
+
for (const [key, value] of Object.entries(params)) {
|
|
47
|
+
if (value === undefined)
|
|
48
|
+
continue;
|
|
49
|
+
for (const item of Array.isArray(value) ? value : [value])
|
|
50
|
+
search.append(key, item);
|
|
51
|
+
}
|
|
52
|
+
return search.size === 0 ? url : `${url}?${search.toString()}`;
|
|
53
|
+
}
|
|
54
|
+
isRetryableRateLimit(error) {
|
|
55
|
+
return (this.errorFormat === 'asa' &&
|
|
56
|
+
error.statusCode === 429 &&
|
|
57
|
+
error.errorCode !== 'cli_cooldown_active' &&
|
|
58
|
+
error.retryAfterSeconds !== undefined &&
|
|
59
|
+
error.retryAfterSeconds <= MAX_RETRY_AFTER_SECONDS);
|
|
37
60
|
}
|
|
38
61
|
// eslint-disable-next-line no-undef
|
|
39
|
-
async request(url, init) {
|
|
62
|
+
async request(url, init, opts = {}, retried = false) {
|
|
40
63
|
const headers = {
|
|
41
64
|
'User-Agent': this.userAgent,
|
|
42
65
|
};
|
|
43
|
-
if (init.body) {
|
|
66
|
+
if (init.body && !(init.body instanceof FormData)) {
|
|
44
67
|
headers['Content-Type'] = 'application/json';
|
|
45
68
|
}
|
|
46
69
|
if (this.token) {
|
|
47
70
|
headers.Authorization = `Bearer ${this.token}`;
|
|
48
71
|
}
|
|
72
|
+
Object.assign(headers, opts.headers);
|
|
49
73
|
let response;
|
|
50
74
|
try {
|
|
51
75
|
response = await fetch(url, { ...init, headers });
|
|
@@ -53,24 +77,35 @@ export class ApiClient {
|
|
|
53
77
|
catch (error) {
|
|
54
78
|
throw new NetworkError(error instanceof Error ? error.message : 'Connection failed');
|
|
55
79
|
}
|
|
80
|
+
opts.onResponse?.(response.headers);
|
|
56
81
|
if (response.status === 204) {
|
|
57
82
|
return undefined;
|
|
58
83
|
}
|
|
84
|
+
const retryAfter = Number.parseInt(response.headers.get('Retry-After') ?? '', 10);
|
|
85
|
+
const errorOptions = this.errorFormat === 'asa' && !Number.isNaN(retryAfter) ? { retryAfterSeconds: retryAfter } : {};
|
|
59
86
|
let body;
|
|
60
87
|
try {
|
|
61
88
|
body = await response.json();
|
|
62
89
|
}
|
|
63
90
|
catch {
|
|
64
91
|
if (!response.ok) {
|
|
65
|
-
throw new ApiError(response.status, `http_${response.status}`, {});
|
|
92
|
+
throw new ApiError(response.status, `http_${response.status}`, {}, errorOptions);
|
|
66
93
|
}
|
|
67
94
|
return undefined;
|
|
68
95
|
}
|
|
69
96
|
if (!response.ok) {
|
|
70
|
-
const error = parseApiError(response.status, body);
|
|
97
|
+
const error = parseApiError(response.status, body, errorOptions, this.errorFormat);
|
|
71
98
|
if (response.status === 401) {
|
|
72
99
|
error.message = 'Token expired or invalid. Run `adapty auth login`.';
|
|
73
100
|
}
|
|
101
|
+
if (!retried && this.isRetryableRateLimit(error)) {
|
|
102
|
+
const seconds = error.retryAfterSeconds ?? 0;
|
|
103
|
+
process.stderr.write(`Rate limited (${error.errorCode}); waiting ${seconds}s per Retry-After, then retrying once.\n`);
|
|
104
|
+
await new Promise((resolve) => {
|
|
105
|
+
setTimeout(resolve, seconds * 1000);
|
|
106
|
+
});
|
|
107
|
+
return this.request(url, init, opts, true);
|
|
108
|
+
}
|
|
74
109
|
throw error;
|
|
75
110
|
}
|
|
76
111
|
return body;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Config } from '@oclif/core';
|
|
2
|
+
import { ApiClient, type QueryParams } from './api-client.js';
|
|
3
|
+
export declare const ASA_API_URL = "https://api-asa-admin.adapty.io/api/v1/cli";
|
|
4
|
+
export declare const ASA_API_URL_ENV_VAR = "ADAPTY_ASA_API_URL";
|
|
5
|
+
export declare function createAsaClient(config: Config): Promise<ApiClient>;
|
|
6
|
+
export interface AsaWriteOptions {
|
|
7
|
+
body?: unknown;
|
|
8
|
+
idempotencyKey?: string;
|
|
9
|
+
params?: QueryParams;
|
|
10
|
+
}
|
|
11
|
+
export interface AsaWriteOutcome<T> {
|
|
12
|
+
replayed: boolean;
|
|
13
|
+
result: T;
|
|
14
|
+
}
|
|
15
|
+
export declare function asaWrite<T>(client: ApiClient, method: 'post' | 'put', path: string, opts?: AsaWriteOptions): Promise<AsaWriteOutcome<T>>;
|
|
16
|
+
export declare function noteReplay(replayed: boolean, log: (msg: string) => void): void;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { ApiClient } from './api-client.js';
|
|
3
|
+
import { resolveToken } from './auth.js';
|
|
4
|
+
import { buildUserAgent } from './client-from-config.js';
|
|
5
|
+
import { AuthRequiredError, NetworkError } from './errors.js';
|
|
6
|
+
export const ASA_API_URL = 'https://api-asa-admin.adapty.io/api/v1/cli';
|
|
7
|
+
export const ASA_API_URL_ENV_VAR = 'ADAPTY_ASA_API_URL';
|
|
8
|
+
export async function createAsaClient(config) {
|
|
9
|
+
const token = await resolveToken(config.configDir);
|
|
10
|
+
if (!token)
|
|
11
|
+
throw new AuthRequiredError();
|
|
12
|
+
return new ApiClient({
|
|
13
|
+
defaultBaseUrl: ASA_API_URL,
|
|
14
|
+
errorFormat: 'asa',
|
|
15
|
+
token,
|
|
16
|
+
urlEnvVar: ASA_API_URL_ENV_VAR,
|
|
17
|
+
userAgent: buildUserAgent(config),
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
export async function asaWrite(client, method, path, opts = {}) {
|
|
21
|
+
const key = opts.idempotencyKey ?? randomUUID();
|
|
22
|
+
let replayed = false;
|
|
23
|
+
const requestOpts = {
|
|
24
|
+
headers: { 'Idempotency-Key': key },
|
|
25
|
+
onResponse(headers) {
|
|
26
|
+
replayed = headers.get('Idempotency-Replayed') === 'true';
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
const send = () => method === 'post'
|
|
30
|
+
? client.post(path, opts.body, opts.params, requestOpts)
|
|
31
|
+
: client.put(path, opts.body, opts.params, requestOpts);
|
|
32
|
+
try {
|
|
33
|
+
const result = await send();
|
|
34
|
+
return { replayed, result };
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
if (!(error instanceof NetworkError))
|
|
38
|
+
throw error;
|
|
39
|
+
const result = await send();
|
|
40
|
+
return { replayed, result };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export function noteReplay(replayed, log) {
|
|
44
|
+
if (replayed)
|
|
45
|
+
log('Already applied earlier — showing the stored result.');
|
|
46
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export declare const confirmFlags: {
|
|
3
|
+
yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
4
|
+
};
|
|
5
|
+
export type ConfirmDecision = 'ask' | 'proceed' | 'refuse';
|
|
6
|
+
export declare function decideConfirmation(opts: {
|
|
7
|
+
isTty: boolean;
|
|
8
|
+
json: boolean;
|
|
9
|
+
yes: boolean;
|
|
10
|
+
}): ConfirmDecision;
|
|
11
|
+
export interface MutationPreview {
|
|
12
|
+
body?: unknown;
|
|
13
|
+
method: string;
|
|
14
|
+
path: string;
|
|
15
|
+
summary: string;
|
|
16
|
+
}
|
|
17
|
+
export declare function renderPreview(preview: MutationPreview): string;
|
|
18
|
+
export declare function confirmMutation(command: Command, preview: MutationPreview, yes: boolean): Promise<void>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import { createInterface } from 'node:readline/promises';
|
|
3
|
+
export const confirmFlags = {
|
|
4
|
+
yes: Flags.boolean({
|
|
5
|
+
char: 'y',
|
|
6
|
+
description: 'Apply without asking; required when the output is piped or --json is used',
|
|
7
|
+
}),
|
|
8
|
+
};
|
|
9
|
+
export function decideConfirmation(opts) {
|
|
10
|
+
if (opts.yes)
|
|
11
|
+
return 'proceed';
|
|
12
|
+
if (opts.json || !opts.isTty)
|
|
13
|
+
return 'refuse';
|
|
14
|
+
return 'ask';
|
|
15
|
+
}
|
|
16
|
+
export function renderPreview(preview) {
|
|
17
|
+
const lines = [preview.summary, `${preview.method} ${preview.path}`];
|
|
18
|
+
if (preview.body !== undefined)
|
|
19
|
+
lines.push(JSON.stringify(preview.body, null, 2));
|
|
20
|
+
return lines.join('\n');
|
|
21
|
+
}
|
|
22
|
+
export async function confirmMutation(command, preview, yes) {
|
|
23
|
+
const decision = decideConfirmation({ isTty: process.stdin.isTTY === true, json: command.jsonEnabled(), yes });
|
|
24
|
+
if (decision === 'proceed')
|
|
25
|
+
return;
|
|
26
|
+
if (decision === 'refuse') {
|
|
27
|
+
const reason = `${preview.summary} changes your account. Re-run with --yes to apply it without a prompt.`;
|
|
28
|
+
process.stderr.write(`${reason}\n${renderPreview(preview)}\n`);
|
|
29
|
+
command.error(reason, { exit: 2 });
|
|
30
|
+
}
|
|
31
|
+
process.stderr.write(`${renderPreview(preview)}\n`);
|
|
32
|
+
const reader = createInterface({ input: process.stdin, output: process.stderr });
|
|
33
|
+
try {
|
|
34
|
+
const answer = await reader.question('Apply? [y/N] ');
|
|
35
|
+
if (!/^y(es)?$/i.test(answer.trim()))
|
|
36
|
+
command.error('Cancelled, nothing was sent.', { exit: 1 });
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
reader.close();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { QueryParams } from './api-client.js';
|
|
2
|
+
import type { AsaMoney, AsaMutationError } from './asa-schemas.js';
|
|
3
|
+
export declare const MAX_BULK_ITEMS = 100;
|
|
4
|
+
export declare const MAX_BY_DAYS = 16;
|
|
5
|
+
export declare const ASA_METRIC_ENTITIES: string[];
|
|
6
|
+
export declare const ASA_GROUP_BY_DIMENSIONS: string[];
|
|
7
|
+
export declare const asaPaginationFlags: {
|
|
8
|
+
page: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
'page-size': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
};
|
|
11
|
+
export declare const byDaysFlag: {
|
|
12
|
+
'by-days': import("@oclif/core/interfaces").OptionFlag<number[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
};
|
|
14
|
+
export declare const periodFlags: {
|
|
15
|
+
'date-from': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
16
|
+
'date-to': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
17
|
+
};
|
|
18
|
+
export declare const assetScopeFlags: {
|
|
19
|
+
app: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
20
|
+
'campaign-group': import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
21
|
+
};
|
|
22
|
+
export declare const orgScopeFlags: {
|
|
23
|
+
search: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
24
|
+
app: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
25
|
+
'campaign-group': import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
26
|
+
};
|
|
27
|
+
export declare const campaignScopeFlags: {
|
|
28
|
+
campaign: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
29
|
+
search: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
30
|
+
app: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
31
|
+
'campaign-group': import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
32
|
+
};
|
|
33
|
+
export declare const adGroupScopeFlags: {
|
|
34
|
+
'ad-group': import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
35
|
+
campaign: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
36
|
+
search: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
37
|
+
app: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
38
|
+
'campaign-group': import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
39
|
+
};
|
|
40
|
+
export declare const adScopeFlags: {
|
|
41
|
+
search: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
42
|
+
'ad-group': import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
43
|
+
campaign: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
44
|
+
'campaign-group': import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
45
|
+
};
|
|
46
|
+
export declare const statusFilter: (options: string[]) => {
|
|
47
|
+
status: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
48
|
+
};
|
|
49
|
+
interface ScopeFlags {
|
|
50
|
+
'ad-group'?: string[];
|
|
51
|
+
app?: string[];
|
|
52
|
+
campaign?: string[];
|
|
53
|
+
'campaign-group'?: string[];
|
|
54
|
+
search?: string;
|
|
55
|
+
status?: string;
|
|
56
|
+
}
|
|
57
|
+
export declare function scopeParams(flags: ScopeFlags): QueryParams;
|
|
58
|
+
export declare const scheduleFlags: {
|
|
59
|
+
'end-time': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
60
|
+
'start-time': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
61
|
+
};
|
|
62
|
+
export declare const pricingModelFlag: {
|
|
63
|
+
'pricing-model': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
64
|
+
};
|
|
65
|
+
export declare function startOfDayUtc(date: string | undefined): string | undefined;
|
|
66
|
+
export declare function todayUtc(): string;
|
|
67
|
+
export declare function periodParams(flags: {
|
|
68
|
+
'date-from'?: string;
|
|
69
|
+
'date-to'?: string;
|
|
70
|
+
}): Record<string, string>;
|
|
71
|
+
export declare function moneyFlag(description: string, opts?: {
|
|
72
|
+
required?: boolean;
|
|
73
|
+
}): import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
74
|
+
export declare const currencyFlag: {
|
|
75
|
+
currency: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
76
|
+
};
|
|
77
|
+
export declare const idempotencyFlags: {
|
|
78
|
+
'idempotency-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
79
|
+
};
|
|
80
|
+
export declare function money(amount: string | undefined, currency: string): AsaMoney | undefined;
|
|
81
|
+
interface BulkOutcome {
|
|
82
|
+
applied: unknown[];
|
|
83
|
+
errors: AsaMutationError[];
|
|
84
|
+
isValidationFailure: boolean;
|
|
85
|
+
kind: string;
|
|
86
|
+
}
|
|
87
|
+
export declare function reportBulkOutcome({ applied, errors, isValidationFailure, kind }: BulkOutcome, log: (msg: string) => void): void;
|
|
88
|
+
export {};
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import { describeListedError } from './errors.js';
|
|
3
|
+
import { isValidUuid } from './flags.js';
|
|
4
|
+
const DATE_REGEX = /^\d{4}-\d{2}-\d{2}$/;
|
|
5
|
+
const MONEY_REGEX = /^\d+(\.\d{1,6})?$/;
|
|
6
|
+
export const MAX_BULK_ITEMS = 100;
|
|
7
|
+
export const MAX_BY_DAYS = 16;
|
|
8
|
+
export const ASA_METRIC_ENTITIES = ['ad', 'ad-group', 'campaign', 'keyword'];
|
|
9
|
+
export const ASA_GROUP_BY_DIMENSIONS = ['country', 'day', 'month', 'quarter', 'week', 'year'];
|
|
10
|
+
export const asaPaginationFlags = {
|
|
11
|
+
page: Flags.integer({
|
|
12
|
+
default: 1,
|
|
13
|
+
description: 'Page number',
|
|
14
|
+
min: 1,
|
|
15
|
+
}),
|
|
16
|
+
'page-size': Flags.integer({
|
|
17
|
+
default: 100,
|
|
18
|
+
description: 'Items per page (max 1000); prefer one big page over a pagination loop',
|
|
19
|
+
max: 1000,
|
|
20
|
+
min: 1,
|
|
21
|
+
}),
|
|
22
|
+
};
|
|
23
|
+
export const byDaysFlag = {
|
|
24
|
+
'by-days': Flags.integer({
|
|
25
|
+
description: 'Renewal window in days for cohort metrics, repeatable; omit for the dashboard defaults',
|
|
26
|
+
multiple: true,
|
|
27
|
+
}),
|
|
28
|
+
};
|
|
29
|
+
async function parseDate(input) {
|
|
30
|
+
if (!DATE_REGEX.test(input))
|
|
31
|
+
throw new Error('Dates must be written as YYYY-MM-DD.');
|
|
32
|
+
return input;
|
|
33
|
+
}
|
|
34
|
+
export const periodFlags = {
|
|
35
|
+
'date-from': Flags.string({
|
|
36
|
+
description: 'Start of the reporting period (YYYY-MM-DD), defaults to today',
|
|
37
|
+
parse: parseDate,
|
|
38
|
+
}),
|
|
39
|
+
'date-to': Flags.string({
|
|
40
|
+
description: 'End of the reporting period (YYYY-MM-DD), defaults to today',
|
|
41
|
+
parse: parseDate,
|
|
42
|
+
}),
|
|
43
|
+
};
|
|
44
|
+
async function parseId(input) {
|
|
45
|
+
if (!isValidUuid(input))
|
|
46
|
+
throw new Error('Ids are the UUIDs printed by the matching list command.');
|
|
47
|
+
return input;
|
|
48
|
+
}
|
|
49
|
+
function idFilter(entity) {
|
|
50
|
+
return Flags.string({ description: `Keep only rows in this ${entity}; repeatable`, multiple: true, parse: parseId });
|
|
51
|
+
}
|
|
52
|
+
const searchFilter = { search: Flags.string({ description: 'Case-insensitive substring match on the name' }) };
|
|
53
|
+
export const assetScopeFlags = {
|
|
54
|
+
app: idFilter('app'),
|
|
55
|
+
'campaign-group': idFilter('campaign group'),
|
|
56
|
+
};
|
|
57
|
+
export const orgScopeFlags = { ...assetScopeFlags, ...searchFilter };
|
|
58
|
+
export const campaignScopeFlags = { ...orgScopeFlags, campaign: idFilter('campaign') };
|
|
59
|
+
export const adGroupScopeFlags = { ...campaignScopeFlags, 'ad-group': idFilter('ad group') };
|
|
60
|
+
export const adScopeFlags = {
|
|
61
|
+
'ad-group': idFilter('ad group'),
|
|
62
|
+
campaign: idFilter('campaign'),
|
|
63
|
+
'campaign-group': idFilter('campaign group'),
|
|
64
|
+
...searchFilter,
|
|
65
|
+
};
|
|
66
|
+
export const statusFilter = (options) => ({
|
|
67
|
+
status: Flags.string({ description: 'Keep only rows in this state', options }),
|
|
68
|
+
});
|
|
69
|
+
export function scopeParams(flags) {
|
|
70
|
+
return {
|
|
71
|
+
ad_group_id: flags['ad-group'],
|
|
72
|
+
app_id: flags.app,
|
|
73
|
+
campaign_group_id: flags['campaign-group'],
|
|
74
|
+
campaign_id: flags.campaign,
|
|
75
|
+
search: flags.search,
|
|
76
|
+
status: flags.status,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
export const scheduleFlags = {
|
|
80
|
+
'end-time': Flags.string({ description: 'Schedule end (YYYY-MM-DD)', parse: parseDate }),
|
|
81
|
+
'start-time': Flags.string({ description: 'Schedule start (YYYY-MM-DD), defaults to today', parse: parseDate }),
|
|
82
|
+
};
|
|
83
|
+
export const pricingModelFlag = {
|
|
84
|
+
'pricing-model': Flags.string({
|
|
85
|
+
default: 'CPC',
|
|
86
|
+
description: 'Pricing model; Apple requires one on every ad group',
|
|
87
|
+
options: ['CPC', 'CPM'],
|
|
88
|
+
}),
|
|
89
|
+
};
|
|
90
|
+
export function startOfDayUtc(date) {
|
|
91
|
+
return date === undefined ? undefined : `${date}T00:00:00Z`;
|
|
92
|
+
}
|
|
93
|
+
export function todayUtc() {
|
|
94
|
+
return new Date().toISOString().slice(0, 10);
|
|
95
|
+
}
|
|
96
|
+
export function periodParams(flags) {
|
|
97
|
+
const params = {};
|
|
98
|
+
if (flags['date-from'])
|
|
99
|
+
params.date_from = flags['date-from'];
|
|
100
|
+
if (flags['date-to'])
|
|
101
|
+
params.date_to = flags['date-to'];
|
|
102
|
+
return params;
|
|
103
|
+
}
|
|
104
|
+
async function parseMoney(input) {
|
|
105
|
+
if (!MONEY_REGEX.test(input))
|
|
106
|
+
throw new Error('Amounts must be plain numbers, e.g. 50 or 12.50.');
|
|
107
|
+
return input;
|
|
108
|
+
}
|
|
109
|
+
export function moneyFlag(description, opts = {}) {
|
|
110
|
+
return Flags.string({
|
|
111
|
+
description: `${description} (amount, e.g. 50 or 12.50)`,
|
|
112
|
+
parse: parseMoney,
|
|
113
|
+
required: opts.required,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
export const currencyFlag = {
|
|
117
|
+
currency: Flags.string({ default: 'USD', description: 'Currency code for the amounts in this call' }),
|
|
118
|
+
};
|
|
119
|
+
export const idempotencyFlags = {
|
|
120
|
+
'idempotency-key': Flags.string({
|
|
121
|
+
description: 'Idempotency key for this write; re-running with the same key replays the stored result instead of applying twice',
|
|
122
|
+
}),
|
|
123
|
+
};
|
|
124
|
+
export function money(amount, currency) {
|
|
125
|
+
return amount === undefined ? undefined : { amount, currency };
|
|
126
|
+
}
|
|
127
|
+
export function reportBulkOutcome({ applied, errors, isValidationFailure, kind }, log) {
|
|
128
|
+
if (isValidationFailure) {
|
|
129
|
+
log(`Nothing was applied: the batch failed validation before Apple was called.`);
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
log(`${applied.length} ${kind} applied, ${errors.length} rejected.`);
|
|
133
|
+
}
|
|
134
|
+
for (const error of errors) {
|
|
135
|
+
log(` ${describeListedError(error).text}`);
|
|
136
|
+
}
|
|
137
|
+
}
|