///
import { SUPER_ADMIN_USER_IDENTIFIER, SUPER_ADMIN_USER_PASSWORD } from '@vendure/common/lib/shared-constants';
import { DocumentNode } from 'graphql';
import gql from 'graphql-tag';
import { print } from 'graphql/language/printer';
import fetch, { Response } from 'node-fetch';
import { Curl } from 'node-libcurl';
import { stringify } from 'querystring';
import { ImportInfo } from '../e2e/graphql/generated-e2e-admin-types';
import { getConfig } from '../src/config/config-helpers';
import { createUploadPostData } from './create-upload-post-data';
const LOGIN = gql`
mutation($username: String!, $password: String!) {
login(username: $username, password: $password) {
user {
id
identifier
channelTokens
}
}
}
`;
export type QueryParams = { [key: string]: string | number };
// tslint:disable:no-console
/**
* A minimalistic GraphQL client for populating and querying test data.
*/
export class SimpleGraphQLClient {
private authToken: string;
private channelToken: string;
private headers: { [key: string]: any } = {};
constructor(private apiUrl: string = '') {}
setAuthToken(token: string) {
this.authToken = token;
this.headers.Authorization = `Bearer ${this.authToken}`;
}
getAuthToken(): string {
return this.authToken;
}
setChannelToken(token: string) {
this.headers[getConfig().channelTokenKey] = token;
}
/**
* Performs both query and mutation operations.
*/
async query>(
query: DocumentNode,
variables?: V,
queryParams?: QueryParams,
): Promise {
const response = await this.request(query, variables, queryParams);
const result = await this.getResult(response);
if (response.ok && !result.errors && result.data) {
return result.data;
} else {
const errorResult = typeof result === 'string' ? { error: result } : result;
throw new ClientError(
{ ...errorResult, status: response.status },
{ query: print(query), variables },
);
}
}
async queryStatus>(query: DocumentNode, variables?: V): Promise {
const response = await this.request(query, variables);
return response.status;
}
importProducts(csvFilePath: string): Promise<{ importProducts: ImportInfo }> {
return this.fileUploadMutation({
mutation: gql`
mutation ImportProducts($csvFile: Upload!) {
importProducts(csvFile: $csvFile) {
imported
processed
errors
}
}
`,
filePaths: [csvFilePath],
mapVariables: () => ({ csvFile: null }),
});
}
async asUserWithCredentials(username: string, password: string) {
// first log out as the current user
if (this.authToken) {
await this.query(
gql`
mutation {
logout
}
`,
);
}
const result = await this.query(LOGIN, { username, password });
return result.login;
}
async asSuperAdmin() {
await this.asUserWithCredentials(SUPER_ADMIN_USER_IDENTIFIER, SUPER_ADMIN_USER_PASSWORD);
}
async asAnonymousUser() {
await this.query(
gql`
mutation {
logout
}
`,
);
}
private async request(
query: DocumentNode,
variables?: { [key: string]: any },
queryParams?: QueryParams,
): Promise {
const queryString = print(query);
const body = JSON.stringify({
query: queryString,
variables: variables ? variables : undefined,
});
const url = queryParams ? this.apiUrl + `?${stringify(queryParams)}` : this.apiUrl;
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...this.headers },
body,
});
const authToken = response.headers.get(getConfig().authOptions.authTokenHeaderKey || '');
if (authToken != null) {
this.setAuthToken(authToken);
}
return response;
}
private async getResult(response: Response): Promise {
const contentType = response.headers.get('Content-Type');
if (contentType && contentType.startsWith('application/json')) {
return response.json();
} else {
return response.text();
}
}
/**
* Uses curl to post a multipart/form-data request to the server. Due to differences between the Node and browser
* environments, we cannot just use an existing library like apollo-upload-client.
*
* Upload spec: https://github.com/jaydenseric/graphql-multipart-request-spec
* Discussion of issue: https://github.com/jaydenseric/apollo-upload-client/issues/32
*/
private fileUploadMutation(options: {
mutation: DocumentNode;
filePaths: string[];
mapVariables: (filePaths: string[]) => any;
}): Promise {
const { mutation, filePaths, mapVariables } = options;
return new Promise((resolve, reject) => {
const curl = new Curl();
const postData = createUploadPostData(mutation, filePaths, mapVariables);
const processedPostData = [
{
name: 'operations',
contents: JSON.stringify(postData.operations),
},
{
name: 'map',
contents:
'{' +
Object.entries(postData.map)
.map(([i, path]) => `"${i}":["${path}"]`)
.join(',') +
'}',
},
...postData.filePaths,
];
curl.setOpt(Curl.option.URL, this.apiUrl);
curl.setOpt(Curl.option.VERBOSE, false);
curl.setOpt(Curl.option.TIMEOUT_MS, 30000);
curl.setOpt(Curl.option.HTTPPOST, processedPostData);
curl.setOpt(Curl.option.HTTPHEADER, [
`Authorization: Bearer ${this.authToken}`,
`${getConfig().channelTokenKey}: ${this.channelToken}`,
]);
curl.perform();
curl.on('end', (statusCode: any, body: any) => {
curl.close();
const response = JSON.parse(body);
if (response.errors && response.errors.length) {
const error = response.errors[0];
console.log(JSON.stringify(error.extensions, null, 2));
throw new Error(error.message);
}
resolve(response.data);
});
curl.on('error', (err: any) => {
curl.close();
console.log(err);
reject(err);
});
});
}
}
export class ClientError extends Error {
constructor(public response: any, public request: any) {
super(ClientError.extractMessage(response));
}
private static extractMessage(response: any): string {
if (response.errors) {
return response.errors[0].message;
} else {
return `GraphQL Error (Code: ${response.status})`;
}
}
}