How to upload file using nestjs-graphql-fastify server and how to test such feature? - graphql

I struggle to upload .csv file to nestjs-graphql-fastify server. Tried following code:
#Mutation(() => Boolean)
async createUsers(
#Args({ name: 'file', type: () => GraphQLUpload })
{ createReadStream, filename }: FileUpload,
): Promise<boolean> {
try {
// backend logic . . .
} catch {
return false;
}
return true;
}
but all I get when testing with postman is this response:
{
"statusCode": 415,
"code": "FST_ERR_CTP_INVALID_MEDIA_TYPE",
"error": "Unsupported Media Type",
"message": "Unsupported Media Type: multipart/form-data; boundary=--------------------------511769018912715357993837"
}
Developing with code-first approach.
Update: Tried to use fastify-multipart but issue still remains. What has changed is response in postman:
POST body missing, invalid Content-Type, or JSON object has no keys.

Found some answer's on Nestjs discord channel.
You had to do following changes:
main.ts
async function bootstrap() {
const adapter = new FastifyAdapter();
const fastify = adapter.getInstance();
fastify.addContentTypeParser('multipart', (request, done) => {
request.isMultipart = true;
done();
});
fastify.addHook('preValidation', async function (request: any, reply) {
if (!request.raw.isMultipart) {
return;
}
request.body = await processRequest(request.raw, reply.raw);
});
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
adapter,
);
await app.listen(apiServerPort, apiServerHost);
}
bootstrap();
upload.scalar.ts
import { Scalar } from '#nestjs/graphql';
import { GraphQLUpload } from 'graphql-upload';
#Scalar('Upload')
export class UploadGraphQLScalar {
protected parseValue(value) {
return GraphQLUpload.parseValue(value);
}
protected serialize(value) {
return GraphQLUpload.serialize(value);
}
protected parseLiteral(ast) {
return GraphQLUpload.parseLiteral(ast, ast.value);
}
}
users.resolver.ts
#Mutation(() => CreateUsersOutput, {name: 'createUsers'})
async createUsers(
#Args('input', new ValidationPipe()) input: CreateUsersInput,
#ReqUser() reqUser: RequestUser,
): Promise<CreateUsersOutput> {
return this.usersService.createUsers(input, reqUser);
}
create-shared.input.ts
#InputType()
export class DataObject {
#Field(() => UploadGraphQLScalar)
#Exclude()
public readonly csv?: Promise<FileUpload>;
}
#InputType()
#ArgsType()
export class CreateUsersInput {
#Field(() => DataObject)
public readonly data: DataObject;
}
Also, I want to mention you should not use global validation pipes (in my case they made files unreadable)
// app.useGlobalPipes(new ValidationPipe({ transform: true }));

You could use graphql-python/gql to try to upload a file:
from gql import Client, gql
from gql.transport.aiohttp import AIOHTTPTransport
transport = AIOHTTPTransport(url='YOUR_URL')
client = Client(transport=transport)
query = gql('''
mutation($file: Upload!) {
createUsers(file: $file)
}
''')
with open("YOUR_FILE_PATH", "rb") as f:
params = {"file": f}
result = client.execute(
query, variable_values=params, upload_files=True
)
print(result)
If you activate logging, you can see some message exchanged between the client and the backend.

Related

Provide explicit type for the mutation GraphQL

I'm trying to create a mutation which will accept list of products. But doing so GraphQL is throwing error for createMultipleProducts method. Not sure what is the mistake here.
import { Inject } from "#nestjs/common";
import { Args, Mutation, Query, Resolver } from "#nestjs/graphql";
import { ClientProxy } from "#nestjs/microservices";
import { ProductRequest } from "src/types/ms-product/product.request.type";
import { ProductResponse } from "src/types/ms-product/product.response.type";
#Resolver(of => ProductResponse)
export class ProductResolver {
constructor(
#Inject('SERVICE__PRODUCT') private readonly clientServiceProduct: ClientProxy
) {}
#Mutation(returns => ProductResponse)
async createProduct(#Args('data') product: ProductRequest): Promise<ProductResponse> {
const PATTERN = {cmd: 'ms-product-create'};
const PAYLOAD = product;
return this.clientServiceProduct.send(PATTERN, PAYLOAD)
.toPromise()
.then((response: ProductResponse) => {
return response;
})
.catch((error) => {
return error;
})
}
#Mutation(returns => [ProductResponse])
async createMultipleProducts(#Args('data') products: [ProductRequest]): Promise<Array<ProductResponse>> {
try {
const PROMISES = products.map(async (product: ProductRequest) => {
const PATTERN = {cmd: 'ms-product-create'};
const PAYLOAD = product;
return await this.clientServiceProduct.send(PATTERN, PAYLOAD).toPromise();
});
return await Promise.all(PROMISES);
} catch (error) {
throw new Error(error);
}
}
#Query(returns => ProductResponse)
async readProduct(#Args('data') id: string) {
return {}
}
}
I'm getting this error:
UnhandledPromiseRejectionWarning: Error: Undefined type error. Make sure you are providing an explicit type for the "createMultipleProducts" (parameter at index [0]) of the "ProductResolver" class.
There is a need to inform GraphQL the type of the arguments explicitly if it is a complex object array.
#Mutation(returns => [ProductResponse])
async createMultipleProducts(#Args({ name: 'data', type: () => [ProductRequest] }) products: ProductRequest[]): Promise<Array<ProductResponse>> {
...
}
New format 2022
#Query(returns => ProductResponse)
async readProduct(#Args('data', () => String ) id: string) {
return {}
}

