Browse and listen to thousands of radio stations across the globe right from your terminal ๐ ๐ป ๐ตโจ
radio
rust
tokio
web-radio
command-line-tool
tui
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}