// JSON:API request builders for App Store Connect. Pure: commands build `Req`s, print them on --dry and send them // with the client otherwise. Request / response format: https://developer.apple.com/documentation/appstoreconnectapi import { log, warn } from '../lib/log'; import { isAmbiguous, isConflict } from '../lib/upsert'; export interface Req { method: 'GET' | 'POST' | 'PATCH' | 'DELETE'; path: string; body?: unknown; note?: string } export const one = (type: string, id: string) => ({ data: { type, id } }); export const many = (type: string, ids: string[]) => ({ data: ids.map(id => ({ type, id })) }); /** One-line description of a request for dry runs and logs. Attribute values are cut short; `hide` keys (emails, * passwords) are masked. */ export function describeReq(r: Req, hide: string[] = ['email', 'demoAccountPassword', 'contactEmail', 'contactPhone']): string { const attrs = (r.body as { data?: { attributes?: Record } } | undefined)?.data?.attributes; const show = (k: string, v: unknown) => (hide.includes(k) ? '"***"' : typeof v === 'string' && v.length > 40 ? JSON.stringify(v.slice(0, 37) + '...') : JSON.stringify(v)); const keys = attrs ? Object.entries(attrs).map(([k, v]) => `${k}=${show(k, v)}`) : []; return `${r.method} ${r.path}${keys.length ? ` {${keys.join(', ')}}` : ''}${r.note ? ` # ${r.note}` : ''}`; } /** * Logs every planned request; sends them in order unless dry. Returns the responses (empty on dry). The plans come * from a fresh read of the store, so a create the store refuses as a duplicate (409 already exists) means an earlier * run made it: it is logged and skipped (its slot in the result is null), never sent again. A create with an unknown * outcome (timeout, 5xx) stops the run without a retry: running the command again reads the store and plans only * what is still missing. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any export async function execute(asc: (m: string, p: string, b?: unknown) => Promise, reqs: Req[], o: { dry: boolean; tag: string }): Promise { const out: unknown[] = []; for (const [i, r] of reqs.entries()) { if (o.dry) { log(o.tag, `would ${describeReq(r)}`); continue; } try { out.push(await asc(r.method, r.path, r.body)); } catch (e) { if (r.method === 'POST' && isConflict(e)) { warn(o.tag, `${describeReq(r)}: already exists, left as it is`); out.push(null); continue; } if (isAmbiguous(e)) throw new Error(`${describeReq(r)}: no clear answer (${(e as Error).message.slice(0, 200)}). Not sent again, so nothing is created twice; ${reqs.length - i - 1} later request(s) not sent. Run the command again: it reads the store and does only what is still missing.`); throw e; } log(o.tag, `${describeReq(r)}: done`); } return out; }