How to properly pass input values to a function using the composition api in vue?

I have an input field that contains a postcode. On submit I want to pass the postcode as an object to an axios request. I have created a CodeSandbox here: https://codesandbox.io/s/determined-beaver-8ebqc
The relevant code is:
App.vue
<template>
<div id="app">
<input v-model="postcode" type="text" placeholder="Enter Postcode">
<button #click="getAddress">Submit</button>
</div>
</template>
<script>
import useAxios from "#/composition/use-axios";
export default {
name: "App",
setup() {
const { sentData, response, fetchData } = useAxios(
"api/address/lookup-address",
"postcode",
"Failed Finding Address"
);
return {
postcode: sentData,
address: response,
getAddress: fetchData
};
}
};
</script>
use-axios.js
import { reactive, toRefs } from "#vue/composition-api";
import axios from "axios";
export default function (url, objectData, errorMessage) {
const state = reactive({
sentData: null,
response: null
});
const fetchData = async () => {
console.log("Sent Data:", state.sentData);
console.log("Response:", state.response);
console.log("URL:", url);
console.log("Object Data:", objectData);
console.log("Object:", { [objectData]: state.sentData });
console.log("Error Message:", errorMessage);
const config = { headers: { "Content-Type": "application/json" } };
try {
const res = await axios.post(url, [objectData]: state.sentData, config);
state.response = await res.data;
} catch (error) {
// Error handling stuff
}
}
return { ...toRefs(state), fetchData };
}
Converting the postcode input string to an object in this way seems very hacky. Also, this would get very messy if I needed to send multiple parameters to the axios request. Say if I want to pass { id: "1234", user: "Me" }, I would like to be able to construct that like:
sentData = { id: ${id}, user: ${user} }
But I'm not able to do this. What is the proper way to do this so that I can keep use-axios generic?
You will need to import ref, reactive and computed from the composition-api and then use them like this:
<script>
import useAxios from "#/composition/use-axios";
import { ref, reactive, computed } from "#vue/composition-api";
export default {
name: "App",
setup() {
let object = ref("");
let state = reactive({ postcode: "" });
const sentDataObject = computed(() => {
state.postcode = object;
return state;
});
const addressList = useAxios(
"api/address/lookup-address",
sentDataObject.value,
"Failed Finding Address"
);
return {
addresses: addressList.response,
postcode: object,
getAddress: addressList.fetchData
};
}
};
</script>
change use-axios.js to:
import { reactive, toRefs } from "#vue/composition-api";
import axios from "axios";
export default function (url, objectData, errorMessage) {
const state = reactive({
sentData: null,
response: null
});
const fetchData = async () => {
console.log("Sent Data:", state.sentData);
console.log("Response:", state.response);
console.log("URL:", url);
console.log("Object Data:", objectData);
console.log("Error Message:", errorMessage);
const config = { headers: { "Content-Type":
"application/json" } };
try {
const res = await axios.post(url, objectData, config);
state.response = await res.data;
} catch (error) {
// Error handling stuff
}
};
return { ...toRefs(state), fetchData };
}
See Codesandbox demo here: https://codesandbox.io/s/dawn-glade-ewzb7

Nest.js handling errors for HttpService

