simple-graphql-client.ts 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. /// <reference types="../typings" />
  2. import { SUPER_ADMIN_USER_IDENTIFIER, SUPER_ADMIN_USER_PASSWORD } from '@vendure/common/lib/shared-constants';
  3. import { DocumentNode } from 'graphql';
  4. import { GraphQLClient } from 'graphql-request';
  5. import gql from 'graphql-tag';
  6. import { print } from 'graphql/language/printer';
  7. import { Curl } from 'node-libcurl';
  8. import { ImportInfo } from '../e2e/graphql/generated-e2e-admin-types';
  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<any> {
  51. return this.fileUploadMutation({
  52. mutation: gql`
  53. mutation CreateAssets($input: [CreateAssetInput!]!) {
  54. createAssets(input: $input) {
  55. id
  56. name
  57. }
  58. }
  59. `,
  60. filePaths,
  61. mapVariables: fp => ({
  62. input: fp.map(() => ({ file: null })),
  63. }),
  64. });
  65. }
  66. importProducts(csvFilePath: string): Promise<{ importProducts: ImportInfo }> {
  67. return this.fileUploadMutation({
  68. mutation: gql`
  69. mutation ImportProducts($csvFile: Upload!) {
  70. importProducts(csvFile: $csvFile) {
  71. imported
  72. processed
  73. errors
  74. }
  75. }
  76. `,
  77. filePaths: [csvFilePath],
  78. mapVariables: () => ({ csvFile: null }),
  79. });
  80. }
  81. /**
  82. * Uses curl to post a multipart/form-data request to the server. Due to differences between the Node and browser
  83. * environments, we cannot just use an existing library like apollo-upload-client.
  84. *
  85. * Upload spec: https://github.com/jaydenseric/graphql-multipart-request-spec
  86. * Discussion of issue: https://github.com/jaydenseric/apollo-upload-client/issues/32
  87. */
  88. private fileUploadMutation(options: {
  89. mutation: DocumentNode;
  90. filePaths: string[];
  91. mapVariables: (filePaths: string[]) => any;
  92. }): Promise<any> {
  93. const { mutation, filePaths, mapVariables } = options;
  94. return new Promise((resolve, reject) => {
  95. const curl = new Curl();
  96. const postData = createUploadPostData(mutation, filePaths, mapVariables);
  97. const processedPostData = [
  98. {
  99. name: 'operations',
  100. contents: JSON.stringify(postData.operations),
  101. },
  102. {
  103. name: 'map',
  104. contents:
  105. '{' +
  106. Object.entries(postData.map)
  107. .map(([i, path]) => `"${i}":["${path}"]`)
  108. .join(',') +
  109. '}',
  110. },
  111. ...postData.filePaths,
  112. ];
  113. curl.setOpt(Curl.option.URL, this.apiUrl);
  114. curl.setOpt(Curl.option.VERBOSE, false);
  115. curl.setOpt(Curl.option.TIMEOUT_MS, 30000);
  116. curl.setOpt(Curl.option.HTTPPOST, processedPostData);
  117. curl.setOpt(Curl.option.HTTPHEADER, [
  118. `Authorization: Bearer ${this.authToken}`,
  119. `${getConfig().channelTokenKey}: ${this.channelToken}`,
  120. ]);
  121. curl.perform();
  122. curl.on('end', (statusCode: any, body: any) => {
  123. curl.close();
  124. const response = JSON.parse(body);
  125. if (response.errors && response.errors.length) {
  126. const error = response.errors[0];
  127. console.log(JSON.stringify(error.extensions, null, 2));
  128. throw new Error(error.message);
  129. }
  130. resolve(response.data);
  131. });
  132. curl.on('error', (err: any) => {
  133. curl.close();
  134. console.log(err);
  135. reject(err);
  136. });
  137. });
  138. }
  139. async asUserWithCredentials(username: string, password: string) {
  140. // first log out as the current user
  141. if (this.authToken) {
  142. await this.query(
  143. gql`
  144. mutation {
  145. logout
  146. }
  147. `,
  148. );
  149. }
  150. const result = await this.query(
  151. gql`
  152. mutation($username: String!, $password: String!) {
  153. login(username: $username, password: $password) {
  154. user {
  155. id
  156. identifier
  157. channelTokens
  158. }
  159. }
  160. }
  161. `,
  162. {
  163. username,
  164. password,
  165. },
  166. );
  167. return result.login;
  168. }
  169. async asSuperAdmin() {
  170. await this.asUserWithCredentials(SUPER_ADMIN_USER_IDENTIFIER, SUPER_ADMIN_USER_PASSWORD);
  171. }
  172. async asAnonymousUser() {
  173. await this.query(
  174. gql`
  175. mutation {
  176. logout
  177. }
  178. `,
  179. );
  180. }
  181. private setHeaders() {
  182. const headers: any = {
  183. [getConfig().channelTokenKey]: this.channelToken,
  184. };
  185. if (this.authToken) {
  186. headers.Authorization = `Bearer ${this.authToken}`;
  187. }
  188. this.client.setHeaders(headers);
  189. }
  190. }