Can I change other pieces of state in a Recoil Atom Effect - node.js

The Situation
I am using Recoil to manage state in a React application. I am using Atom Effects to back up recoil values with an API.
When my Atom detects something wrong with the authentication token I would like it to somehow convey that issue to the rest of the application so that the application can update state.
The Details
I have several Atoms:
An atom which stores an authentication token, for use when communicating with the storage API.
Atoms for various pieces of the application which I would like to save on that storage API.
In order to perform the API synchronization I have an apiStorageEffect Atom Effect. The effect uses a recoil-stored authentication token using getPromise and forwards changes using an onSet handler. That handler uses the token when calling the save method. The save method can detect when authentication has failed.
When authentication fails I need to trigger a log out event (setting the token to null, and resetting a series of atoms to their defaults).
Here is a minimal version of the storage effect:
const apiStorageEffect = (key) => async ({
onSet,
getPromise,
}) => {
const authToken = await getPromise(authTokenAtom)
if (!authToken) { return }
onSet(async (newValue) => {
const result = await saveToApiStorage(key, newValue, authToken)
if (result.failed) {
// INSERT CODE TO TRIGGER LOGOUT HERE
}
})
}
The Question
I can think of a few possible paths for triggering logout from within an effect, but all of them involve somehow sending information outside of the affected atom to the main application.
Is it possible to modify atoms OTHER than the atom / node in question from within an atom effect?
Is it possible for an atom effect to emit an event that can be immediately handled elsewhere in the React application?
Is it possible for an atom effect to invoke a method that can modify recoil state more broadly?

Atom effects are still pretty new (currently unstable).
The behavior that you desire would naturally be accomplished by subscribing to all of the related atoms via a selector, and processing the effect there... however, selector effects don't yet exist.
The most straightforward way to do this now is to convert your effect into a component which performs the operation using the effect hook (explained in the recoil docs). That component would then be placed underneath the RecoilRoot providing the recoil state. Here's an example based on the information in your question:
TS Playground
import {useCallback, useEffect} from 'react';
import {atom, RecoilRoot, useRecoilValue, useResetRecoilState} from 'recoil';
declare function saveToApiStorage (
key: string,
data: { value1: number; value2: number; },
authToken: string,
): Promise<{failed: boolean}>;
export const authTokenState = atom<string | null>({
key: 'authToken',
default: null,
});
// the key used in your effect
export const keyState = atom({key: 'key', default: ''});
// some bit of state for the API
export const value1State = atom({key: 'value1', default: 0});
// another one
export const value2State = atom({key: 'value2', default: 0});
// this is the converted effect component
export function ApiStorageEffect (): null {
const key = useRecoilValue(keyState);
const authToken = useRecoilValue(authTokenState);
const value1 = useRecoilValue(value1State);
const value2 = useRecoilValue(value2State);
const resetAuthToken = useResetRecoilState(authTokenState);
const resetValue1 = useResetRecoilState(value1State);
const resetValue2 = useResetRecoilState(value2State);
const updateStorage = useCallback(async () => {
if (!authToken) return;
const result = await saveToApiStorage(key, {value1, value2}, authToken);
if (result.failed) {
resetAuthToken();
resetValue1();
resetValue2();
}
}, [
authToken,
key,
resetAuthToken,
resetValue1,
resetValue2,
value1,
value2,
]);
// use void operator for correct return type
useEffect(() => void updateStorage(), [updateStorage]);
return null;
}
// include underneath the recoil root
function App () {
return (
<RecoilRoot>
<ApiStorageEffect />
{/* rest of app... */}
</RecoilRoot>
);
}

Related

Pass Image Bitmap to Azure Face SDK detectWithStream()