I'm trying to test NestJS's built in HttpService (which is based on Axios). I'm having trouble testing error/exception states though. In my test suite I have:
let client: SomeClearingFirmClient;
const mockConfigService = {
get: jest.fn((type) => {
switch(type) {
case 'someApiBaseUrl': {
return 'http://example.com'
}
case 'someAddAccountEndpoint': {
return '/ClientAccounts/Add';
}
case 'someApiKey': {
return 'some-api-key';
}
default:
return 'test';
}
}),
};
const successfulAdd: AxiosResponse = {
data: {
batchNo: '39cba402-bfa9-424c-b265-1c98204df7ea',
warning: '',
},
status: 200,
statusText: 'OK',
headers: {},
config: {},
};
const failAddAuth: AxiosError = {
code: '401',
config: {},
name: '',
message: 'Not Authorized',
}
const mockHttpService = {
post: jest.fn(),
get: jest.fn(),
}
it('Handles a failure', async () => {
expect.assertions(1);
mockHttpService.post = jest.fn(() => of(failAddAuth));
const module: TestingModule = await Test.createTestingModule({
providers: [
{
provide: ConfigService,
useValue: mockConfigService,
},
{
provide: HttpService,
useValue: mockHttpService,
},
SomeClearingFirmClient,
],
}).compile();
client = module.get<SomeClearingFirmClient>(SomeClearingFirmClient);
const payload = new SomeClearingPayload();
try {
await client.addAccount(payload);
} catch(e) {
console.log('e', e);
}
});
And my implementation is:
async addAccount(payload: any): Promise<SomeAddResponse> {
const addAccountEndpoint = this.configService.get('api.someAddAccountEndpoint');
const url = `${this.baseUrl}${addAccountEndpoint}?apiKey=${this.apiKey}`;
const config = {
headers: {
'Content-Type': 'application/json',
}
};
const response = this.httpService.post(url, payload, config)
.pipe(
map(res => {
return res.data;
}),
catchError(e => {
throw new HttpException(e.response.data, e.response.status);
}),
).toPromise().catch(e => {
throw new HttpException(e.message, e.code);
});
return response;
}
Regardless of whether I use Observables or Promises, I can't get anything to catch. 4xx level errors sail on through as a success. I feel like I remember Axios adding some sort of config option to reject/send an Observable error to subscribers on failures... but I could be imagining that. Am I doing something wrong in my test harness? The other StackOverflow posts I've seen seem to say that piping through catchError should do the trick, but my errors are going through the map operator.
Your mockHttpService seems to return no error, but a value:
mockHttpService.post = jest.fn(() => of(failAddAuth));
What of(failAddAuth) does is to emit a value(failAddAuth) and then complete.
That's why the catchError from this.httpService.post(url, payload, config) will never be reached, because no errors occur.
In order to make sure that catchError is hit, the observable returned by post() must emit an error notification.
You could try this:
// Something to comply with `HttpException`'s arguments
const err = { response: 'resp', status: '4xx' };
mockHttpService.post = jest.fn(() => throwError(err));
throwError(err) is the same as new Observable(s => s.error(err))(Source code).

Set the sequence of execution of asynchronous dispatch (redux\redux-saga)

I need to perform an asynchronous request. To do this, I'm debating a preloader, then I make a request, then I want to stop the preloader. The console should have: "show loader, load-app, hide loader", and output "show loader, hide loader, loading-app". How to save a sequence of calls?
How set the sequence of execution of asynchronous dispatch (redux\redux-saga)?
import { showLoader, hideLoader } from '../../reducer1'
import { authorizeToken } from '../reducer2'
async componentDidMount() {
const { dispatch } = this.props
const tokenLS = localStorage.getItem('token')
await dispatch(showLoader()); //show loader
await dispatch(authorizeToken(tokenLS)); // async request
await dispatch(hideLoader()); //hide loader
}
}
This code for Loader
import * as act from './actions'
const initialState = {
loadingPage: false
}
export const showLoader = () => {
console.log('show loader')
document.body.classList.add('loading-app')
return { type: act.startLoading }
}
export const hideLoader = () => {
console.log('hide loader')
document.body.classList.remove('loading-app')
return { type: act.finishLoading }
}
export default function loading(state = initialState, action) {
switch (action.type) {
case act.startLoading:
return { ...state, loadingPage: true }
case act.finishLoading:
return { ...state, loadingPage: false }
default:
return state
}
}
This code for async request:
function* authorizeWithToken({ payload: { token } }) {
try {
const { token:userToken } = yield call(authApi.authUserFromToken, token)
yield put({ type: AUTH_SUCCESS, payload: { token: userToken } })
yield console.log('end request')
} catch (error) {
throw new Error(`error request with token ${token}`)
}
}
export function* authorizeSaga() {
yield takeLatest(AUTH_REQUEST, authorize)
}
export function* authorizeWithTokenSaga() {
yield takeLatest(AUTH_REQUEST_TOKEN, authorizeWithToken)
}
This is reducer:
export const authorizeToken = (token) => ({
type: AUTH_REQUEST_TOKEN,
payload: {token}
})
I think there's some misunderstanding here. Your componentDidMount function does not need to by async. redux-saga enables you to move all async actions into sagas and out of the components. That way your components are easier to manage. I would change your componentDidMount to dispatch a single action and let your saga handle all the logic. Try changing it to this:
import { authorizeToken } from '../reducer2'
componentDidMount() {
const { dispatch } = this.props
const tokenLS = localStorage.getItem('token')
dispatch(authorizeToken(tokenLS)); // async request
}
Now in your saga, try this:
import { showLoader, hideLoader } from '../../reducer1'
function* authorizeWithToken({ payload: { token } }) {
try {
yield put(showLoader());
const { token:userToken } = yield call(authApi.authUserFromToken, token)
yield put({ type: AUTH_SUCCESS, payload: { token: userToken } })
yield put(hideLoader())
} catch (error) {
throw new Error(`error request with token ${token}`)
}
}
Now your saga will be in charge of displaying the loader, fetching the data, and hiding the loader. In that order.

