Browse and listen to thousands of radio stations across the globe right from your terminal ๐ŸŒŽ ๐Ÿ“ป ๐ŸŽตโœจ
radio rust tokio web-radio command-line-tool tui
at main 6.4 kB view raw
1// deno-lint-ignore-file no-explicit-any 2import { 3 ClientError, 4 gql, 5 GraphQLClient, 6 GraphQLRequestError, 7 TooManyNestedObjectsError, 8 UnknownDaggerError, 9 NotAwaitedRequestError, 10 ExecError, 11} from "../deps.ts"; 12 13import { Metadata, QueryTree } from "./client.gen.ts"; 14 15/** 16 * Format argument into GraphQL query format. 17 */ 18function buildArgs(args: any): string { 19 const metadata: Metadata = args.__metadata || {}; 20 21 // Remove unwanted quotes 22 const formatValue = (key: string, value: string) => { 23 // Special treatment for enumeration, they must be inserted without quotes 24 if (metadata[key]?.is_enum) { 25 return JSON.stringify(value).replace(/['"]+/g, ""); 26 } 27 28 return JSON.stringify(value).replace( 29 /\{"[a-zA-Z]+":|,"[a-zA-Z]+":/gi, 30 (str) => { 31 return str.replace(/"/g, ""); 32 } 33 ); 34 }; 35 36 if (args === undefined || args === null) { 37 return ""; 38 } 39 40 const formattedArgs = Object.entries(args).reduce( 41 (acc: any, [key, value]) => { 42 // Ignore internal metadata key 43 if (key === "__metadata") { 44 return acc; 45 } 46 47 if (value !== undefined && value !== null) { 48 acc.push(`${key}: ${formatValue(key, value as string)}`); 49 } 50 51 return acc; 52 }, 53 [] 54 ); 55 56 if (formattedArgs.length === 0) { 57 return ""; 58 } 59 60 return `(${formattedArgs})`; 61} 62 63/** 64 * Find QueryTree, convert them into GraphQl query 65 * then compute and return the result to the appropriate field 66 */ 67async function computeNestedQuery( 68 query: QueryTree[], 69 client: GraphQLClient 70): Promise<void> { 71 // Check if there is a nested queryTree to be executed 72 const isQueryTree = (value: any) => value["_queryTree"] !== undefined; 73 74 // Check if there is a nested array of queryTree to be executed 75 const isArrayQueryTree = (value: any[]) => 76 value.every((v) => v instanceof Object && isQueryTree(v)); 77 78 // Prepare query tree for final query by computing nested queries 79 // and building it with their results. 80 const computeQueryTree = async (value: any): Promise<string> => { 81 // Resolve sub queries if operation's args is a subquery 82 for (const op of value["_queryTree"]) { 83 await computeNestedQuery([op], client); 84 } 85 86 // push an id that will be used by the container 87 return buildQuery([ 88 ...value["_queryTree"], 89 { 90 operation: "id", 91 }, 92 ]); 93 }; 94 95 // Remove all undefined args and assert args type 96 const queryToExec = query.filter((q): q is Required<QueryTree> => !!q.args); 97 98 for (const q of queryToExec) { 99 await Promise.all( 100 // Compute nested query for single object 101 Object.entries(q.args).map(async ([key, value]: any) => { 102 if (value instanceof Object && isQueryTree(value)) { 103 // push an id that will be used by the container 104 const getQueryTree = await computeQueryTree(value); 105 106 q.args[key] = await compute(getQueryTree, client); 107 } 108 109 // Compute nested query for array of object 110 if (Array.isArray(value) && isArrayQueryTree(value)) { 111 const tmp: any = q.args[key]; 112 113 for (let i = 0; i < value.length; i++) { 114 // push an id that will be used by the container 115 const getQueryTree = await computeQueryTree(value[i]); 116 117 tmp[i] = await compute(getQueryTree, client); 118 } 119 120 q.args[key] = tmp; 121 } 122 }) 123 ); 124 } 125} 126 127/** 128 * Convert the queryTree into a GraphQL query 129 * @param q 130 * @returns 131 */ 132export function buildQuery(q: QueryTree[]): string { 133 const query = q.reduce((acc, { operation, args }, i) => { 134 const qLen = q.length; 135 136 acc += ` ${operation} ${args ? `${buildArgs(args)}` : ""} ${ 137 qLen - 1 !== i ? "{" : "}".repeat(qLen - 1) 138 }`; 139 140 return acc; 141 }, ""); 142 143 return `{${query} }`; 144} 145 146/** 147 * Convert querytree into a Graphql query then compute it 148 * @param q | QueryTree[] 149 * @param client | GraphQLClient 150 * @returns 151 */ 152export async function computeQuery<T>( 153 q: QueryTree[], 154 client: GraphQLClient 155): Promise<T> { 156 await computeNestedQuery(q, client); 157 158 const query = buildQuery(q); 159 160 return await compute(query, client); 161} 162 163/** 164 * Return a Graphql query result flattened 165 * @param response any 166 * @returns 167 */ 168export function queryFlatten<T>(response: any): T { 169 // Recursion break condition 170 // If our response is not an object or an array we assume we reached the value 171 if (!(response instanceof Object) || Array.isArray(response)) { 172 return response; 173 } 174 175 const keys = Object.keys(response); 176 177 if (keys.length != 1) { 178 // Dagger is currently expecting to only return one value 179 // If the response is nested in a way were more than one object is nested inside throw an error 180 throw new TooManyNestedObjectsError( 181 "Too many nested objects inside graphql response", 182 { response: response } 183 ); 184 } 185 186 const nestedKey = keys[0]; 187 188 return queryFlatten(response[nestedKey]); 189} 190 191/** 192 * Send a GraphQL document to the server 193 * return a flatten result 194 * @hidden 195 */ 196export async function compute<T>( 197 query: string, 198 client: GraphQLClient 199): Promise<T> { 200 let computeQuery: Awaited<T>; 201 try { 202 computeQuery = await client.request( 203 gql` 204 ${query} 205 ` 206 ); 207 } catch (e: any) { 208 if (e instanceof ClientError) { 209 const msg = e.response.errors?.[0]?.message ?? `API Error`; 210 const ext = e.response.errors?.[0]?.extensions; 211 212 if (ext?._type === "EXEC_ERROR") { 213 throw new ExecError(msg, { 214 cmd: (ext.cmd as string[]) ?? [], 215 exitCode: (ext.exitCode as number) ?? -1, 216 stdout: (ext.stdout as string) ?? "", 217 stderr: (ext.stderr as string) ?? "", 218 }); 219 } 220 221 throw new GraphQLRequestError(msg, { 222 request: e.request, 223 response: e.response, 224 cause: e, 225 }); 226 } 227 228 // Looking for connection error in case the function has not been awaited. 229 if (e.errno === "ECONNREFUSED") { 230 throw new NotAwaitedRequestError( 231 "Encountered an error while requesting data via graphql through a synchronous call. Make sure the function called is awaited.", 232 { cause: e } 233 ); 234 } 235 236 // Just throw the unknown error 237 throw new UnknownDaggerError( 238 "Encountered an unknown error while requesting data via graphql", 239 { cause: e } 240 ); 241 } 242 243 return queryFlatten(computeQuery); 244}