1
0

simple-graphql-client.ts 8.2 KB

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