simple-graphql-client.ts 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. import { DocumentNode } from 'graphql';
  2. import { GraphQLClient } from 'graphql-request';
  3. import gql from 'graphql-tag';
  4. import { print } from 'graphql/language/printer';
  5. import { Curl } from 'node-libcurl';
  6. import { CREATE_ASSETS } from '../../admin-ui/src/app/data/definitions/product-definitions';
  7. import { CreateAssets, ImportInfo } from '../../shared/generated-types';
  8. import { SUPER_ADMIN_USER_IDENTIFIER, SUPER_ADMIN_USER_PASSWORD } from '../../shared/shared-constants';
  9. import { getConfig } from '../src/config/config-helpers';
  10. import { createUploadPostData } from './create-upload-post-data';
  11. // tslint:disable:no-console
  12. /**
  13. * A minimalistic GraphQL client for populating and querying test data.
  14. */
  15. export class SimpleGraphQLClient {
  16. private client: GraphQLClient;
  17. private authToken: string;
  18. private channelToken: string;
  19. constructor(private apiUrl: string = '') {
  20. this.client = new GraphQLClient(apiUrl);
  21. }
  22. setAuthToken(token: string) {
  23. this.authToken = token;
  24. this.setHeaders();
  25. }
  26. getAuthToken(): string {
  27. return this.authToken;
  28. }
  29. setChannelToken(token: string) {
  30. this.channelToken = token;
  31. this.setHeaders();
  32. }
  33. /**
  34. * Performs both query and mutation operations.
  35. */
  36. async query<T = any, V = Record<string, any>>(query: DocumentNode, variables?: V): Promise<T> {
  37. const queryString = print(query);
  38. const result = await this.client.rawRequest<T>(queryString, variables);
  39. const authToken = result.headers.get(getConfig().authOptions.authTokenHeaderKey);
  40. if (authToken != null) {
  41. this.setAuthToken(authToken);
  42. }
  43. return result.data as T;
  44. }
  45. async queryStatus<T = any, V = Record<string, any>>(query: DocumentNode, variables?: V): Promise<number> {
  46. const queryString = print(query);
  47. const result = await this.client.rawRequest<T>(queryString, variables);
  48. return result.status;
  49. }
  50. uploadAssets(filePaths: string[]): Promise<CreateAssets.Mutation> {
  51. return this.fileUploadMutation({
  52. mutation: CREATE_ASSETS,
  53. filePaths,
  54. mapVariables: fp => ({
  55. input: fp.map(() => ({ file: null })),
  56. }),
  57. });
  58. }
  59. importProducts(csvFilePath: string): Promise<{ importProducts: ImportInfo }> {
  60. return this.fileUploadMutation({
  61. mutation: gql`
  62. mutation ImportProducts($csvFile: Upload!) {
  63. importProducts(csvFile: $csvFile) {
  64. imported
  65. processed
  66. errors
  67. }
  68. }
  69. `,
  70. filePaths: [csvFilePath],
  71. mapVariables: () => ({ csvFile: null }),
  72. });
  73. }
  74. /**
  75. * Uses curl to post a multipart/form-data request to the server. Due to differences between the Node and browser
  76. * environments, we cannot just use an existing library like apollo-upload-client.
  77. *
  78. * Upload spec: https://github.com/jaydenseric/graphql-multipart-request-spec
  79. * Discussion of issue: https://github.com/jaydenseric/apollo-upload-client/issues/32
  80. */
  81. private fileUploadMutation(options: {
  82. mutation: DocumentNode;
  83. filePaths: string[];
  84. mapVariables: (filePaths: string[]) => any;
  85. }): Promise<any> {
  86. const { mutation, filePaths, mapVariables } = options;
  87. return new Promise((resolve, reject) => {
  88. const curl = new Curl();
  89. const postData = createUploadPostData(mutation, filePaths, mapVariables);
  90. const processedPostData = [
  91. {
  92. name: 'operations',
  93. contents: JSON.stringify(postData.operations),
  94. },
  95. {
  96. name: 'map',
  97. contents:
  98. '{' +
  99. Object.entries(postData.map)
  100. .map(([i, path]) => `"${i}":["${path}"]`)
  101. .join(',') +
  102. '}',
  103. },
  104. ...postData.filePaths,
  105. ];
  106. curl.setOpt(Curl.option.URL, this.apiUrl);
  107. curl.setOpt(Curl.option.VERBOSE, false);
  108. curl.setOpt(Curl.option.TIMEOUT_MS, 30000);
  109. curl.setOpt(Curl.option.HTTPPOST, processedPostData);
  110. curl.setOpt(Curl.option.HTTPHEADER, [
  111. `Authorization: Bearer ${this.authToken}`,
  112. `${getConfig().channelTokenKey}: ${this.channelToken}`,
  113. ]);
  114. curl.perform();
  115. curl.on('end', (statusCode, body) => {
  116. curl.close();
  117. const response = JSON.parse(body);
  118. if (response.errors && response.errors.length) {
  119. const error = response.errors[0];
  120. console.log(JSON.stringify(error.extensions, null, 2));
  121. throw new Error(error.message);
  122. }
  123. resolve(response.data);
  124. });
  125. curl.on('error', err => {
  126. curl.close();
  127. console.log(err);
  128. reject(err);
  129. });
  130. });
  131. }
  132. async asUserWithCredentials(username: string, password: string) {
  133. // first log out as the current user
  134. if (this.authToken) {
  135. await this.query(
  136. gql`
  137. mutation {
  138. logout
  139. }
  140. `,
  141. );
  142. }
  143. const result = await this.query(
  144. gql`
  145. mutation($username: String!, $password: String!) {
  146. login(username: $username, password: $password) {
  147. user {
  148. id
  149. identifier
  150. channelTokens
  151. }
  152. }
  153. }
  154. `,
  155. {
  156. username,
  157. password,
  158. },
  159. );
  160. }
  161. async asSuperAdmin() {
  162. await this.asUserWithCredentials(SUPER_ADMIN_USER_IDENTIFIER, SUPER_ADMIN_USER_PASSWORD);
  163. }
  164. async asAnonymousUser() {
  165. await this.query(
  166. gql`
  167. mutation {
  168. logout
  169. }
  170. `,
  171. );
  172. }
  173. private setHeaders() {
  174. const headers: any = {
  175. [getConfig().channelTokenKey]: this.channelToken,
  176. };
  177. if (this.authToken) {
  178. headers.Authorization = `Bearer ${this.authToken}`;
  179. }
  180. this.client.setHeaders(headers);
  181. }
  182. }