adapty 0.9.3 → 0.9.5
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 +46 -0
- package/dist/cli/input/segment.d.ts +20 -0
- package/dist/cli/input/segment.js +61 -0
- package/dist/commands/asa/change-history/get.d.ts +14 -0
- package/dist/commands/asa/change-history/get.js +39 -0
- package/dist/commands/asa/change-history/list.d.ts +27 -0
- package/dist/commands/asa/change-history/list.js +102 -0
- package/dist/commands/asa/segments/create.d.ts +17 -0
- package/dist/commands/asa/segments/create.js +53 -0
- package/dist/commands/asa/segments/list.d.ts +16 -0
- package/dist/commands/asa/segments/list.js +59 -0
- package/dist/commands/asa/segments/update.d.ts +20 -0
- package/dist/commands/asa/segments/update.js +60 -0
- package/dist/commands/segments/create.d.ts +14 -0
- package/dist/commands/segments/create.js +30 -0
- package/dist/commands/segments/update.d.ts +17 -0
- package/dist/commands/segments/update.js +35 -0
- package/dist/lib/api-schemas.d.ts +24 -0
- package/dist/lib/asa-client.d.ts +3 -0
- package/dist/lib/asa-client.js +26 -0
- package/dist/lib/asa-flags.d.ts +31 -0
- package/dist/lib/asa-flags.js +69 -0
- package/dist/lib/asa-schemas.d.ts +47 -0
- package/oclif.manifest.json +929 -362
- package/package.json +4 -4
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Args, Command } from '@oclif/core';
|
|
2
|
+
import { appIdFlag } from '../../cli/input/app.js';
|
|
3
|
+
import { segmentFilters, segmentWriteFlags } from '../../cli/input/segment.js';
|
|
4
|
+
import { createAuthenticatedClient } from '../../lib/client-from-config.js';
|
|
5
|
+
import { isValidUuid } from '../../lib/flags.js';
|
|
6
|
+
import { printResponse } from '../../lib/output.js';
|
|
7
|
+
export default class SegmentsUpdate extends Command {
|
|
8
|
+
static args = {
|
|
9
|
+
segment_id: Args.string({ description: 'Segment ID (UUID)', required: true }),
|
|
10
|
+
};
|
|
11
|
+
static description = `Replace a segment's title, description and filters
|
|
12
|
+
|
|
13
|
+
The server takes the whole segment, as the dashboard sends it: pass every filter you want to keep, not only
|
|
14
|
+
the changed one. Read the current ones with \`segments get\`.`;
|
|
15
|
+
static enableJsonFlag = true;
|
|
16
|
+
static examples = [
|
|
17
|
+
'<%= config.bin %> segments update --app UUID 550e8400-e29b-41d4-a716-446655440000 --title "[ASA] Campaign: Brand" --filter campaign:IN:2144520245',
|
|
18
|
+
];
|
|
19
|
+
static flags = { ...appIdFlag, ...segmentWriteFlags };
|
|
20
|
+
async run() {
|
|
21
|
+
const { args, flags } = await this.parse(SegmentsUpdate);
|
|
22
|
+
if (!isValidUuid(args.segment_id)) {
|
|
23
|
+
this.error('Invalid segment ID format.', { exit: 2 });
|
|
24
|
+
}
|
|
25
|
+
const body = {
|
|
26
|
+
description: flags.description,
|
|
27
|
+
filters: segmentFilters(flags),
|
|
28
|
+
title: flags.title,
|
|
29
|
+
};
|
|
30
|
+
const client = await createAuthenticatedClient(this.config);
|
|
31
|
+
const result = await client.put(`/apps/${flags.app}/segments/${args.segment_id}`, body);
|
|
32
|
+
printResponse(result, this.log.bind(this));
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -136,6 +136,30 @@ export type SegmentDTO = {
|
|
|
136
136
|
id: string;
|
|
137
137
|
title: string;
|
|
138
138
|
};
|
|
139
|
+
export type SegmentFilterDTO = {
|
|
140
|
+
field_name: string;
|
|
141
|
+
operator: string;
|
|
142
|
+
segment_filter_id: null | string;
|
|
143
|
+
value_list: unknown[];
|
|
144
|
+
};
|
|
145
|
+
/** What `segments get`, `create` and `update` return; the list stays `SegmentDTO`. */
|
|
146
|
+
export type SegmentDetailDTO = SegmentDTO & {
|
|
147
|
+
created_at: string;
|
|
148
|
+
filters: SegmentFilterDTO[];
|
|
149
|
+
profile_count: number;
|
|
150
|
+
refresh_in_progress: boolean;
|
|
151
|
+
updated_at: string;
|
|
152
|
+
};
|
|
153
|
+
export type SegmentFilterInput = {
|
|
154
|
+
field_name: string;
|
|
155
|
+
operator: string;
|
|
156
|
+
value_list: unknown[];
|
|
157
|
+
};
|
|
158
|
+
export type SegmentWriteRequestDTO = {
|
|
159
|
+
description?: string | undefined;
|
|
160
|
+
filters: SegmentFilterInput[];
|
|
161
|
+
title: string;
|
|
162
|
+
};
|
|
139
163
|
export type PlacementPaywallAudienceEntryDTO = {
|
|
140
164
|
content_type: 'paywall';
|
|
141
165
|
paywall_id: string;
|
package/dist/lib/asa-client.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ApiClient } from './api-client.js';
|
|
2
2
|
import type { QueryParams } from './api-client.js';
|
|
3
|
+
import type { AsaSegmentEntity, AsaSegmentSelection } from './asa-flags.js';
|
|
3
4
|
import type { Config } from '@oclif/core';
|
|
4
5
|
export declare const ASA_API_URL = "https://api-asa-admin.adapty.io/api/v1/cli";
|
|
5
6
|
export declare const ASA_API_URL_ENV_VAR = "ADAPTY_ASA_API_URL";
|
|
@@ -19,3 +20,5 @@ export type AsaWriteOutcome<T> = {
|
|
|
19
20
|
};
|
|
20
21
|
export declare function asaWrite<T>(client: ApiClient, method: 'post' | 'put', path: string, opts?: AsaWriteOptions): Promise<AsaWriteOutcome<T>>;
|
|
21
22
|
export declare function noteReplay(replayed: boolean, log: (msg: string) => void): void;
|
|
23
|
+
/** The ASA entities behind a segment source, with the Apple ids the portal filters on. */
|
|
24
|
+
export declare function fetchAsaSegmentEntities(client: ApiClient, selection: AsaSegmentSelection): Promise<AsaSegmentEntity[]>;
|
package/dist/lib/asa-client.js
CHANGED
|
@@ -49,3 +49,29 @@ export function noteReplay(replayed, log) {
|
|
|
49
49
|
log('Already applied earlier — showing the stored result.');
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
|
+
/** The ASA entities behind a segment source, with the Apple ids the portal filters on. */
|
|
53
|
+
export async function fetchAsaSegmentEntities(client, selection) {
|
|
54
|
+
if (selection.source === 'campaign') {
|
|
55
|
+
const campaigns = await Promise.all(selection.ids.map(id => client.get(`/campaigns/${id}`)));
|
|
56
|
+
return campaigns.map(campaign => ({ id: String(campaign.campaign_id), name: campaign.name }));
|
|
57
|
+
}
|
|
58
|
+
if (selection.source === 'ad-group') {
|
|
59
|
+
const adGroups = await Promise.all(selection.ids.map(id => client.get(`/ad-groups/${id}`)));
|
|
60
|
+
return adGroups.map(adGroup => ({ id: String(adGroup.ad_group_id), name: adGroup.name }));
|
|
61
|
+
}
|
|
62
|
+
// No single-keyword read exists; the ad group is small, so one scoped page covers the lookup
|
|
63
|
+
const page = await client.get('/keywords', {
|
|
64
|
+
'ad_group_id': selection.scopeAdGroup,
|
|
65
|
+
'page[size]': '1000',
|
|
66
|
+
});
|
|
67
|
+
const byId = new Map(page.data.map(keyword => [keyword.internal_id, keyword]));
|
|
68
|
+
const missing = selection.ids.filter(id => !byId.has(id));
|
|
69
|
+
if (missing.length > 0) {
|
|
70
|
+
throw new Error(`Keyword(s) not found in ad group ${selection.scopeAdGroup}: ${missing.join(', ')}`);
|
|
71
|
+
}
|
|
72
|
+
return selection.ids.map((id) => {
|
|
73
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- membership checked above
|
|
74
|
+
const keyword = byId.get(id);
|
|
75
|
+
return { id: String(keyword.keyword_id), name: keyword.text };
|
|
76
|
+
});
|
|
77
|
+
}
|
package/dist/lib/asa-flags.d.ts
CHANGED
|
@@ -131,4 +131,35 @@ type BulkOutcome = {
|
|
|
131
131
|
kind: string;
|
|
132
132
|
};
|
|
133
133
|
export declare function reportBulkOutcome({ applied, errors, isValidationFailure, kind }: BulkOutcome, log: (msg: string) => void): void;
|
|
134
|
+
export type AsaSegmentSource = 'ad-group' | 'campaign' | 'keyword';
|
|
135
|
+
export declare const asaSegmentSourceFlags: {
|
|
136
|
+
'ad-group': import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
137
|
+
campaign: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
138
|
+
keyword: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
139
|
+
};
|
|
140
|
+
export type AsaSegmentSourceFlags = {
|
|
141
|
+
'ad-group'?: string[] | undefined;
|
|
142
|
+
'campaign'?: string[] | undefined;
|
|
143
|
+
'keyword'?: string[] | undefined;
|
|
144
|
+
};
|
|
145
|
+
/**
|
|
146
|
+
* One source kind per segment, as in the Ads Manager modal. Keywords are looked up inside one ad group, so
|
|
147
|
+
* `--keyword` borrows `--ad-group` as its scope instead of competing with it.
|
|
148
|
+
*/
|
|
149
|
+
export type AsaSegmentSelection = {
|
|
150
|
+
ids: string[];
|
|
151
|
+
scopeAdGroup: string | undefined;
|
|
152
|
+
source: AsaSegmentSource;
|
|
153
|
+
};
|
|
154
|
+
export declare function resolveAsaSegmentSource(flags: AsaSegmentSourceFlags): AsaSegmentSelection;
|
|
155
|
+
export declare const ASA_SEGMENT_FIELD: Record<AsaSegmentSource, string>;
|
|
156
|
+
export type AsaSegmentEntity = {
|
|
157
|
+
id: string;
|
|
158
|
+
name: string;
|
|
159
|
+
};
|
|
160
|
+
/** The dashboard's own title and description for an Ads Manager segment, so it recognises the segment as its own. */
|
|
161
|
+
export declare function buildAsaSegmentNaming(source: AsaSegmentSource, entities: AsaSegmentEntity[]): {
|
|
162
|
+
description: string;
|
|
163
|
+
title: string;
|
|
164
|
+
};
|
|
134
165
|
export {};
|
package/dist/lib/asa-flags.js
CHANGED
|
@@ -247,3 +247,72 @@ export function reportBulkOutcome({ applied, errors, isValidationFailure, kind }
|
|
|
247
247
|
log(` ${describeListedError(error).text}`);
|
|
248
248
|
}
|
|
249
249
|
}
|
|
250
|
+
export const asaSegmentSourceFlags = {
|
|
251
|
+
'ad-group': idFilter('ad group'),
|
|
252
|
+
'campaign': idFilter('campaign'),
|
|
253
|
+
'keyword': idFilter('keyword'),
|
|
254
|
+
};
|
|
255
|
+
export function resolveAsaSegmentSource(flags) {
|
|
256
|
+
const keywords = flags.keyword ?? [];
|
|
257
|
+
const adGroups = flags['ad-group'] ?? [];
|
|
258
|
+
const campaigns = flags.campaign ?? [];
|
|
259
|
+
if (keywords.length > 0) {
|
|
260
|
+
const [scopeAdGroup] = adGroups;
|
|
261
|
+
if (campaigns.length > 0 || adGroups.length !== 1 || scopeAdGroup === undefined) {
|
|
262
|
+
throw new Error('--keyword takes exactly one --ad-group as the lookup scope and no --campaign.');
|
|
263
|
+
}
|
|
264
|
+
return { ids: [...new Set(keywords)], scopeAdGroup, source: 'keyword' };
|
|
265
|
+
}
|
|
266
|
+
if (campaigns.length > 0 && adGroups.length > 0) {
|
|
267
|
+
throw new Error('Pass either --campaign or --ad-group, not both: a segment is built from one kind of entity.');
|
|
268
|
+
}
|
|
269
|
+
if (campaigns.length > 0) {
|
|
270
|
+
return { ids: [...new Set(campaigns)], scopeAdGroup: undefined, source: 'campaign' };
|
|
271
|
+
}
|
|
272
|
+
if (adGroups.length > 0) {
|
|
273
|
+
return { ids: [...new Set(adGroups)], scopeAdGroup: undefined, source: 'ad-group' };
|
|
274
|
+
}
|
|
275
|
+
throw new Error('Pass at least one --campaign, --ad-group or --keyword.');
|
|
276
|
+
}
|
|
277
|
+
export const ASA_SEGMENT_FIELD = {
|
|
278
|
+
'ad-group': 'ad_group',
|
|
279
|
+
'campaign': 'campaign',
|
|
280
|
+
// The portal calls a keyword attribution "creative"
|
|
281
|
+
'keyword': 'creative',
|
|
282
|
+
};
|
|
283
|
+
const ASA_TITLE_MAX = 120;
|
|
284
|
+
const ASA_DESCRIPTION_MAX = 240;
|
|
285
|
+
const ASA_PREVIEW_COUNT = 3;
|
|
286
|
+
const ASA_SOURCE_WORDS = {
|
|
287
|
+
'ad-group': { many: 'ad groups', one: 'ad group', title: 'Ad group' },
|
|
288
|
+
'campaign': { many: 'campaigns', one: 'campaign', title: 'Campaign' },
|
|
289
|
+
'keyword': { many: 'keywords', one: 'keyword', title: 'Keyword' },
|
|
290
|
+
};
|
|
291
|
+
function truncate(text, max) {
|
|
292
|
+
if (text.length <= max) {
|
|
293
|
+
return text;
|
|
294
|
+
}
|
|
295
|
+
return max <= 3 ? text.slice(0, max) : `${text.slice(0, max - 3)}...`;
|
|
296
|
+
}
|
|
297
|
+
function withRest(preview, rest) {
|
|
298
|
+
return rest > 0 ? `${preview} +${rest}...` : preview;
|
|
299
|
+
}
|
|
300
|
+
/** The dashboard's own title and description for an Ads Manager segment, so it recognises the segment as its own. */
|
|
301
|
+
export function buildAsaSegmentNaming(source, entities) {
|
|
302
|
+
const words = ASA_SOURCE_WORDS[source];
|
|
303
|
+
if (entities.length === 1) {
|
|
304
|
+
const [entity] = entities;
|
|
305
|
+
return {
|
|
306
|
+
description: truncate(`Created from Apple Search Ads ${words.one} "${entity.name}".\n\nID: ${entity.id}`, ASA_DESCRIPTION_MAX),
|
|
307
|
+
title: truncate(`[ASA] ${words.title}: ${entity.name}`, ASA_TITLE_MAX),
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
const shown = entities.slice(0, ASA_PREVIEW_COUNT);
|
|
311
|
+
const rest = entities.length - shown.length;
|
|
312
|
+
const names = withRest(shown.map(entity => entity.name).join(', '), rest);
|
|
313
|
+
const ids = withRest(shown.map(entity => entity.id).join(', '), rest);
|
|
314
|
+
return {
|
|
315
|
+
description: truncate(`Created from Apple Search Ads ${words.many} (${entities.length}).\n\nPreview: ${names}\n\nIDs: ${ids}`, ASA_DESCRIPTION_MAX),
|
|
316
|
+
title: truncate(`[ASA] ${entities.length} ${words.many}: ${names}`, ASA_TITLE_MAX),
|
|
317
|
+
};
|
|
318
|
+
}
|
|
@@ -424,3 +424,50 @@ export type AsaCompetitorBrandKeywordPoolDTO = {
|
|
|
424
424
|
export type AsaCompetitorBrandKeywordPoolsDTO = {
|
|
425
425
|
pools: AsaCompetitorBrandKeywordPoolDTO[];
|
|
426
426
|
};
|
|
427
|
+
export type AsaChangeHistoryEntityRefDTO = {
|
|
428
|
+
detail_id: string;
|
|
429
|
+
entity_id: string;
|
|
430
|
+
entity_name: null | string;
|
|
431
|
+
};
|
|
432
|
+
export type AsaChangeHistoryEventDTO = {
|
|
433
|
+
campaign_group_id: string;
|
|
434
|
+
changes_count: number;
|
|
435
|
+
entities: AsaChangeHistoryEntityRefDTO[];
|
|
436
|
+
entity_type: string;
|
|
437
|
+
event_time: string;
|
|
438
|
+
event_type: string;
|
|
439
|
+
modified_by: null | string;
|
|
440
|
+
transaction_id: string;
|
|
441
|
+
user_type: string;
|
|
442
|
+
};
|
|
443
|
+
export type AsaFieldChangeDTO = {
|
|
444
|
+
field: string;
|
|
445
|
+
new_values: string[];
|
|
446
|
+
old_values: string[];
|
|
447
|
+
};
|
|
448
|
+
export type AsaChangeHistoryDetailDTO = {
|
|
449
|
+
campaign_group_id: string;
|
|
450
|
+
changes: AsaFieldChangeDTO[];
|
|
451
|
+
detail_id: string;
|
|
452
|
+
entity_id: string;
|
|
453
|
+
entity_metadata: Record<string, unknown>;
|
|
454
|
+
entity_type: string;
|
|
455
|
+
event_time: string;
|
|
456
|
+
event_type: string;
|
|
457
|
+
modified_by: null | string;
|
|
458
|
+
user_type: string;
|
|
459
|
+
};
|
|
460
|
+
export type AsaChangeHistoryFieldEventDTO = {
|
|
461
|
+
campaign_group_id: string;
|
|
462
|
+
entity_id: string;
|
|
463
|
+
entity_name: null | string;
|
|
464
|
+
entity_type: string;
|
|
465
|
+
event_time: string;
|
|
466
|
+
event_type: string;
|
|
467
|
+
field: string;
|
|
468
|
+
modified_by: null | string;
|
|
469
|
+
new_values: string[];
|
|
470
|
+
old_values: string[];
|
|
471
|
+
transaction_id: string;
|
|
472
|
+
user_type: string;
|
|
473
|
+
};
|