40 lines
2.8 KiB
TypeScript
40 lines
2.8 KiB
TypeScript
// 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<string, unknown> } } | 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<any>, reqs: Req[], o: { dry: boolean; tag: string }): Promise<unknown[]> {
|
|
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;
|
|
}
|