I am trying to write a React app that grabs a frame from the webcam and passes it to the Azure Face SDK (documentation) to detect faces in the image and get attributes of those faces - in this case, emotions and head pose.
I have gotten a modified version of the quickstart example code here working, which makes a call to the detectWithUrl() method. However, the image that I have in my code is a bitmap, so I thought I would try calling detectWithStream() instead. The documentation for this method says it needs to be passed something of type msRest.HttpRequestBody - I found some documentation for this type, which looks like it wants to be a Blob, string, ArrayBuffer, or ArrayBufferView. The problem is, I don't really understand what those are or how I might get from a bitmap image to an HttpRequestBody of that type. I have worked with HTTP requests before, but I don't quite understand why one is being passed to this method, or how to make it.
I have found some similar examples and answers to what I am trying to do, like this one. Unfortunately they are all either in a different language, or they are making calls to the Face API instead of using the SDK.
Edit: I had forgotten to bind the detectFaces() method before, and so I was originally getting a different error related to that. Now that I have fixed that problem, I'm getting the following error:
Uncaught (in promise) Error: image must be a string, Blob, ArrayBuffer, ArrayBufferView, or a function returning NodeJS.ReadableStream
Inside constructor():
this.detectFaces = this.detectFaces.bind(this);
const msRest = require("#azure/ms-rest-js");
const Face = require("#azure/cognitiveservices-face");
const key = <key>;
const endpoint = <endpoint>;
const credentials = new msRest.ApiKeyCredentials({ inHeader: { 'Ocp-Apim-Subscription-Key': key } });
const client = new Face.FaceClient(credentials, endpoint);
this.state = {
client: client
}
// get video
const constraints = {
video: true
}
navigator.mediaDevices.getUserMedia(constraints).then((stream) => {
let videoTrack = stream.getVideoTracks()[0];
const imageCapture = new ImageCapture(videoTrack);
imageCapture.grabFrame().then(function(imageBitmap) {
// detect faces
this.detectFaces(imageBitmap);
});
})
The detectFaces() method:
async detectFaces(imageBitmap) {
const detectedFaces = await this.state.client.face.detectWithStream(
imageBitmap,
{
returnFaceAttributes: ["Emotion", "HeadPose"],
detectionModel: "detection_01"
}
);
console.log (detectedFaces.length + " face(s) detected");
});
Can anyone help me understand what to pass to the detectWithStream() method, or maybe help me understand which method would be better to use instead to detect faces from a webcam image?
I figured it out, thanks to this page under the header "Image to blob"! Here is the code that I added before making the call to detectFaces():
// convert image frame into blob
let canvas = document.createElement('canvas');
canvas.width = imageBitmap.width;
canvas.height = imageBitmap.height;
let context = canvas.getContext('2d');
context.drawImage(imageBitmap, 0, 0);
canvas.toBlob((blob) => {
// detect faces
this.detectFaces(blob);
})
This code converts the bitmap image to a Blob, then passes the Blob to detectFaces(). I also changed detectFaces() to accept blob instead of imageBitmap, like this, and then everything worked:
async detectFaces(blob) {
const detectedFaces = await this.state.client.face.detectWithStream(
blob,
{
returnFaceAttributes: ["Emotion", "HeadPose"],
detectionModel: "detection_01"
}
);
...
}

Feathers JS MongoDB Service: Dynamic Collection Names

