I use SockJS and StompJS and when I open my application in the browser sometimes it tries to subscribe to some topics before it even connected to the websocket. I'd like the topic subscription wait until the app is connected to websocket.
export class SocksService {
...
public subscribe<T>(destination: string, callback?: (body: T) => void, headers?: object): Observable<Subscription> {
const subscription = new Subject<Subscription>();
headers = headers || {};
this.status.pipe(
first(status => status === ConnectionStatus.CONNECTED)
).subscribe((status: ConnectionStatus) => {
subscription.next(this.stompClient.subscribe(destination, (message: Message) => {
if (callback) {
callback(JSON.parse(message.body) as T);
}
}, headers));
});
return subscription.asObservable();
}
...
}
That's why I implemented this code and I call it like:
this.socksService.subscribe<User>('/topic/user', (user: User) => {
console.log('user received', user);
}).subscribe(subscription => this.userSubscription = subscription);
So I subscribe to the topic only when the connection status is connected and it will be called only for the first time the client successfully connects.
I'd like to unsubscribe later from the topic, so I need the Subscription object returned by the inner subscribe and I also need the message from the inner subscribe.
What I implemented works good, but I think there must be a better way to do it.
(I tried rx-stomp, but it has many bugs.)
You said you tried rx-stomp but have you tried the Angular2+ version of rx-stomp, called ng2-stompjs? It exposes key classes as Angular Injectable Services.
See the mention of it under rx-stomp, here:
https://github.com/stomp-js/ng2-stompjs#documentation
...and it's implementation (Angular 7) via a nice step-by-step, here:
https://stomp-js.github.io/guide/ng2-stompjs/ng2-stomp-with-angular7.html#receiving-messages
e.g.:
this.rxStompService.watch('/topic/demo').subscribe((message: Message) => {
this.receivedMessages.push(message.body);
});
Related
I'm trying to send API calls using the state of one of my hooks as a payload in the HTTP request. The issue I'm having is that the hook being asynchronous is causing irritating behaviour. The two issues I'm facing are:
The state is one step behind when sending the payload, so the HTTP request is going through with my initialised data which I don't want to send.
The component is not re-rendering with the updated state.
Some of my implementation code includes:
Function definition for get request that sets state to the role i'm using
function roleSelector(role_id: string) {
webHelpers.get('/api/worker/role/' + role_id, environment, 'api', token, (data: any) => {
setLocalRole(data);
});
}
Function that handles the changes in component which trigger the cycle and call the above function as well as send off the request after.
function handleAuxChange(aux_value: boolean, role_id: string) {
roleSelector(role_id);
localRole.skills = [];
localRole.allow_aux = !aux_value;
updateRole(localRole);
}
Function that sends off the HTTP POST request:
function updateRole(role: UserRole) {
let skills = [...role.skills];
role.details.forEach(s => {
skills.push({ skill_id: s.skill.id, level_id: s.level.id })
});
role.skills = skills;
webHelpers.post('/api/worker/role', environment, 'api', role, token, (data: any) => {
if (data.status === undefined) {
enqueueSnackbar('role successfully set to active!', { 'variant': 'success' });
}
else {
console.log(role);
enqueueSnackbar(`${data.status}: Unable to apply changes to the role`, { 'variant': 'error' })
}
});
}
Is there something in my implementation that i'm doing/not doing that's leading me to be a step behind in state? I've tried writing in a useEffect hook too with localRole as an entry in the default dependency array but this was no help for me either.
You could make roleSelector return the data. And the handleAuxChange can use the info directly.
eg.
function roleSelector(role_id: string, cb: (data:any) => void) {
return webHelpers.get('/api/worker/role/' + role_id, environment, 'api', token, (data: any) => {
setLocalRole(data);
cb(data);
});
}
function handleAuxChange(aux_value: boolean, role_id: string) {
roleSelector(role_id, localRole => {
localRole.skills = [];
localRole.allow_aux = !aux_value;
updateRole(localRole);
});
}
I have an Angular 9 web app and so far I have made it work quite nicely with Firestore. I have a form for adding user which saves the user to Firestore correctly but without adding them to Authenticated users, the add feature will be removed when I fix the issue below.
users.component.ts:
this.userService.addUser(user);
and in the userService I call:
async addUser(user: User) {
await this.firestore.collection('users').doc(user.email).set(user);
}
The problem is when I want to register a new user with FirebaseAuthentication, the user data does not get saved in users collection. When user clicks on Register in register.component.ts I call:
register() {
this.authenticationService
.register(this.form.controls['email'].value, this.form.controls['password'].value)
.then(() => {
this.userService.addUser({
nickname: this.form.controls['nickname'].value,
email: this.form.controls['email'].value,
role: 'Customer',
});
})
.catch((error) => {
console.log(error);
});
}
The register method in authenticationService.ts:
register(email: string, password: string) {
return this.angularFireAuth.auth.createUserWithEmailAndPassword(email, password);
}
I have tried different approaches, using Promises, async / await, calling directly from register method in authenticationService.ts, using collection('users').add instead of set, using uid I get as a response as document uid instead of email.
I'm inclined to think that there is some kind of rollback mechanism, since I have subscribed to data$:
this.firestore
.collection<User>(this.path)
.snapshotChanges()
.pipe(
takeUntil(this.destroy$),
map((items) => {
return items.map((item) => {
const data = item.payload.doc.data() as User;
return {
...data,
};
});
})
)
.subscribe((data) => {
this.users$.next(data);
});
in register.component.ts ngOnInit:
this.userService.users$.subscribe((data) => {
console.log(data);
});
Which gave me some insight:
The subject triggers twice instantly, with 4 elements and then 3.
users.component.ts
constructor(private userService: UserService, public dialog: MatDialog) {}
register.component.ts
constructor(
private userService: UserService,
private formBuilder: FormBuilder,
private authenticationService: AuthenticationService
) {}
Both components are part of the same module, but for some reason if I navigate to users.component.ts and then to register.component.ts saving user works! It gets added to both Authenticated users and Firestore users collection.
I am not getting any errors at any point.
So I really have no idea what might be causing it to behave differently, but I need it to work from register.component.ts without first navigating to users.component.ts.
EDIT:
My rules:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// This rule allows anyone on the internet to view, edit, and delete
// all data in your Firestore database. It is useful for getting
// started, but it is configured to expire after 30 days because it
// leaves your app open to attackers. At that time, all client
// requests to your Firestore database will be denied.
//
// Make sure to write security rules for your app before that time, or else
// your app will lose access to your Firestore database
match /{document=**} {
allow read, write: if request.time < timestamp.date(2020, 8, 6);
}
}
}
If you see a rollback like this, it typically means that the server rejected the write operation based on the security rules you have specified for the database. So it seems like the user doesn't have permission to write.
You'll have to modify the security rules of the database to allow the user to write their profile data.
You'll have to manually add the created user to your collection.
After createUserWithEmailAndPassword() is sucesfully executed, you should call a custom function that get's the User Object and saves it to Firestore.
example:
register(email: string, password: string) {
let rs = this.angularFireAuth.auth.createUserWithEmailAndPassword(email, password);
return this.saveUser(rs.user)
}
saveUser(user){
const userRef: AngularFirestoreDocument<User> = this.angularFistore.doc(`users/${user.email}`);
userRef.get().subscribe(snapDoc =>{
if(!snapDoc.exists){
const data; //Initialize
//do anything else
return userRef.set(data, { merge: true })
})
}
I'm using NestJS CQRS recipe in order to manage interactions between two entities: User and UserProfile. The architecture is an API Gateway NestJS server + a NestJS server for each microservice (User, UserProfile, etc.).
I have already set up basic interactions through User and UserProfile modules on API Gateway with their own sagas/events/commands:
When a user is created, a user profile is created
When the user profile creation fails, the previously created user is deleted
In details:
In User module, CreateUser command raises a UserCreated event that is intercepted by User saga, which will trigger CreateUserProfile command (from UserProfile module).
If the latter fails, a UserProfileFailedToCreate event is raised and intercepted by UserProfile saga, which will trigger DeleteUser command (from User module).
Everything works fine.
If the CreateUser command fails, I resolve(Promise.reject(new HttpException(error, error.status)) which indicates to the end user that something went wrong during the user creation.
My problem is that I cannot replicate the same behavior for the CreateUserProfile command since the HTTP request promise has already been resolved from the first command, obviously.
So my question is: is there any way to make a command fail if a subsequent command fails in the saga? I understand that the HTTP request is totally disconnected from any subsequent commands triggered by a saga, but I want to know if anybody has already played with events or something else here to replicate this data flow?
One of the reasons I'm using CQRS, besides having a much cleaner code for data interactions among microservices, is to be able to rollback repositories actions in case any of the chained commands fails, which works fine. But I need a way to indicate to the end user that the chain went through an issue and was rollbacked.
UserController.ts
#Post('createUser')
async createUser(#Body() createUserDto: CreateUserDto): Promise<{user: IAuthUser, token: string}> {
const { authUser } = await this.authService.createAuthUser(createUserDto);
// this is executed after resolve() in CreateUserCommand
return {user: authUser, token: this.authService.createAccessTokenFromUser(authUser)};
}
UserService.ts
async createAuthUser(createUserDto: CreateUserDto): Promise<{authUser: IAuthUser}> {
return await this.commandBus
.execute(new CreateAuthUserCommand(createUserDto))
.catch(error => { throw new HttpException(error, error.status); });
}
CreateUserCommand.ts
async execute(command: CreateAuthUserCommand, resolve: (value?) => void) {
const { createUserDto } = command;
const createAuthUserDto: CreateAuthUserDto = {
email: createUserDto.email,
password: createUserDto.password,
phoneNumber: createUserDto.phoneNumber,
};
try {
const user = this.publisher.mergeObjectContext(
await this.client
.send<IAuthUser>({ cmd: 'createAuthUser' }, createAuthUserDto)
.toPromise()
.then((dbUser: IAuthUser) => {
const {password, passwordConfirm, ...publicUser} = Object.assign(dbUser, createUserDto);
return new AuthUser(publicUser);
}),
);
user.notifyCreated();
user.commit();
resolve(user); // <== This makes the HTTP request return its reponse
} catch (error) {
resolve(Promise.reject(error));
}
}
UserSagas.ts
authUserCreated = (event$: EventObservable<any>): Observable<ICommand> => {
return event$
.ofType(AuthUserCreatedEvent)
.pipe(
map(event => {
const createUserProfileDto: CreateUserProfileDto = {
avatarUrl: '',
firstName: event.authUser.firstName,
lastName: event.authUser.lastName,
nationality: '',
userId: event.authUser.id,
username: event.authUser.username,
};
return new CreateUserProfileCommand(createUserProfileDto);
}),
);
}
CreateUserProfileCommand.ts
async execute(command: CreateUserProfileCommand, resolve: (value?) => void) {
const { createUserProfileDto } = command;
try {
const userProfile = this.publisher.mergeObjectContext(
await this.client
.send<IUserProfile>({ cmd: 'createUserProfile' }, createUserProfileDto)
.toPromise()
.then((dbUserProfile: IUserProfile) => new UserProfile(dbUserProfile)),
);
userProfile.notifyCreated();
userProfile.commit();
resolve(userProfile);
} catch (error) {
const userProfile = this.publisher.mergeObjectContext(new UserProfile({id: createUserProfileDto.userId} as IUserProfile));
userProfile.notifyFailedToCreate();
userProfile.commit();
resolve(Promise.reject(new HttpException(error, 500)).catch(() => {}));
}
}
UserProfileSagas.ts
userProfileFailedToCreate = (event$: EventObservable<any>): Observable<ICommand> => {
return event$
.ofType(UserProfileFailedToCreateEvent)
.pipe(
map(event => {
return new DeleteAuthUserCommand(event.userProfile);
}),
);
}
DeleteUserCommand.ts
async execute(command: DeleteAuthUserCommand, resolve: (value?) => void) {
const { deleteAuthUserDto } = command;
try {
const user = this.publisher.mergeObjectContext(
await this.client
.send<IAuthUser>({ cmd: 'deleteAuthUser' }, deleteAuthUserDto)
.toPromise()
.then(() => new AuthUser({} as IAuthUser)),
);
user.notifyDeleted();
user.commit();
resolve(user);
} catch (error) {
resolve(Promise.reject(new HttpException(error, error.status)).catch(() => {}));
}
}
In DDD terms your creation of User and UserProfile constitutes a business transaction - a group of business operations/rules that must be consistent - that spans multiple microservices.
In that case returning the database User before the UserProfile has been created means you return data in an inconsistent state. This is not necessarily wrong, but you should handle this appropriately in the client if you do it like that.
I see three possible ways to deal with this scenario:
You let the Sagas run until they have executed commands that indicate the business transaction has ended, only then resolve an outcome for the client indicating either success or failure (e.g. in error details you can report which steps succeeded and which did not). So you do not yet resolve in CreateAuthUserCommand.
If it can take a long time for the UserProfile to be created (it could even have to be manually validated by a Moderator) then you might want to resolve the User in CreateAuthUserCommand and then subsequently have the client subscribe to UserProfile-related events. You need a mechanism for that, but it decouples the client from the running transaction, and it can do other stuff.
Alternatively you could break up the business transaction into two parts for which the client sends separate requests: one creates/returns the authenticated User, and the other returns the created UserProfile. Though it seems that User + UserProfile belong to the same bounded context, the fact that they reside in two different microservices may indicate they are not (in this case I think the first microservice really is for Authentication and the other for UserProfiles which indicate different bounded contexts to me). Best-practice is to have a microservice implement their own encapsulated bounded context.
(Note: Answered an old question in hopes it is informative to others)
Recently Apollo Client released a websocket subscription feature, but so far I've only seen it used by launching a query using subscribeToMore inside the componentWillMount lifecycle hook.
Here is an example taken from https://dev-blog.apollodata.com/tutorial-graphql-subscriptions-client-side-40e185e4be76#0a8f
const messagesSubscription = gql`
subscription messageAdded($channelId: ID!) {
messageAdded(channelId: $channelId) {
id
text
}
}
`
componentWillMount() {
this.props.data.subscribeToMore({
document: messagesSubscription,
variables: {
channelId: this.props.match.params.channelId,
},
updateQuery: (prev, {subscriptionData}) => {
if (!subscriptionData.data) {
return prev;
}
const newMessage = subscriptionData.data.messageAdded;
// don't double add the message
if (!prev.channel.messages.find((msg) => msg.id === newMessage.id)) {
return Object.assign({}, prev, {
channel: Object.assign({}, prev.channel, {
messages: [...prev.channel.messages, newMessage],
})
});
} else {
return prev;
}
}
});
}
But subscribeToMore is specific to Apollo Client React integration. In VanillaJS there is a watchQuery, but it's stated it should not be used for subscriptions. There is also a subscribe that might be what I'm searching for, but is not documented.
Is there any way using Apollo GraphQL client to handle subscriptions, without being inside a React Component?
Turns out it is the subscribe method. I found a description here: https://dev-blog.apollodata.com/graphql-subscriptions-in-apollo-client-9a2457f015fb#eeba
ApolloClient.subscribe takes a query and variables, and returns an observable. We then call subscribe on the observable, and give it a next function which will call updateQuery. updateQuery specifies how we want our query result to be updated when given the subscription result.
subscribe(repoName, updateQuery){
// call the "subscribe" method on Apollo Client
this.subscriptionObserver = this.props.client.subscribe({
query: SUBSCRIPTION_QUERY,
variables: { repoFullName: repoName },
}).subscribe({
next(data) {
// ... call updateQuery to integrate the new comment
// into the existing list of comments
},
error(err) { console.error('err', err); },
});
}
I'm using AzureMobileServices custom API in my hybrid app. The customAPI name is lets say 'userUtils'.
The login and authentication part works good, I am able to authenticate user using facebook/google oAuth.
After login, in its done function , I call a customAPI function - insert (a http POST) - that basically adds the user in docDB if not already added. and returns back user details to my js client.
(I've 'POST' permission set to 'users with application key' for this customAPI ( btw, is this permission setting right or should it be 'only authenticated users'?)).
//below code is part of a typescript file
this._zumoClient = new WindowsAzure.MobileServiceClient( "https://abcdefExample.azure-mobile.net/", "someExampleApplicationKey");
...
....
....
public authenicateUser(p_oAuthProvider: string) {
var p: Promise<any> = new Promise(
(resolve: (result: any) => void, reject: (err: any) => void) =>
this._zumoClient.login(p_oAuthProvider, null).done(
(loginResult) => {
console.log("login returned with result : " + loginResult.userId);
var theUserAuthId = loginResult.userId;
this._zumoClient.invokeApi('UserUtils/insert', {
method: 'POST',
body: { userAuthId: theUserAuthId }
})
.done(
(loginResult: any) => {
resolve(loginResult)
},
(loginErr: any) => {
reject(loginErr);
}
);
},
(loginErr: any) => {
reject(loginErr);
}
)
);
return p;
}
this first 'invokeAPI' call to my customAPI works good.
However, at a later point, I try to call another customAPI 'usersSocialContacts' (a http GET), I get 401 - 'unauthorized request' ( I've 'get' permission set to 'only authenticated users' for this customAPI).
public getUserSocialContacts() {
var p: Promise<any> = new Promise(
(resolve: (result: any) => void, reject: (err: any) => void) =>
this._zumoClient.invokeApi('UserUtils/userSocialContacts', {
method: 'GET'
})
.done(
(contactsResult: any) => {
console.log("got contacts -" + JSON.stringify(contactsResult));
resolve(contactsResult);
},
(contatcsErr: any) => {
reject(contatcsErr);
}
)
);
return p;
}
And if I set the GET permission for this customAPI to 'allow users with the Application key', then the api function gets called but the request.user is undefined.
How does this work, how do I let the customAPI let know that this user has been authenticated and pass the user in every request, from js client. I read some suggestions in some SO questions or otherwise in google search, to cache the user's auth token and pass it in every request to zumo services using an optional filter object in login call, found some examples for C#/.net, but didn't find any clear example or documentation for javascript client, maybe I missed. Azure documentation gives a js example of calling a customAPI from js client, but that doesn't show caching part and sending authenticated user in subsequent requests.
Would appreciate any knowledge sharing or any pointers to right documentation in this regard.
If you would like only logged in users to access the custom api, then you need to set the permission to 'only authenticated users'.
Once the user is logged in, if you are using the same instance of MobileServiceClient (without logging out) then any requests made to zumo service should contain zumo auth token.