I have a JHipster gateway+microservice application. I have added a spring service with jhipster spring-controller and then edited the code like this:
#RestController
#RequestMapping("/api/data")
public class DataResource {
/**
* GET vin
*/
#GetMapping("/vin")
public ResponseEntity<Object> vin(#Valid #RequestBody String address) {
Chart3DataDTO[] data=new Chart3DataDTO[15];
for (int i=0;i<15;i++){
data[i]=new Chart3DataDTO(System.currentTimeMillis()+i, 200+i, 201+i, 202+i);
}
return ResponseEntity.ok(data);
}
For completeness, this is the DTO
public class Chart3DataDTO {
private Long xAxis;
private Integer[] yAxis=new Integer[3];
public Chart3DataDTO(Long xAxis, Integer yAxis1, Integer yAxis2, Integer yAxis3) {
this.xAxis = xAxis;
this.yAxis = new Integer[]{yAxis1, yAxis2, yAxis3};
}
public Long getxAxis() {
return xAxis;
}
public Integer[] getyAxis() {
return yAxis;
}
}
Then I have dockerized gateway and microservice, jhipster docker-compose and started all. Everything works but when the Angular frontent asks for /api/data/vin I get:
if not logged in: 401 (which is fine)
if logged in: the JHipster page 'an error has occurred', instead of returning the JSON of the DTO
What did I miss?
Also, it doesn't appear listed on the Jhipster registry API
2ND EDIT: Added client angular code
import { Injectable } from '#angular/core';
import { Observable, of, throwError } from 'rxjs';
import { HttpClient, HttpHeaders, HttpErrorResponse } from '#angular/common/http';
import { catchError, tap, map } from 'rxjs/operators';
const httpOptions = {
headers: new HttpHeaders({ 'Content-Type': 'application/json',
'Access-Control-Allow-Origin':'*'
})
};
//const apiUrl = 'api/vin';
const apiUrl = '/api/data/vin';
#Injectable({
providedIn: 'root'
})
export class ApiService {
constructor(private http: HttpClient) {}
/*
private handleError<T> (operation = 'operation', result?: T) {
return (error: any): Observable<T> => {
// TODO: send the error to remote logging infrastructure
console.error(error); // log to console instead
// Let the app keep running by returning an empty result.
return of(result as T);
};
}
*/
getInputVoltage(address: String): Observable<[]> {
return this.http.get<[]>(`${apiUrl}` + '?address=' + address,httpOptions);
}
}
And
import { Component, OnInit } from '#angular/core';
import * as Highcharts from 'highcharts';
import { ApiService } from '../api.service';
#Component({
selector: 'jhi-device-graph',
templateUrl: './device-graph.component.html',
styleUrls: ['./device-graph.component.scss']
})
export class DeviceGraphComponent implements OnInit {
Highcharts: typeof Highcharts = Highcharts;
chartOptions: Highcharts.Options = {
xAxis: {
type: 'datetime'
},
yAxis: {
title: {
text: 'Voltage'
}
},
title: {
text: 'Input voltage'
},
series: [
{
data: [
[Date.UTC(2010, 0, 1), 29.9],
[Date.UTC(2010, 2, 1), 71.5],
[Date.UTC(2010, 3, 1), 106.4]
],
type: 'line',
name: 'Vin1'
},
{
data: [
[Date.UTC(2010, 0, 1), 39.9],
[Date.UTC(2010, 2, 1), 91.5],
[Date.UTC(2010, 3, 1), 96.4]
],
type: 'line',
name: 'Vin2'
}
]
};
data: String[] = [];
isLoadingResults = true;
constructor(private api: ApiService) {}
ngOnInit(): void {
this.api.getInputVoltage('10.1.30.1').subscribe(
(res: any) => {
this.data = res;
//console.log(this.data);
this.isLoadingResults = false;
},
err => {
//console.log(err);
this.isLoadingResults = false;
}
);
}
}
Your angular client sends a GET request on /api/data/vin with query parameters while your REST controller expects a request body, this can't work.
Your controller must expect a #RequestParam
Also, as the request goes through a gateway, it must be prefixed by /services and your service name, so in your case the URL is /services/graph/api/data/vin.
Also using #Valid on a String does not do anything unless you add some other validation annotations like #NotBlank or #Size(max=30)
#GetMapping("/vin")
public ResponseEntity<Object> vin(#Valid #RequestParam String address) {
#RequestBody must be used only for POST or PUT.
Assuming you are sending a parameter when you make the get call (seeing that #RequestBody address) and without seeing the logs, you can try changing the ResponseEntity<Object> to ResponseEntity<Chart3DataDTO[]>
Related
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.
I have a hard time passing the right angular request to the header. This is my service:
import { Injectable } from '#angular/core';
import { HttpClient, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, HttpHeaders }
from '#angular/common/http';
import { Utente } from '../model/Utente ';
import { Prodotto } from '../model/Prodotto ';
import { OktaAuthService } from '#okta/okta-angular';
import { Observable, from } from 'rxjs';
import { Carrello } from '../model/Carrello ';
import { userInfo } from 'node:os';
import { getLocaleCurrencyCode } from '#angular/common';
const headers = new HttpHeaders().set('Accept', 'application/json');
#Injectable({
providedIn: 'root'
})
export class HttpClientService {
constructor(
private httpClient:HttpClient, private oktaAuth:OktaAuthService ) {}
getCarr(){
return this.httpClient.get<Carrello[]>('http://localhost:8080/prodotti/utente/vedicarrelloo', {headers} );
}
}
This is my spring method:
#Transactional(readOnly = true)
public List<Carrello> getCarrello(#AuthenticationPrincipal OidcUser utente){
Utente u= utenteRepository.findByEmail(utente.getEmail());
return carrelloRepository.findByUtente(u);
}
In console I get this error (error 500):
https://i.stack.imgur.com/BiONS.png
this error corresponds in my console to "java.lang.NullPointerException: null.
But if I access localhost: 8080, I can see the answer correctly, so I assume there is a problem in passing the request header in angular, can anyone tell me where am I wrong, please? I specify that I get this error only in the methods where the OidcUser is present, the rest works perfectly. Thank you!
You need to send an access token with your request. Like this:
import { Component, OnInit } from '#angular/core';
import { OktaAuthService } from '#okta/okta-angular';
import { HttpClient } from '#angular/common/http';
import sampleConfig from '../app.config';
interface Message {
date: string;
text: string;
}
#Component({
selector: 'app-messages',
templateUrl: './messages.component.html',
styleUrls: ['./messages.component.css']
})
export class MessagesComponent implements OnInit {
failed: Boolean;
messages: Array<Message> [];
constructor(public oktaAuth: OktaAuthService, private http: HttpClient) {
this.messages = [];
}
async ngOnInit() {
const accessToken = await this.oktaAuth.getAccessToken();
this.http.get(sampleConfig.resourceServer.messagesUrl, {
headers: {
Authorization: 'Bearer ' + accessToken,
}
}).subscribe((data: any) => {
let index = 1;
const messages = data.messages.map((message) => {
const date = new Date(message.date);
const day = date.toLocaleDateString();
const time = date.toLocaleTimeString();
return {
date: `${day} ${time}`,
text: message.text,
index: index++
};
});
[].push.apply(this.messages, messages);
}, (err) => {
console.error(err);
this.failed = true;
});
}
}
On the Spring side, if you want it to accept a JWT, you'll need to change to use Jwt instead of OidcUser. Example here.
#GetMapping("/")
public String index(#AuthenticationPrincipal Jwt jwt) {
return String.format("Hello, %s!", jwt.getSubject());
}
I am using navigateTo to open a page with listview and would like to pass the results back using navigateBack but unable to achieve that. Any idea?
With Service class and Observable, you can achieve this.
notify.service.ts
import { Injectable } from '#angular/core';
import { Subject } from 'rxjs-compat/Subject';
#Injectable({
providedIn: 'root'
})
export class NotifyService {
private refreshDataForView = new Subject<any>();
refreshDataForParentViewObservable$ = this.refreshDataForView.asObservable();
public relaodDataForParentView(data: any) {
if (data) {
this.refreshDataForView.next(data);
}
}
}
Second componenet.ts
constructor(
private notifyService: NotifyService
) { }
goBack() {
this.notifyService.relaodDataForParentView({ data: 'any data you wanrt to pass here ' });
this.router.back();
}
First component.ts
reloadDataSubscription: any;
constructor(
private notifyService: NotifyService
) {}
ngOnInit() {
this.reloadDataSubscription = this.notifyService.refreshDataForParentViewObservable$
.subscribe((res) => {
console.log('======', res);
// do what you want to do with the data passed from second view
});
}
I spent many hours without success. I know it's a common problem, many solutions but for me works only Interceptor that I want to avoid.
My service - here I get email with plus like john.doe+100#gmail.com
#Injectable({
providedIn: 'root',
})
export class UsersHttpService {
httpParams = new HttpParams({encoder: new CustomEncoder()});
removeUsersFromGroup(groupId: string, email: string): Observable<any> {
console.log(email); //john.doe+100#gmail.com
let parsedEmail = encodeURI(email); //one of many attempts
return this.http.delete(`${this.env.URI}/monitoring/api/v1/groups/${groupId}/users/`, {
params: {
groupId,
email: email.replace(' ', '+')
},
});
}
And my CustomEncoder:
export class CustomEncoder implements HttpParameterCodec {
encodeKey(key: string): string {
return encodeURIComponent(key);
}
encodeValue(value: string): string {
// console.log('encodeValue encodeValue');
// console.log(value);
// console.log(encodeURIComponent(value));
return encodeURIComponent(value);
}
decodeKey(key: string): string {
return decodeURIComponent(key);
}
decodeValue(value: string): string {
// console.log('decodeValue decodeValue');
// console.log(value);
// console.log(decodeURIComponent(value));
return decodeURIComponent(value);
}
}
When I send request from Angular then in the Network tab in web browser I see:
DELETE https://myapp/groups/d39a4f50-8ebd-11ea-a9ae-5103b15ad73b/users/?groupId=d39a4f50-8ebd-11ea-a9ae-5103b15ad73b&email=john.doe 100#gmail.com
with a space! What's wrong? Were is the problem? IN the console I get email with + but in Network tab without space instead of + sign.
My params are properly encoded (there is 200 status from backend (spring boot), email with +) ONLY when I use global interceptor (which should be avoided):
import {
HttpEvent,
HttpHandler,
HttpInterceptor,
HttpParams,
HttpRequest,
} from "#angular/common/http";
import {Injectable} from "#angular/core";
import {Observable} from "rxjs";
import {CustomEncoder} from "./customEncoder";
#Injectable()
export class EncodeHttpParamsInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const params = new HttpParams({
encoder: new CustomEncoder(),
fromString: req.params.toString(),
});
return next.handle(req.clone({params}));
}
}
Does anyone have any idea??? I tried to use:
return this.http.delete(${this.env.ORBITAL_URI}/monitoring/api/v1/groups/${groupId}/users/, {
params: {
groupId,
email: encodeURI(email) //or encodeURIComponent(email)
},
});
and then in Network tab I see something like john.doe%2B%40gmail.com but I get 500 error from backend
My solution - without any interceptor:
removeUsersFromGroup(groupId: string, email: string): Observable<any> {
const params = new HttpParams({
encoder: new CustomEncoder(),
fromObject: {
groupId,
email,
},
});
return this.http.delete(`${this.env.URI}/myapp/v1/groups/${groupId}/users/`, {
params: params,
});
}
Now it works as expected:)
I'm trying to get a sockjs + stomp connection to my spring boot websockets. This is my configuration:
#Configuration
#EnableWebSocketMessageBroker
public class WebsocketConfiguration extends AbstractWebSocketMessageBrokerConfigurer {
private final String MESSAGE_BROKER_PREFIX = "/topic";
private final String WEBSOCKET_PREFIX = "/sockjs-node";
private final String REQUEST_PREFIX = "/";
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint(WEBSOCKET_PREFIX)
.withSockJS();
}
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker(MESSAGE_BROKER_PREFIX);
config.setApplicationDestinationPrefixes(REQUEST_PREFIX);
}
}
And and my endpoint definition:
#Controller
public class Foo {
#SubscribeMapping("/{pipelineId}/{topic}")
private void subscribe(
HttpSession session,
#PathVariable String pipelineId,
#PathVariable String topic
) {
System.out.println(session.getId());
}
#EventListener
public void onApplicationEvent(SessionConnectEvent event) {
System.out.println(event.getSource());
}
#EventListener
public void onApplicationEvent(SessionDisconnectEvent event) {
System.out.println(event.getSessionId());
}
}
And from the javascript side:
var ws = new SockJS('/sockjs-node');
var client = Stomp.over(ws);
var subscription = client.subscribe("/topic/foo/bar", () => {
console.log("asdas");
});
but the connection does not happen and none of the methods get invoked. In the javascript console I can see:
>>> SUBSCRIBE
id:sub-0
destination:/topic/lala
stomp.js:199 Uncaught TypeError: Cannot read property 'send' of undefined
at Client._transmit (webpack:///./node_modules/#stomp/stompjs/lib/stomp.js?:199:26)
at Client.subscribe (webpack:///./node_modules/#stomp/stompjs/lib/stomp.js?:468:12)
at Object.eval (webpack:///./src/index.js?:128:27)
I am able to connect using wscat --connect ws://localhost:8080/sockjs-node/902/phebsu4o/websocket, but interestingly enough only the disconnect handler gets invoked and the connect handler doesn't. What am I missing here?
I found a js client which actually works on github.
import React from "react";
import SockJS from "sockjs-client";
import Stomp from "stompjs";
import PropTypes from "prop-types";
class SockJsClient extends React.Component {
static defaultProps = {
onConnect: () => {},
onDisconnect: () => {},
getRetryInterval: (count) => {return 1000 * count;},
headers: {},
autoReconnect: true,
debug: false
}
static propTypes = {
url: PropTypes.string.isRequired,
topics: PropTypes.array.isRequired,
onConnect: PropTypes.func,
onDisconnect: PropTypes.func,
getRetryInterval: PropTypes.func,
onMessage: PropTypes.func.isRequired,
headers: PropTypes.object,
autoReconnect: PropTypes.bool,
debug: PropTypes.bool
}
constructor(props) {
super(props);
this.state = {
connected: false
};
this.subscriptions = new Map();
this.retryCount = 0;
}
componentDidMount() {
this.connect();
}
componentWillUnmount() {
this.disconnect();
}
render() {
return (<div></div>);
}
_initStompClient = () => {
// Websocket held by stompjs can be opened only once
this.client = Stomp.over(new SockJS(this.props.url));
if (!this.props.debug) {
this.client.debug = () => {};
}
}
_cleanUp = () => {
this.setState({ connected: false });
this.retryCount = 0;
this.subscriptions.clear();
}
_log = (msg) => {
if (this.props.debug) {
console.log(msg);
}
}
connect = () => {
this._initStompClient();
this.client.connect(this.props.headers, () => {
this.setState({ connected: true });
this.props.topics.forEach((topic) => {
this.subscribe(topic);
});
this.props.onConnect();
}, (error) => {
if (this.state.connected) {
this._cleanUp();
// onDisconnect should be called only once per connect
this.props.onDisconnect();
}
if (this.props.autoReconnect) {
this._timeoutId = setTimeout(this.connect, this.props.getRetryInterval(this.retryCount++));
}
});
}
disconnect = () => {
// On calling disconnect explicitly no effort will be made to reconnect
// Clear timeoutId in case the component is trying to reconnect
if (this._timeoutId) {
clearTimeout(this._timeoutId);
}
if (this.state.connected) {
this.subscriptions.forEach((subid, topic) => {
this.unsubscribe(topic);
});
this.client.disconnect(() => {
this._cleanUp();
this.props.onDisconnect();
this._log("Stomp client is successfully disconnected!");
});
}
}
subscribe = (topic) => {
let sub = this.client.subscribe(topic, (msg) => {
this.props.onMessage(JSON.parse(msg.body));
});
this.subscriptions.set(topic, sub);
}
unsubscribe = (topic) => {
let sub = this.subscriptions.get(topic);
sub.unsubscribe();
this.subscriptions.delete(topic);
}
// Below methods can be accessed by ref attribute from the parent component
sendMessage = (topic, msg, opt_headers = {}) => {
if (this.state.connected) {
this.client.send(topic, opt_headers, msg);
} else {
console.error("Send error: SockJsClient is disconnected");
}
}
}
export default SockJsClient;