Goal
I'm attempting to build a multi-tenant application with Feathers JS. On login, the tenant ID will be included in the request. From then on, the tenant ID will be gotten from the user field with the request parameters. Every user has this tenantId field.
In MongoDB, I have one unique collection of every data type per tenant. The collection names look like tenantId.documents and tenantId.users
Problem
The service generated via the feathers generate service CLI command looks like so:
export class Documents extends Service {
//eslint-disable-next-line #typescript-eslint/no-unused-vars
constructor(options: Partial<MongoDBServiceOptions>, app: Application) {
super(options);
const client: Promise<Db> = app.get('mongoClient');
client.then(db => {
this.Model = db.collection('documents');
});
}
}
As you can see, the generated Services seem to need their collection name ("documents" in this case) during instantiation. Normally, this makes sense since it saves time awaiting a call to app.get("mongoClient")
However, since I need to dynamically change which collection I read from based on the User's tenantId, this won't work for me.
I implemented something like the following:
export class Documents extends Service {
client: Promise<Db>
//eslint-disable-next-line #typescript-eslint/no-unused-vars
constructor(options: Partial<MongoDBServiceOptions>, app: Application) {
super(options);
this.client = app.get("mongoClient");
}
async create(data: IDocumentData, params: Params) {
const db: Db = await this.client;
this.Model = db.collection(`${params.user!!.organizationId}.documents`);
return super.create(data, params);
}
}
The problems are these:
I need to await this.client every request, even when the promise will probably already be fulfilled by the time a user actually makes a request to this service
I have to implement every method of the parent Service even though I barely need to add any real functionality.
Question
What is the most feathers-y way to solve this problem?
I don't want to override every method that I need in every service
I don't see a way to handle this with middleware or hooks.
I also don't think it's necessary to create one service instance per tenant in my application. It seems wasteful since I don't need to make any additional external requests based on the tenant ID, I just need to change the collection
Is there a good, pretty way to do this in Feathers?
Thanks to the helpful Feather community Slack channel, I think I came across a halfway-decent solution to this specific issue. It doesn't address all of my concerns, but at least it de-clutters my code.
First, I should create a new class that extends the built in Service class that implements the feature that I want. It could look something like this:
class DynamicMongoService extends Service {
client: Promise<Db>;
collectionName: string;
constructor(
options: Partial<MongoDBServiceOptions>,
app: Application,
collectionName: string
) {
super(options);
this.client = app.get("mongoClient");
this.collectionName = collectionName;
}
async getCollection(params: Params) {
const db: Db = await this.client;
this.Model = db.collection(
`${params!!.user!!.organizationId}.${this.collectionName}`
);
}
async find(params?: Params) {
await this.getCollection(params!!);
return super.create(params!!);
}
async get(id: Id, params?: Params) {
await this.getCollection(params!!);
return super.get(id, params);
}
async create(data: Partial<any> | Array<Partial<any>>, params?: Params) {
await this.getCollection(params!!);
return super.create(data, params);
}
async update(id: NullableId, data: any, params?: Params) {
await this.getCollection(params!!);
return super.update(id!!, data, params);
}
async patch(id: NullableId, data: Partial<any>, params?: Params) {
await this.getCollection(params!!);
return super.patch(id!!, data, params);
}
async remove(id: NullableId, params?: Params) {
await this.getCollection(params!!);
return super.patch(id!!, params!!);
}
}
The key elements are thus:
Pass collection name in the constructor
Get the collection name before each method
An implementation of this service would look like this:
export class Documents extends DynamicMongoService {
constructor(options: Partial<MongoDBServiceOptions>, app: Application) {
super(options, app, "documents");
}
}
Not the best, but easy enough!

How can I create an entity specific to a user?

I'm creating an action for Google Assistant with Dialogflow and actions-on-google-nodejs that accesses the GitKraken Glo API to add cards to people's boards. I'm authenticating my users with Account Linking. I want my users to be able to say things like Add a card to [board name] or Add a card. If a board name isn't given I want the action to prompt the user for it. How can I create a session entity that get's all the board names for the logged in user?
Sorry if this doesn't make much sense, I'm pretty new to Actions on
Google and Dialogflow. Feel free to ask questions for clarity.
There are a few things you'll need to do first to use a Session Entity:
The Entity Type needs to already exist. Session Entities update existing ones. The easiest way to do this is to create the Entity you want in the Dialogflow UI. It doesn't need to have any Entities in it, but having one as a default can be useful.
You need a Service Account for your project in Google Cloud that will do the update, and a secret key for this account.
Your life will be a lot easier if you use a library, such as the dialogflow-nodejs library.
In general, your code needs to do the following, typically when the user first starts the session (ie - in your Welcome Intent Handler):
Get the list of boards
Update the Session Entity Type, creating an Entity for each board. Doing this update involves:
Issuing a patch against the projects.agent.sessions.entityTypes method with a SessionEntityType for the Entity Type you're overriding.
The SessionEntityType will contain an array of Entities with the canonical name (likely the board name, or some unique identifier) and any aliases for it (at least the board name, possibly anything else, possibly including aliases like "the first one" or "the most recent one").
The README for the library includes links to sample code about how to do this using the nodejs library. Code that I have that does this work has a function like this:
function setSessionEntity( env, entityType ){
const config = envToConfig( env );
const client = new dialogflow.SessionEntityTypesClient( config );
let parent = env.dialogflow.parent;
if( entityType.displayName && !entityType.name ){
entityType.name = `${parent}/entityTypes/${entityType.displayName}`;
}
if( !entityType.entityOverrideMode ){
entityType.entityOverrideMode = 'ENTITY_OVERRIDE_MODE_OVERRIDE';
}
const request = {
parent: parent,
sessionEntityType: entityType
};
return client.createSessionEntityType( request );
}
conv.user.email
You can use conv.user object :
const Users = {};
app.intent('Get Signin', (conv, params, signin) => {
if (signin.status === 'OK') {
const email = conv.user.email
Users[email] = { };
conv.ask(`I got your email as ${email}. What do you want to do next?`)
} else {
conv.ask(`I won't be able to save your data, but what do you want to next?`)
}
})
app.intent('actions.intent.TEXT', (conv, input) => {
if (signin.status === 'OK') {
Users[conv.user.email] = {
lastinput: input
};
}
});
conv.id
Also with conv id is unique id for the current conversation.
// Create an app instance
const app = dialogflow()
// Register handlers for Dialogflow intents
const Users = {};
app.intent('Default Welcome Intent', conv => {
Users[conv.id] = {
conversationId: conv.id,
name: '1234'
};
})
app.intent('actions.intent.TEXT', (conv, input) => {
Users[conv.id] = {
lastinput: input
};
});
app.intent('Goodbye', conv => {
delete Users[conv.id];
})