rxjs first completes whole stream chain

I have a angular 5 app with the rxjs WebsocketSubject sending jsonrpc messages.
This is my sendRequest function
sendRequest(request: Request): Promise<Response> {
console.log(request);
this.socket.next(JSON.stringify(request));
return this.onResponse().filter((response: Response) => {
return response.id === request.id;
}).first().toPromise().then((response) => {
console.log(response);
if (response.error) {
console.log('error');
throw new RpcError(response.error);
}
return response;
});
}
I am using the first() operator to complete this filter subscription. But onResponse() comes directly from my WebsocketSubject and this will then be completed.
Are there any methods for decoupling the original subject?
Or should I create a new Observale.create(...)?
What happens with the written .filter function. Does it last anywhere or do I have to remove it anywhere preventing ever lasting filter calls?
Edit 1
Also using this does not help.
sendRequest(request: Request): Promise<Response> {
console.log(request);
this.socket.next(JSON.stringify(request));
return new Promise<Response>((resolve, reject) => {
const responseSubscription = this.onResponse().filter((response: Response) => {
console.log('filter');
return response.id === request.id;
}).subscribe((response: Response) => {
// responseSubscription.unsubscribe();
resolve(response);
});
});
}
If I execute the unsubscribe the whole websocketSubject is closed. Not doing so logs 'filter' on time more per request !!
Edit 2
Here is the whole websocketService i have written
import {Injectable} from "#angular/core";
import {WebSocketSubject, WebSocketSubjectConfig} from "rxjs/observable/dom/WebSocketSubject";
import {MessageFactory, Notification, Request, Response, RpcError} from "../misc/jsonrpc";
import {ReplaySubject} from "rxjs/ReplaySubject";
import {Observable} from "rxjs/Observable";
import 'rxjs/add/operator/toPromise';
import 'rxjs/add/operator/filter';
import 'rxjs/add/operator/first';
import 'rxjs/add/observable/from';
export enum ConnectionState {
CONNECTED = "Connected",
CONNECTING = "Connecting",
CLOSING = "Closing",
DISCONNECTED = "Disconnected"
}
#Injectable()
export class WebsocketService {
private connectionState = new ReplaySubject<ConnectionState>(1);
private socket: WebSocketSubject<ArrayBuffer | Object>;
private config: WebSocketSubjectConfig;
constructor() {
console.log('ctor');
const protocol = location.protocol === 'https' ? 'wss' : 'ws';
const host = location.hostname;
const port = 3000; // location.port;
this.config = {
binaryType: "arraybuffer",
url: `${protocol}://${host}:${port}`,
openObserver: {
next: () => this.connectionState.next(ConnectionState.CONNECTED)
},
closingObserver: {
next: () => this.connectionState.next(ConnectionState.CLOSING)
},
closeObserver: {
next: () => this.connectionState.next(ConnectionState.DISCONNECTED)
},
resultSelector: (e: MessageEvent) => {
try {
if (e.data instanceof ArrayBuffer) {
return e.data;
} else {
return JSON.parse(e.data);
}
} catch (e) {
console.error(e);
return null;
}
}
};
this.connectionState.next(ConnectionState.CONNECTING);
this.socket = new WebSocketSubject(this.config);
this.connectionState.subscribe((state) => {
console.log(`WS state ${state}`);
});
}
onBinaryData(): Observable<ArrayBuffer> {
return this.socket.filter((message: any) => {
return message instanceof ArrayBuffer;
});
}
onMessageData(): Observable<Object> {
return this.socket.filter((message: any) => {
return !(message instanceof ArrayBuffer);
});
}
onResponse(): Observable<Response> {
return this.onMessageData().filter((message) => {
return MessageFactory.from(message).isResponse();
}).map((message): Response => {
return MessageFactory.from(message).toResponse();
});
}
sendRequest(request: Request): Promise<Response> {
console.log(request);
this.socket.next(JSON.stringify(request));
return new Promise<Response>((resolve, reject) => {
const responseSubscription = this.onResponse().filter((response: Response) => {
console.log('filter');
return response.id === request.id;
}).subscribe((response: Response) => {
responseSubscription.unsubscribe();
resolve(response);
});
});
}
sendNotification(notification: Notification): void {
this.socket.next(JSON.stringify(notification));
}
}
And the result in my log
Using Angular 5.0.2
websocket.service.ts:27 ctor
websocket.service.ts:69 WS state Connecting
core.js:3565 Angular is running in the development mode. Call enableProdMode() to enable the production mode.
websocket.service.ts:96 Request {jsonrpc: "2.0", id: "b042005c-5fbf-5ffc-fbd1-df68fae5882e", method: "appointment_list_get", params: undefined}
websocket.service.ts:69 WS state Connected
websocket.service.ts:103 filter
websocket.service.ts:69 WS state Disconnected
I need to find a way decoupling my filter from the original stream somehow.
This is working.
The key was to decouple the message handling from the underlaying websocketSubject.
import {Injectable} from "#angular/core";
import {WebSocketSubject, WebSocketSubjectConfig} from "rxjs/observable/dom/WebSocketSubject";
import {MessageFactory, Notification, Request, Response, RpcError} from "../misc/jsonrpc";
import {ReplaySubject} from "rxjs/ReplaySubject";
import {Observable} from "rxjs/Observable";
import 'rxjs/add/operator/toPromise';
import 'rxjs/add/operator/filter';
import 'rxjs/add/operator/first';
import 'rxjs/add/observable/from';
import {Subject} from "rxjs/Subject";
export enum ConnectionState {
CONNECTED = "Connected",
CONNECTING = "Connecting",
CLOSING = "Closing",
DISCONNECTED = "Disconnected"
}
#Injectable()
export class WebsocketService {
private connectionState = new ReplaySubject<ConnectionState>(1);
private socket: WebSocketSubject<ArrayBuffer | Object>;
private config: WebSocketSubjectConfig;
private messageObserver = new Subject<MessageFactory>();
private binaryObserver = new Subject<ArrayBuffer>();
constructor() {
const protocol = location.protocol === 'https' ? 'wss' : 'ws';
const host = location.hostname;
const port = 3000; // location.port;
this.config = {
binaryType: "arraybuffer",
url: `${protocol}://${host}:${port}`,
openObserver: {
next: () => this.connectionState.next(ConnectionState.CONNECTED)
},
closingObserver: {
next: () => this.connectionState.next(ConnectionState.CLOSING)
},
closeObserver: {
next: () => this.connectionState.next(ConnectionState.DISCONNECTED)
},
resultSelector: (e: MessageEvent) => {
try {
if (e.data instanceof ArrayBuffer) {
return e.data;
} else {
return JSON.parse(e.data);
}
} catch (e) {
console.error(e);
return null;
}
}
};
this.connectionState.next(ConnectionState.CONNECTING);
this.socket = new WebSocketSubject(this.config);
this.socket.filter((message: any) => {
return message instanceof ArrayBuffer;
}).subscribe((message: ArrayBuffer) => {
this.binaryObserver.next(message);
});
this.socket.filter((message: any) => {
return !(message instanceof ArrayBuffer);
}).subscribe((message: ArrayBuffer) => {
this.messageObserver.next(MessageFactory.from(message));
});
this.connectionState.subscribe((state) => {
console.log(`WS state ${state}`);
});
}
onResponse(): Observable<Response> {
return this.messageObserver.filter((message: MessageFactory) => {
return message.isResponse();
}).map((message: MessageFactory): Response => {
return message.toResponse();
});
}
sendRequest(request: Request): Promise<Response> {
console.log(request);
this.socket.next(JSON.stringify(request));
return this.onResponse().filter((response: Response) => {
return request.id === response.id;
}).first().toPromise().then((response) => {
console.log(response);
if (response.error) {
console.log('error');
throw new RpcError(response.error);
}
return response;
});
}
sendNotification(notification: Notification): void {
this.socket.next(JSON.stringify(notification));
}
}

Resources