adapty 0.8.3 → 0.8.4-beta.1
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 +2 -132
- package/dist/commands/asa/ad-groups/create.js +1 -1
- package/dist/commands/asa/ad-groups/update.js +1 -1
- package/dist/commands/asa/ads/create.js +1 -1
- package/dist/commands/asa/ads/update.js +1 -1
- package/dist/commands/asa/automations/create.d.ts +0 -9
- package/dist/commands/asa/automations/create.js +3 -19
- package/dist/commands/asa/automations/run.js +1 -1
- package/dist/commands/asa/automations/update.d.ts +0 -9
- package/dist/commands/asa/automations/update.js +6 -22
- package/dist/commands/asa/campaigns/bulk-create.js +1 -1
- package/dist/commands/asa/campaigns/create.js +1 -1
- package/dist/commands/asa/campaigns/update.js +1 -1
- package/dist/commands/asa/keywords/add.js +1 -1
- package/dist/commands/asa/keywords/update.js +1 -1
- package/dist/commands/asa/metrics/index.d.ts +1 -4
- package/dist/commands/asa/metrics/index.js +8 -18
- package/dist/commands/asa/negative-keywords/add.js +1 -1
- package/dist/commands/asa/product-pages/sync.js +1 -1
- package/dist/commands/flows/config/get.js +2 -1
- package/dist/commands/flows/publish.d.ts +15 -0
- package/dist/commands/flows/publish.js +52 -0
- package/dist/commands/flows/update.d.ts +15 -0
- package/dist/commands/flows/update.js +28 -0
- package/dist/commands/placements/create.js +29 -4
- package/dist/commands/placements/update.js +29 -4
- package/dist/lib/api-schemas.d.ts +20 -1
- package/dist/lib/asa-flags.d.ts +0 -24
- package/dist/lib/asa-flags.js +0 -54
- package/dist/lib/errors.js +1 -2
- package/dist/lib/flow-help.d.ts +12 -0
- package/dist/lib/flow-help.js +27 -0
- package/dist/lib/placement-audiences.d.ts +6 -0
- package/dist/lib/placement-audiences.js +25 -0
- package/oclif.manifest.json +601 -664
- package/package.json +3 -2
- package/dist/lib/asa-keyword-action.d.ts +0 -18
- package/dist/lib/asa-keyword-action.js +0 -160
- /package/dist/lib/{asa-confirm.d.ts → confirm.d.ts} +0 -0
- /package/dist/lib/{asa-confirm.js → confirm.js} +0 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Args, Command, Flags } from '@oclif/core';
|
|
2
|
+
import { createAuthenticatedClient } from '../../lib/client-from-config.js';
|
|
3
|
+
import { appFlag, isValidUuid } from '../../lib/flags.js';
|
|
4
|
+
import { printResponse } from '../../lib/output.js';
|
|
5
|
+
export default class FlowsUpdate extends Command {
|
|
6
|
+
static args = {
|
|
7
|
+
flow_id: Args.string({ description: 'Flow ID (UUID)', required: true }),
|
|
8
|
+
};
|
|
9
|
+
static description = 'Rename a flow';
|
|
10
|
+
static enableJsonFlag = true;
|
|
11
|
+
static examples = ['<%= config.bin %> flows update --app UUID 550e8400-e29b-41d4-a716-446655440000 --name "Onboarding v2"'];
|
|
12
|
+
static flags = {
|
|
13
|
+
...appFlag,
|
|
14
|
+
name: Flags.string({ description: 'New flow name', required: true }),
|
|
15
|
+
};
|
|
16
|
+
async run() {
|
|
17
|
+
const { args, flags } = await this.parse(FlowsUpdate);
|
|
18
|
+
if (!isValidUuid(args.flow_id)) {
|
|
19
|
+
this.error('Invalid flow ID format.', { exit: 2 });
|
|
20
|
+
}
|
|
21
|
+
const body = { name: flags.name };
|
|
22
|
+
const client = await createAuthenticatedClient(this.config);
|
|
23
|
+
const result = await client.put(`/apps/${flags.app}/flows/${args.flow_id}/`, body);
|
|
24
|
+
this.log('Flow updated!');
|
|
25
|
+
printResponse(result, this.log.bind(this));
|
|
26
|
+
return result;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -1,18 +1,24 @@
|
|
|
1
1
|
import { Command, Flags } from '@oclif/core';
|
|
2
2
|
import { createAuthenticatedClient } from '../../lib/client-from-config.js';
|
|
3
3
|
import { appFlag } from '../../lib/flags.js';
|
|
4
|
+
import { draftFlowError } from '../../lib/flow-help.js';
|
|
4
5
|
import { printResponse } from '../../lib/output.js';
|
|
6
|
+
import { audienceEntryProblem } from '../../lib/placement-audiences.js';
|
|
5
7
|
export default class PlacementsCreate extends Command {
|
|
6
8
|
static description = 'Create a placement with a paywall';
|
|
7
9
|
static enableJsonFlag = true;
|
|
8
10
|
static examples = [
|
|
9
|
-
'<%= config.bin %> placements create --app UUID --title "Default" --developer-id default --audiences \'[{"segment_ids":[],"paywall_id":"PAYWALL_UUID","priority":0}]\'',
|
|
11
|
+
'<%= config.bin %> placements create --app UUID --title "Default" --developer-id default --audiences \'[{"content_type":"paywall","segment_ids":[],"paywall_id":"PAYWALL_UUID","priority":0}]\'',
|
|
12
|
+
'<%= config.bin %> placements create --app UUID --title "Default" --developer-id default --audiences \'[{"content_type":"flow","segment_ids":[],"flow_id":"FLOW_UUID","priority":0}]\'',
|
|
10
13
|
'<%= config.bin %> placements create --app UUID --title "Default" --developer-id default --paywall-id PAYWALL_UUID',
|
|
11
14
|
];
|
|
12
15
|
static flags = {
|
|
13
16
|
...appFlag,
|
|
14
17
|
audiences: Flags.string({
|
|
15
|
-
description: 'JSON array of audience entries
|
|
18
|
+
description: 'JSON array of audience entries. Every entry needs an explicit content_type. ' +
|
|
19
|
+
'Paywall: {content_type:"paywall", segment_ids, paywall_id, priority}. ' +
|
|
20
|
+
'Flow: {content_type:"flow", segment_ids, flow_id, priority}. ' +
|
|
21
|
+
'A flow must be published first (flows publish) — attaching a draft flow returns 400.',
|
|
16
22
|
exactlyOne: ['paywall-id', 'audiences'],
|
|
17
23
|
}),
|
|
18
24
|
'developer-id': Flags.string({ description: 'Developer ID for the placement', required: true }),
|
|
@@ -36,15 +42,34 @@ export default class PlacementsCreate extends Command {
|
|
|
36
42
|
body.paywall_id = flags['paywall-id'];
|
|
37
43
|
}
|
|
38
44
|
else {
|
|
45
|
+
let parsed;
|
|
39
46
|
try {
|
|
40
|
-
|
|
47
|
+
parsed = JSON.parse(flags.audiences);
|
|
41
48
|
}
|
|
42
49
|
catch (error) {
|
|
43
50
|
this.error(`Invalid --audiences JSON: ${error instanceof Error ? error.message : String(error)}`, { exit: 2 });
|
|
44
51
|
}
|
|
52
|
+
if (!Array.isArray(parsed)) {
|
|
53
|
+
this.error('--audiences must be a JSON array of audience entries.', { exit: 2 });
|
|
54
|
+
}
|
|
55
|
+
for (const [index, entry] of parsed.entries()) {
|
|
56
|
+
const problem = audienceEntryProblem(entry);
|
|
57
|
+
if (problem)
|
|
58
|
+
this.error(`--audiences[${index}]: ${problem}`, { exit: 2 });
|
|
59
|
+
}
|
|
60
|
+
body.audiences = parsed;
|
|
45
61
|
}
|
|
46
62
|
const client = await createAuthenticatedClient(this.config);
|
|
47
|
-
|
|
63
|
+
let result;
|
|
64
|
+
try {
|
|
65
|
+
result = await client.post(`/apps/${flags.app}/placements`, body);
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
const message = draftFlowError(error, flags.app, body.audiences);
|
|
69
|
+
if (message)
|
|
70
|
+
this.error(message, { exit: 2 });
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
48
73
|
this.log('Placement created!');
|
|
49
74
|
printResponse(result, this.log.bind(this));
|
|
50
75
|
return result;
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { Args, Command, Flags } from '@oclif/core';
|
|
2
2
|
import { createAuthenticatedClient } from '../../lib/client-from-config.js';
|
|
3
3
|
import { appFlag, isValidUuid } from '../../lib/flags.js';
|
|
4
|
+
import { draftFlowError } from '../../lib/flow-help.js';
|
|
4
5
|
import { printResponse } from '../../lib/output.js';
|
|
6
|
+
import { audienceEntryProblem } from '../../lib/placement-audiences.js';
|
|
5
7
|
export default class PlacementsUpdate extends Command {
|
|
6
8
|
static args = {
|
|
7
9
|
placement_id: Args.string({ description: 'Placement ID (UUID)', required: true }),
|
|
@@ -9,13 +11,17 @@ export default class PlacementsUpdate extends Command {
|
|
|
9
11
|
static description = 'Update a placement';
|
|
10
12
|
static enableJsonFlag = true;
|
|
11
13
|
static examples = [
|
|
12
|
-
'<%= config.bin %> placements update --app UUID 550e8400-... --title "Default" --developer-id default --audiences \'[{"segment_ids":[],"paywall_id":"PAYWALL_UUID","priority":0}]\'',
|
|
14
|
+
'<%= config.bin %> placements update --app UUID 550e8400-... --title "Default" --developer-id default --audiences \'[{"content_type":"paywall","segment_ids":[],"paywall_id":"PAYWALL_UUID","priority":0}]\'',
|
|
15
|
+
'<%= config.bin %> placements update --app UUID 550e8400-... --title "Default" --developer-id default --audiences \'[{"content_type":"flow","segment_ids":[],"flow_id":"FLOW_UUID","priority":0}]\'',
|
|
13
16
|
'<%= config.bin %> placements update --app UUID 550e8400-... --title "Default" --developer-id default --paywall-id PAYWALL_UUID',
|
|
14
17
|
];
|
|
15
18
|
static flags = {
|
|
16
19
|
...appFlag,
|
|
17
20
|
audiences: Flags.string({
|
|
18
|
-
description: 'JSON array of audience entries
|
|
21
|
+
description: 'JSON array of audience entries. Every entry needs an explicit content_type. ' +
|
|
22
|
+
'Paywall: {content_type:"paywall", segment_ids, paywall_id, priority}. ' +
|
|
23
|
+
'Flow: {content_type:"flow", segment_ids, flow_id, priority}. ' +
|
|
24
|
+
'A flow must be published first (flows publish) — attaching a draft flow returns 400.',
|
|
19
25
|
exactlyOne: ['paywall-id', 'audiences'],
|
|
20
26
|
}),
|
|
21
27
|
'developer-id': Flags.string({ description: 'Developer ID for the placement', required: true }),
|
|
@@ -45,15 +51,34 @@ export default class PlacementsUpdate extends Command {
|
|
|
45
51
|
body.paywall_id = flags['paywall-id'];
|
|
46
52
|
}
|
|
47
53
|
else {
|
|
54
|
+
let parsed;
|
|
48
55
|
try {
|
|
49
|
-
|
|
56
|
+
parsed = JSON.parse(flags.audiences);
|
|
50
57
|
}
|
|
51
58
|
catch (error) {
|
|
52
59
|
this.error(`Invalid --audiences JSON: ${error instanceof Error ? error.message : String(error)}`, { exit: 2 });
|
|
53
60
|
}
|
|
61
|
+
if (!Array.isArray(parsed)) {
|
|
62
|
+
this.error('--audiences must be a JSON array of audience entries.', { exit: 2 });
|
|
63
|
+
}
|
|
64
|
+
for (const [index, entry] of parsed.entries()) {
|
|
65
|
+
const problem = audienceEntryProblem(entry);
|
|
66
|
+
if (problem)
|
|
67
|
+
this.error(`--audiences[${index}]: ${problem}`, { exit: 2 });
|
|
68
|
+
}
|
|
69
|
+
body.audiences = parsed;
|
|
54
70
|
}
|
|
55
71
|
const client = await createAuthenticatedClient(this.config);
|
|
56
|
-
|
|
72
|
+
let result;
|
|
73
|
+
try {
|
|
74
|
+
result = await client.put(`/apps/${flags.app}/placements/${args.placement_id}`, body);
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
const message = draftFlowError(error, flags.app, body.audiences);
|
|
78
|
+
if (message)
|
|
79
|
+
this.error(message, { exit: 2 });
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
57
82
|
this.log('Placement updated!');
|
|
58
83
|
printResponse(result, this.log.bind(this));
|
|
59
84
|
return result;
|
|
@@ -87,8 +87,14 @@ export interface FlowRemoteConfigDTO {
|
|
|
87
87
|
}
|
|
88
88
|
export interface FlowConfigDTO {
|
|
89
89
|
config: Record<string, unknown>;
|
|
90
|
+
/** Human message for a failed publication (raw passthrough from the flow version). */
|
|
91
|
+
publication_error?: null | string;
|
|
92
|
+
/** Publication lifecycle of the flow version: transforming|transformed|uploading|uploaded|published|failed. */
|
|
93
|
+
publication_status?: string;
|
|
90
94
|
remote_configs: FlowRemoteConfigDTO[];
|
|
91
95
|
status: FlowStatus;
|
|
96
|
+
/** Raw transform failure: a JSON issues payload or a summary string. Parse with `parseFlowPublicationError`. */
|
|
97
|
+
transform_error?: null | string;
|
|
92
98
|
/** Millisecond timestamp of the last content change; the value `expected_updated_at` is compared against on write. */
|
|
93
99
|
updated_at: number;
|
|
94
100
|
}
|
|
@@ -129,20 +135,33 @@ export interface SegmentDTO {
|
|
|
129
135
|
id: string;
|
|
130
136
|
title: string;
|
|
131
137
|
}
|
|
132
|
-
export interface
|
|
138
|
+
export interface PlacementPaywallAudienceEntryDTO {
|
|
139
|
+
content_type: 'paywall';
|
|
133
140
|
paywall_id: string;
|
|
134
141
|
priority: number;
|
|
135
142
|
segment_ids?: string[];
|
|
136
143
|
}
|
|
144
|
+
export interface PlacementFlowAudienceEntryDTO {
|
|
145
|
+
content_type: 'flow';
|
|
146
|
+
/** The flow must be `published` (not draft) — the backend returns 400 otherwise. */
|
|
147
|
+
flow_id: string;
|
|
148
|
+
priority: number;
|
|
149
|
+
segment_ids?: string[];
|
|
150
|
+
}
|
|
151
|
+
export type PlacementAudienceEntryDTO = PlacementFlowAudienceEntryDTO | PlacementPaywallAudienceEntryDTO;
|
|
137
152
|
export interface PlacementSummaryDTO {
|
|
138
153
|
developer_id: string;
|
|
139
154
|
id: string;
|
|
155
|
+
/** Placement activation state: true = Live, false = Inactive. */
|
|
156
|
+
is_active: boolean;
|
|
140
157
|
title: string;
|
|
141
158
|
}
|
|
142
159
|
export interface PlacementDetailDTO {
|
|
143
160
|
audiences?: PlacementAudienceEntryDTO[];
|
|
144
161
|
developer_id: string;
|
|
145
162
|
id: string;
|
|
163
|
+
/** Placement activation state: true = Live, false = Inactive. */
|
|
164
|
+
is_active: boolean;
|
|
146
165
|
title: string;
|
|
147
166
|
}
|
|
148
167
|
export interface PlacementWriteRequestDTO {
|
package/dist/lib/asa-flags.d.ts
CHANGED
|
@@ -44,20 +44,6 @@ export declare const adScopeFlags: {
|
|
|
44
44
|
campaign: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
45
45
|
'campaign-group': import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
46
46
|
};
|
|
47
|
-
export declare const metricsScopeFlags: {
|
|
48
|
-
'ad-group': import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
49
|
-
app: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
50
|
-
campaign: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
51
|
-
};
|
|
52
|
-
export declare function metricsScopeBody(flags: {
|
|
53
|
-
'ad-group'?: string[];
|
|
54
|
-
app?: string[];
|
|
55
|
-
campaign?: string[];
|
|
56
|
-
}): {
|
|
57
|
-
campaign_id?: string[] | undefined;
|
|
58
|
-
app_id?: string[] | undefined;
|
|
59
|
-
ad_group_id?: string[] | undefined;
|
|
60
|
-
};
|
|
61
47
|
export declare const statusFilter: (options: string[]) => {
|
|
62
48
|
status: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
63
49
|
};
|
|
@@ -89,16 +75,6 @@ export declare function moneyFlag(description: string, opts?: {
|
|
|
89
75
|
export declare const currencyFlag: {
|
|
90
76
|
currency: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
91
77
|
};
|
|
92
|
-
export declare const addKeywordActionFlags: {
|
|
93
|
-
'cpt-bid': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
94
|
-
'cpt-bid-type': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
95
|
-
'match-type': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
96
|
-
negate: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
97
|
-
'no-negate': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
98
|
-
'pause-original': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
99
|
-
'skip-enable-duplicates': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
100
|
-
'target-ad-group': import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
101
|
-
};
|
|
102
78
|
export declare const idempotencyFlags: {
|
|
103
79
|
'idempotency-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
104
80
|
};
|
package/dist/lib/asa-flags.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
|
-
import { CPT_BID_TYPES, KEYWORD_MATCH_TYPES, NEGATE_TYPES } from './asa-keyword-action.js';
|
|
3
2
|
import { describeListedError } from './errors.js';
|
|
4
3
|
import { isValidUuid } from './flags.js';
|
|
5
4
|
const DATE_REGEX = /^\d{4}-\d{2}-\d{2}$/;
|
|
@@ -64,18 +63,6 @@ export const adScopeFlags = {
|
|
|
64
63
|
'campaign-group': idFilter('campaign group'),
|
|
65
64
|
...searchFilter,
|
|
66
65
|
};
|
|
67
|
-
export const metricsScopeFlags = {
|
|
68
|
-
'ad-group': idFilter('ad group'),
|
|
69
|
-
app: idFilter('app'),
|
|
70
|
-
campaign: idFilter('campaign'),
|
|
71
|
-
};
|
|
72
|
-
export function metricsScopeBody(flags) {
|
|
73
|
-
return {
|
|
74
|
-
...(flags['ad-group'] === undefined ? {} : { ad_group_id: flags['ad-group'] }),
|
|
75
|
-
...(flags.app === undefined ? {} : { app_id: flags.app }),
|
|
76
|
-
...(flags.campaign === undefined ? {} : { campaign_id: flags.campaign }),
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
66
|
export const statusFilter = (options) => ({
|
|
80
67
|
status: Flags.string({ description: 'Keep only rows in this state', options }),
|
|
81
68
|
});
|
|
@@ -129,47 +116,6 @@ export function moneyFlag(description, opts = {}) {
|
|
|
129
116
|
export const currencyFlag = {
|
|
130
117
|
currency: Flags.string({ default: 'USD', description: 'Currency code for the amounts in this call' }),
|
|
131
118
|
};
|
|
132
|
-
// Params of the add-as-keyword-to action, shared by `automations create` and `automations update`.
|
|
133
|
-
// The API picks the params variant by shape, so which of these apply depends on the rule's
|
|
134
|
-
// operate_with: --negate/--no-negate/--skip-enable-duplicates are search-term only,
|
|
135
|
-
// --pause-original is targeting-keyword only. See lib/asa-keyword-action.ts.
|
|
136
|
-
export const addKeywordActionFlags = {
|
|
137
|
-
'cpt-bid': Flags.string({
|
|
138
|
-
description: 'cpt_bid.value: the bid itself with --cpt-bid-type set_to, or a percent markup on the entity bid with ' +
|
|
139
|
-
'search_term_current_cpt / keyword_current_bid; not accepted with ad_group_default_bid',
|
|
140
|
-
parse: parseMoney,
|
|
141
|
-
}),
|
|
142
|
-
'cpt-bid-type': Flags.string({
|
|
143
|
-
description: 'Where the bid of the created keyword comes from; the API has no default',
|
|
144
|
-
options: CPT_BID_TYPES,
|
|
145
|
-
}),
|
|
146
|
-
'match-type': Flags.string({
|
|
147
|
-
description: 'Match type of the created keyword; the API has no default',
|
|
148
|
-
options: KEYWORD_MATCH_TYPES,
|
|
149
|
-
}),
|
|
150
|
-
negate: Flags.string({
|
|
151
|
-
description: 'Also add the search term as a negative keyword at this level (search-term rules)',
|
|
152
|
-
exclusive: ['no-negate'],
|
|
153
|
-
options: NEGATE_TYPES,
|
|
154
|
-
}),
|
|
155
|
-
'no-negate': Flags.boolean({
|
|
156
|
-
description: 'Leave no negative keyword behind (search-term rules)',
|
|
157
|
-
exclusive: ['negate'],
|
|
158
|
-
}),
|
|
159
|
-
'pause-original': Flags.boolean({
|
|
160
|
-
allowNo: true,
|
|
161
|
-
description: 'Pause the source keyword in its own ad group (targeting-keyword rules)',
|
|
162
|
-
}),
|
|
163
|
-
'skip-enable-duplicates': Flags.boolean({
|
|
164
|
-
allowNo: true,
|
|
165
|
-
description: 'Skip a keyword that already exists in the target ad group instead of enabling it (search-term rules)',
|
|
166
|
-
}),
|
|
167
|
-
'target-ad-group': Flags.string({
|
|
168
|
-
description: 'Ad group the keyword is added to (UUID), repeatable; a rule without one does nothing',
|
|
169
|
-
multiple: true,
|
|
170
|
-
parse: parseId,
|
|
171
|
-
}),
|
|
172
|
-
};
|
|
173
119
|
export const idempotencyFlags = {
|
|
174
120
|
'idempotency-key': Flags.string({
|
|
175
121
|
description: 'Idempotency key for this write; re-running with the same key replays the stored result instead of applying twice',
|
package/dist/lib/errors.js
CHANGED
|
@@ -2,8 +2,7 @@ export function describeListedError(error) {
|
|
|
2
2
|
const code = error.error_code ?? error.apple_error_code ?? error.validation_error_code ?? undefined;
|
|
3
3
|
const reason = error.message ?? error.apple_error_message ?? error.validation_error_message ?? code ?? 'rejected';
|
|
4
4
|
const position = error.input_ref === null || error.input_ref === undefined ? '' : ` (item ${error.input_ref + 1})`;
|
|
5
|
-
|
|
6
|
-
return { code, text: `${field}${reason}${position}` };
|
|
5
|
+
return { code, text: `${reason}${position}` };
|
|
7
6
|
}
|
|
8
7
|
export class ApiError extends Error {
|
|
9
8
|
statusCode;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare function builderUrl(flowId: string): string;
|
|
2
|
+
/** The two "• …" footer lines pointing at the Builder UI and the flows agent skill. */
|
|
3
|
+
export declare function flowFixLinks(flowId: string): string;
|
|
4
|
+
/**
|
|
5
|
+
* When `error` is the backend's draft-flow rejection, returns an enriched message with the
|
|
6
|
+
* publish command and fix links; otherwise returns null so the caller rethrows unchanged.
|
|
7
|
+
* `flowId` is taken from the single flow audience, or `<FLOW_ID>` when it is ambiguous.
|
|
8
|
+
*/
|
|
9
|
+
export declare function draftFlowError(error: unknown, app: string, audiences: null | {
|
|
10
|
+
content_type?: string;
|
|
11
|
+
flow_id?: string;
|
|
12
|
+
}[]): null | string;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Shared "how to fix a flow" links, used by `flows publish` (config-error message)
|
|
2
|
+
// and by `placements create`/`update` (draft-attach error). One source for both URLs.
|
|
3
|
+
import { ApiError } from './errors.js';
|
|
4
|
+
export function builderUrl(flowId) {
|
|
5
|
+
return `https://app.adapty.io/flows/${flowId}/builder`;
|
|
6
|
+
}
|
|
7
|
+
/** The two "• …" footer lines pointing at the Builder UI and the flows agent skill. */
|
|
8
|
+
export function flowFixLinks(flowId) {
|
|
9
|
+
return ` • Builder UI: ${builderUrl(flowId)}\n • Adapty flows agent skill: https://adapty.io/docs/flow-generator-skill`;
|
|
10
|
+
}
|
|
11
|
+
// The backend message when a placement tries to attach an unpublished flow. There is no
|
|
12
|
+
// distinct error_code (it comes back as generic validation_error), so match on the text.
|
|
13
|
+
const DRAFT_FLOW_MARKER = 'Flow must be published before placing in a placement';
|
|
14
|
+
/**
|
|
15
|
+
* When `error` is the backend's draft-flow rejection, returns an enriched message with the
|
|
16
|
+
* publish command and fix links; otherwise returns null so the caller rethrows unchanged.
|
|
17
|
+
* `flowId` is taken from the single flow audience, or `<FLOW_ID>` when it is ambiguous.
|
|
18
|
+
*/
|
|
19
|
+
export function draftFlowError(error, app, audiences) {
|
|
20
|
+
if (!(error instanceof ApiError) || !error.message.includes(DRAFT_FLOW_MARKER))
|
|
21
|
+
return null;
|
|
22
|
+
const flows = (audiences ?? []).filter((a) => a.content_type === 'flow' && typeof a.flow_id === 'string');
|
|
23
|
+
const flowId = flows.length === 1 ? flows[0].flow_id : '<FLOW_ID>';
|
|
24
|
+
return ('Cannot attach a draft flow to a placement — publish it first.\n' +
|
|
25
|
+
`Publish: adapty flows publish --app ${app} ${flowId}\n` +
|
|
26
|
+
flowFixLinks(flowId));
|
|
27
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns a human-readable problem string when `entry` is not a valid placement
|
|
3
|
+
* audience entry, or `null` when it is well-formed. Every entry must carry an
|
|
4
|
+
* explicit `content_type` ('paywall' | 'flow'); there is no implicit paywall.
|
|
5
|
+
*/
|
|
6
|
+
export declare function audienceEntryProblem(entry: unknown): null | string;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Runtime guard for --audiences entries parsed from user/agent-supplied JSON.
|
|
2
|
+
// The DTO union in api-schemas.ts requires content_type, but a JSON.parse gives
|
|
3
|
+
// back `unknown`, so the type alone cannot enforce it — this does, the same way
|
|
4
|
+
// for humans and agents (no prompting).
|
|
5
|
+
/**
|
|
6
|
+
* Returns a human-readable problem string when `entry` is not a valid placement
|
|
7
|
+
* audience entry, or `null` when it is well-formed. Every entry must carry an
|
|
8
|
+
* explicit `content_type` ('paywall' | 'flow'); there is no implicit paywall.
|
|
9
|
+
*/
|
|
10
|
+
export function audienceEntryProblem(entry) {
|
|
11
|
+
if (typeof entry !== 'object' || entry === null) {
|
|
12
|
+
return 'each entry must be a JSON object';
|
|
13
|
+
}
|
|
14
|
+
const { content_type: contentType, flow_id: flowId, paywall_id: paywallId } = entry;
|
|
15
|
+
if (contentType !== 'flow' && contentType !== 'paywall') {
|
|
16
|
+
return 'content_type is required and must be "paywall" or "flow"';
|
|
17
|
+
}
|
|
18
|
+
if (contentType === 'paywall' && typeof paywallId !== 'string') {
|
|
19
|
+
return 'a paywall entry requires paywall_id';
|
|
20
|
+
}
|
|
21
|
+
if (contentType === 'flow' && typeof flowId !== 'string') {
|
|
22
|
+
return 'a flow entry requires flow_id';
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|