How to create OutPutContext via V2 client library for node js

I am working on entity and intents creation in my agent using v2 client library for node.js . And for that i am going through this sample which is on git. And it says something related to session id and context id. Can anyone explain me what is sessionId and contextId. And also provide me link where i can read those thing in details.
I am unable to create context by following those example. How can i create context while creating intent at the same time.
The following is code to create a context. You cannot create a context and an intent in a single API call, you first need to create the context and then create the intent that uses the context. The response to the create context API call will return a context ID you can use in your intent.
const dialogflow = require('dialogflow');
// Instantiates clients
const entityTypesClient = new dialogflow.EntityTypesClient();
// The path to the agent the created entity type belongs to.
const agentPath = entityTypesClient.projectAgentPath(projectId);
const createEntityTypeRequest = {
parent: agentPath,
entityType: {
displayName: displayName,
kind: kind,
},
};
entityTypesClient
.createEntityType(createEntityTypeRequest)
.then(responses => {
console.log(`Created ${responses[0].name} entity type`);
})
.catch(err => {
console.error('Failed to create size entity type:', err);
});
Source: https://github.com/googleapis/nodejs-dialogflow/blob/master/samples/resource.js
Contexts are very closely associated with SessionID. Say for eg, you have a chatbot that gets spun up on two computers serving two different user's. Each user will have a respective session_id (If you're coding in NODE, when a new user fires the chatbot, you need to ensure he/she will get a unique session_id).
Now, every unique session id will have unique contexts. From above example, let's say user 1 will initialize an intent that has input context named 'abc' with lifespan of 2 and user 2 will initialize another intent that has input context named 'xyz' with lifespan of 5, these respective contexts gets recorded against each of these user's individual session id's. You can programatically control (edit) contexts and its lifecycle. This is the biggest advantage of code facilitated Dialogflow as opposed to using GUI. Using services like Firebase, you can also preserve session id's and its associated contexts so, next time same user sign's in again, they can start from where they had last left.
I can share a snippet from one of my previous projects where I was managing contexts programatically. Initialization script is as follows:
/**
* #author Pruthvi Kumar
* #email pruthvikumar.123#gmail.com
* #create date 2018-08-15 04:42:22
* #modify date 2018-08-15 04:42:22
* #desc Dialogflow config for chatbot.
*/
const dialogflow_config = {
projectId: 'xxx',
sessionId: 'chatbot-session-id', //This is default assignment. This will hve to be overridden by sessionId as obtained from client in order to main context per sessionId.
languageCode: 'en-US'
};
exports.configStoreSingleton = (function () {
let instanceStacks;
let instanceSessionId;
let contextStack = {};
let intentsStack = {};
let successfulIntentResponseStack = {};
function init() {
contextStack[dialogflow_config['sessionId']] = [];
intentsStack[dialogflow_config['sessionId']] = [];
successfulIntentResponseStack[dialogflow_config['sessionId']] = [];
return {
contextStack: contextStack,
intentsStack: intentsStack,
successfulIntentResponseStack: successfulIntentResponseStack
};
}
return {
init: function () {
if (!instanceStacks || (instanceSessionId !== dialogflow_config['sessionId'] && (!intentsStack[dialogflow_config['sessionId']]))) {
console.log('[dialogflow_config]: Singleton is not instantiated previously or New userSession is triggered! Fresh instance stack will be provisioned');
instanceStacks = init();
instanceSessionId = dialogflow_config['sessionId'];
}
return instanceStacks;
}
};
})();
exports.updateSessionIdOfDialogflowConfig = function (sessionId) {
if (typeof (sessionId) === 'string') {
dialogflow_config['sessionId'] = sessionId;
return true;
} else {
console.warn('[dialogflow_config]: SessionId must be of type STRING!');
return;
}
};
exports.getDialogflowConfig = function () {
return dialogflow_config;
};
And then, to programmatically manage contexts:
/**
* #author Pruthvi Kumar
* #email pruthvikumar.123#gmail.com
* #create date 2018-08-15 04:37:15
* #modify date 2018-08-15 04:37:15
* #desc Operate on Dialogflow Contexts
*/
const dialogflow = require('dialogflow');
const dialogflowConfig = require('../modules/dialogflow_config');
const structjson = require('./dialogflow_structjson');
const util = require('util');
const contextsClient = new dialogflow.ContextsClient();
exports.setContextHistory = function (sessionId, intent_name, context_payload, preservedContext=false) {
/* maintain context stack per session */
/* context_payload = {input_contexts: [], output_contexts = []}
*/
const contextStack = dialogflowConfig.configStoreSingleton.init().contextStack;
if (intent_name) {
contextStack[sessionId].push({
intent: intent_name,
contexts: context_payload,
preserveContext: preservedContext
});
} else {
console.warn('[dialogflow_contexts]: Intent name is not provided OR Nothing in context_payload to add to history!');
}
};
exports.getContextHistory = function () {
const contextStack = dialogflowConfig.configStoreSingleton.init().contextStack;
return contextStack;
}
exports.preserveContext = function () {
const contextStack = dialogflowConfig.configStoreSingleton.init().contextStack;
//Traverse contextStack, get the last contexts.
let context_to_be_preserved = contextStack[dialogflowConfig.getDialogflowConfig()['sessionId']][contextStack[dialogflowConfig.getDialogflowConfig()['sessionId']].length - 1];
//console.log(`context to be preserved is: ${util.inspect(context_to_be_preserved)}`);
return context_to_be_preserved['contexts'].map((context, index) => {
let context_id = exports.getContextId(context);
return exports.updateContext(context_id, true)
});
}
From here, you can reference this github resource to build your own contexts - https://github.com/googleapis/nodejs-dialogflow/blob/master/samples/resource.js
Happy creating digital souls!

