|
| 1 | +import {CacheOptions, GraphQLResponse, UnauthorizedHandler, graphqlRequestDoc} from './graphql.js' |
| 2 | +import {setNextDeprecationDate} from '../../../private/node/context/deprecations-store.js' |
| 3 | +import {buildHeaders} from '../../../private/node/api/headers.js' |
| 4 | +import {RequestModeInput} from '../http.js' |
| 5 | +import {devStoreFqdn} from '../context/fqdn.js' |
| 6 | +import Bottleneck from 'bottleneck' |
| 7 | +import {TypedDocumentNode} from '@graphql-typed-document-node/core' |
| 8 | +import {Variables} from 'graphql-request' |
| 9 | + |
| 10 | +// API Rate limiter for partners API (Limit is 10 requests per second) |
| 11 | +// Jobs are launched every 150ms to add an extra 50ms margin per request. |
| 12 | +// Only 10 requests can be executed concurrently. |
| 13 | +const limiter = new Bottleneck({ |
| 14 | + minTime: 150, |
| 15 | + maxConcurrent: 10, |
| 16 | +}) |
| 17 | + |
| 18 | +async function setupRequest(token: string) { |
| 19 | + const api = 'Dev Store' |
| 20 | + const fqdn = await devStoreFqdn() |
| 21 | + const url = `https://${fqdn}/dev_stores/unstable/graphql.json` |
| 22 | + return { |
| 23 | + token, |
| 24 | + api, |
| 25 | + url, |
| 26 | + responseOptions: {onResponse: handleDeprecations}, |
| 27 | + } |
| 28 | +} |
| 29 | + |
| 30 | +export const devStoresHeaders = (token: string): {[key: string]: string} => { |
| 31 | + return buildHeaders(token) |
| 32 | +} |
| 33 | + |
| 34 | +// export const devStoresAppLogsUrl = async ( |
| 35 | +// organizationId: string, |
| 36 | +// cursor?: string, |
| 37 | +// filters?: { |
| 38 | +// status?: string |
| 39 | +// source?: string |
| 40 | +// }, |
| 41 | +// ): Promise<string> => { |
| 42 | +// const fqdn = await devStoreFqdn() |
| 43 | +// const url = `https://${fqdn}/dev_stores/unstable/organizations/${organizationId}/app_logs/poll` |
| 44 | +// return addCursorAndFiltersToAppLogsUrl(url, cursor, filters) |
| 45 | +// } |
| 46 | + |
| 47 | +export interface RequestOptions { |
| 48 | + requestMode: RequestModeInput |
| 49 | +} |
| 50 | + |
| 51 | +/** |
| 52 | + * @param organizationId - The organization ID. |
| 53 | + * @param query - GraphQL query to execute. |
| 54 | + * @param token - Partners token. |
| 55 | + * @param variables - GraphQL variables to pass to the query. |
| 56 | + * @param cacheOptions - Cache options for the request. If not present, the request will not be cached. |
| 57 | + * @param requestOptions - Preferred behaviour for the request. |
| 58 | + * @param unauthorizedHandler - Optional handler for unauthorized requests. |
| 59 | + */ |
| 60 | +export interface DevStoresRequestOptions<TResult, TVariables extends Variables> { |
| 61 | + query: TypedDocumentNode<TResult, TVariables> |
| 62 | + organizationId: string |
| 63 | + token: string |
| 64 | + variables?: TVariables |
| 65 | + cacheOptions?: CacheOptions |
| 66 | + requestOptions?: RequestOptions |
| 67 | + unauthorizedHandler: UnauthorizedHandler |
| 68 | +} |
| 69 | + |
| 70 | +/** |
| 71 | + * Executes an org-scoped GraphQL query against the App Management API. Uses typed documents. |
| 72 | + * |
| 73 | + * @param options - The options for the request. |
| 74 | + * @returns The response of the query of generic type <T>. |
| 75 | + */ |
| 76 | +export async function devStoresRequestDoc<TResult, TVariables extends Variables>( |
| 77 | + options: DevStoresRequestOptions<TResult, TVariables>, |
| 78 | +): Promise<TResult> { |
| 79 | + const cacheExtraKey = options.cacheOptions?.cacheExtraKey ?? '' |
| 80 | + const newCacheOptions = options.cacheOptions ? {...options.cacheOptions, cacheExtraKey} : undefined |
| 81 | + |
| 82 | + const result = limiter.schedule<TResult>(async () => |
| 83 | + graphqlRequestDoc<TResult, TVariables>({ |
| 84 | + ...(await setupRequest(options.token)), |
| 85 | + query: options.query, |
| 86 | + variables: options.variables, |
| 87 | + cacheOptions: newCacheOptions, |
| 88 | + preferredBehaviour: options.requestOptions?.requestMode, |
| 89 | + unauthorizedHandler: options.unauthorizedHandler, |
| 90 | + }), |
| 91 | + ) |
| 92 | + |
| 93 | + return result |
| 94 | +} |
| 95 | + |
| 96 | +interface Deprecation { |
| 97 | + supportedUntilDate?: string |
| 98 | +} |
| 99 | + |
| 100 | +interface WithDeprecations { |
| 101 | + deprecations: Deprecation[] |
| 102 | +} |
| 103 | + |
| 104 | +/** |
| 105 | + * Sets the next deprecation date from [GraphQL response extensions](https://www.apollographql.com/docs/resources/graphql-glossary/#extensions) |
| 106 | + * if `response.extensions.deprecations` objects contain a `supportedUntilDate` (ISO 8601-formatted string). |
| 107 | + * |
| 108 | + * @param response - The response of the query. |
| 109 | + */ |
| 110 | +export function handleDeprecations<T>(response: GraphQLResponse<T>): void { |
| 111 | + if (!response.extensions) return |
| 112 | + |
| 113 | + const deprecationDates: Date[] = [] |
| 114 | + for (const deprecation of (response.extensions as WithDeprecations).deprecations) { |
| 115 | + if (deprecation.supportedUntilDate) { |
| 116 | + deprecationDates.push(new Date(deprecation.supportedUntilDate)) |
| 117 | + } |
| 118 | + } |
| 119 | + |
| 120 | + setNextDeprecationDate(deprecationDates) |
| 121 | +} |
0 commit comments