simple-graphql-client.ts 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. import { SUPER_ADMIN_USER_IDENTIFIER, SUPER_ADMIN_USER_PASSWORD } from '@vendure/common/lib/shared-constants';
  2. import { VendureConfig } from '@vendure/core';
  3. import { DocumentNode } from 'graphql';
  4. import gql from 'graphql-tag';
  5. import { print } from 'graphql/language/printer';
  6. import fetch, { Response } from 'node-fetch';
  7. import { Curl } from 'node-libcurl';
  8. import { stringify } from 'querystring';
  9. import { createUploadPostData } from './utils/create-upload-post-data';
  10. const LOGIN = gql`
  11. mutation($username: String!, $password: String!) {
  12. login(username: $username, password: $password) {
  13. user {
  14. id
  15. identifier
  16. channels {
  17. token
  18. }
  19. }
  20. }
  21. }
  22. `;
  23. export type QueryParams = { [key: string]: string | number };
  24. // tslint:disable:no-console
  25. /**
  26. * @description
  27. * A minimalistic GraphQL client for populating and querying test data.
  28. *
  29. * @docsCategory testing
  30. */
  31. export class SimpleGraphQLClient {
  32. private authToken: string;
  33. private channelToken: string | null = null;
  34. private headers: { [key: string]: any } = {};
  35. constructor(private vendureConfig: Required<VendureConfig>, private apiUrl: string = '') {}
  36. /**
  37. * @description
  38. * Sets the authToken to be used in each GraphQL request.
  39. */
  40. setAuthToken(token: string) {
  41. this.authToken = token;
  42. this.headers.Authorization = `Bearer ${this.authToken}`;
  43. }
  44. /**
  45. * @description
  46. * Sets the authToken to be used in each GraphQL request.
  47. */
  48. setChannelToken(token: string | null) {
  49. this.channelToken = token;
  50. this.headers[this.vendureConfig.channelTokenKey] = this.channelToken;
  51. }
  52. /**
  53. * @description
  54. * Returns the authToken currently being used.
  55. */
  56. getAuthToken(): string {
  57. return this.authToken;
  58. }
  59. /**
  60. * @description
  61. * Performs both query and mutation operations.
  62. */
  63. async query<T = any, V = Record<string, any>>(
  64. query: DocumentNode,
  65. variables?: V,
  66. queryParams?: QueryParams,
  67. ): Promise<T> {
  68. const response = await this.request(query, variables, queryParams);
  69. const result = await this.getResult(response);
  70. if (response.ok && !result.errors && result.data) {
  71. return result.data;
  72. } else {
  73. const errorResult = typeof result === 'string' ? { error: result } : result;
  74. throw new ClientError(
  75. { ...errorResult, status: response.status },
  76. { query: print(query), variables },
  77. );
  78. }
  79. }
  80. /**
  81. * @description
  82. * Performs a query or mutation and returns the resulting status code.
  83. */
  84. async queryStatus<T = any, V = Record<string, any>>(query: DocumentNode, variables?: V): Promise<number> {
  85. const response = await this.request(query, variables);
  86. return response.status;
  87. }
  88. /**
  89. * @description
  90. * Attemps to log in with the specified credentials.
  91. */
  92. async asUserWithCredentials(username: string, password: string) {
  93. // first log out as the current user
  94. if (this.authToken) {
  95. await this.query(
  96. gql`
  97. mutation {
  98. logout
  99. }
  100. `,
  101. );
  102. }
  103. const result = await this.query(LOGIN, { username, password });
  104. if (result.login.user.channels.length === 1) {
  105. this.setChannelToken(result.login.user.channels[0].token);
  106. }
  107. return result.login;
  108. }
  109. /**
  110. * @description
  111. * Logs in as the SuperAdmin user.
  112. */
  113. async asSuperAdmin() {
  114. await this.asUserWithCredentials(SUPER_ADMIN_USER_IDENTIFIER, SUPER_ADMIN_USER_PASSWORD);
  115. }
  116. /**
  117. * @description
  118. * Logs out so that the client is then treated as an anonymous user.
  119. */
  120. async asAnonymousUser() {
  121. await this.query(
  122. gql`
  123. mutation {
  124. logout
  125. }
  126. `,
  127. );
  128. }
  129. private async request(
  130. query: DocumentNode,
  131. variables?: { [key: string]: any },
  132. queryParams?: QueryParams,
  133. ): Promise<Response> {
  134. const queryString = print(query);
  135. const body = JSON.stringify({
  136. query: queryString,
  137. variables: variables ? variables : undefined,
  138. });
  139. const url = queryParams ? this.apiUrl + `?${stringify(queryParams)}` : this.apiUrl;
  140. const response = await fetch(url, {
  141. method: 'POST',
  142. headers: { 'Content-Type': 'application/json', ...this.headers },
  143. body,
  144. });
  145. const authToken = response.headers.get(this.vendureConfig.authOptions.authTokenHeaderKey || '');
  146. if (authToken != null) {
  147. this.setAuthToken(authToken);
  148. }
  149. return response;
  150. }
  151. private async getResult(response: Response): Promise<any> {
  152. const contentType = response.headers.get('Content-Type');
  153. if (contentType && contentType.startsWith('application/json')) {
  154. return response.json();
  155. } else {
  156. return response.text();
  157. }
  158. }
  159. /**
  160. * @description
  161. * Uses curl to post a multipart/form-data request to the server. Due to differences between the Node and browser
  162. * environments, we cannot just use an existing library like apollo-upload-client.
  163. *
  164. * Upload spec: https://github.com/jaydenseric/graphql-multipart-request-spec
  165. * Discussion of issue: https://github.com/jaydenseric/apollo-upload-client/issues/32
  166. */
  167. fileUploadMutation(options: {
  168. mutation: DocumentNode;
  169. filePaths: string[];
  170. mapVariables: (filePaths: string[]) => any;
  171. }): Promise<any> {
  172. const { mutation, filePaths, mapVariables } = options;
  173. return new Promise((resolve, reject) => {
  174. const curl = new Curl();
  175. const postData = createUploadPostData(mutation, filePaths, mapVariables);
  176. const processedPostData = [
  177. {
  178. name: 'operations',
  179. contents: JSON.stringify(postData.operations),
  180. },
  181. {
  182. name: 'map',
  183. contents:
  184. '{' +
  185. Object.entries(postData.map)
  186. .map(([i, path]) => `"${i}":["${path}"]`)
  187. .join(',') +
  188. '}',
  189. },
  190. ...postData.filePaths,
  191. ];
  192. curl.setOpt(Curl.option.URL, this.apiUrl);
  193. curl.setOpt(Curl.option.VERBOSE, false);
  194. curl.setOpt(Curl.option.TIMEOUT_MS, 30000);
  195. curl.setOpt(Curl.option.HTTPPOST, processedPostData);
  196. curl.setOpt(Curl.option.HTTPHEADER, [
  197. `Authorization: Bearer ${this.authToken}`,
  198. `${this.vendureConfig.channelTokenKey}: ${this.channelToken}`,
  199. ]);
  200. curl.perform();
  201. curl.on('end', (statusCode: any, body: any) => {
  202. curl.close();
  203. const response = JSON.parse(body);
  204. if (response.errors && response.errors.length) {
  205. const error = response.errors[0];
  206. console.log(JSON.stringify(error.extensions, null, 2));
  207. throw new Error(error.message);
  208. }
  209. resolve(response.data);
  210. });
  211. curl.on('error', (err: any) => {
  212. curl.close();
  213. console.log(err);
  214. reject(err);
  215. });
  216. });
  217. }
  218. }
  219. export class ClientError extends Error {
  220. constructor(public response: any, public request: any) {
  221. super(ClientError.extractMessage(response));
  222. }
  223. private static extractMessage(response: any): string {
  224. if (response.errors) {
  225. return response.errors[0].message;
  226. } else {
  227. return `GraphQL Error (Code: ${response.status})`;
  228. }
  229. }
  230. }