use msal to connect to Azure B2C - state parameter

I am using sample from: https://github.com/Azure-Samples/active-directory-b2c-javascript-msal-singlepageapp as a base to implement B2C signup.
How do I pass the state parameter in the example? I saw there was an issue about the state, so i guess it is possible to use state in the example. But, I can't figure out how to use it and how to retrieve it after token is returned.
I use state in my loginRedirect() method.. so I'll post my code here which should help you enough to make this work. I'm using MSAL in angular but the methods that I call should be the same.
In this example user clicks on a login button which calls a login method:
{
const args: AuthenticationParameters = {
state: "some string" //set state parameter (type: string)
};
this.msalService.loginRedirect(args);
}
This code will then redirect user to login.. and then back to your website (your redirectURI).. On this page you should implement handleRedirectCallback method which will be triggered after user is redirected. In this callback you will also get a response (or error) from login process which will include your state string.
this.msalService.handleRedirectCallback((authError, response) => {
if (response) {
const state = this.msalService.getAccountState(response.accountState);
// you don't need to use "this.msalService.getAccountState()" I think
...
do something with state. I use it to redirect back to initial page url.
...
}
});
In reviewing the source code for MSAL.js, I don't see how you can control the value of state. AuthenticationRequestParameters is not exposed and the value of state is set to a new guid when AuthenticationRequestParameters is constructed.
Example:
In the following code of MSAL.js, we have no control over the authenticationRequest variable.
loginRedirect(scopes? : Array<string> , extraQueryParameters? : string): void {
...
this.authorityInstance.ResolveEndpointsAsync()
.then(() => {
const authenticationRequest = new AuthenticationRequestParameters(this.authorityInstance, this.clientId, scopes, ResponseTypes.id_token, this._redirectUri);
...
});
...
}
You can send the state parameter on the loginRequest:
const loginRequest = {
...
scopes: "your scopes",
state: "my custom state value"
}
Then you capture it in the response on accountState, like this:
clientApplication.loginPopup(loginRequest).then(function (loginResponse) {
if (loginResponse.account) {
// will print: my custom state value
console.log(loginResponse.accountState);
....
}
});
You can find the documentation here: https://learn.microsoft.com/en-us/azure/active-directory/develop/msal-js-pass-custom-state-authentication-request

Resources