relay prisma graphql update store - reactjs

I am trying to get prisma and relay working. Here's my repo:
https://github.com/jamesmbowler/prisma-relay-todo
It's a simple todo list. I am able to add the todos, but the ui does not update. When I refresh, the todo is there.
All of the examples of updating the store, that I can find, use a "parent" to the object that is being updated / created.
See https://facebook.github.io/relay/docs/en/mutations.html#using-updater-and-optimisticupdater
Also, the "updater configs" also requires a "parentID". https://facebook.github.io/relay/docs/en/mutations.html#updater-configs
From relay-runtime's RelayConnectionHandler.js comment here:
https://github.com/facebook/relay/blob/master/packages/relay-runtime/handlers/connection/RelayConnectionHandler.js#L232
* store => {
* const user = store.get('<id>');
* const friends = RelayConnectionHandler.getConnection(user, 'FriendsFragment_friends');
* const edge = store.create('<edge-id>', 'FriendsEdge');
* RelayConnectionHandler.insertEdgeAfter(friends, edge);
* }
Is it possible to update the store without having a "parent"? I just have todos, with no parent.
Again, creating the record works, and gives this response:
{ "data" : {
"createTodo" : {
"id" : "cjpdbivhc00050903ud6bkl3x",
"name" : "testing",
"complete" : false
} } }
Here's my updater function
updater: store => {
const payload = store.getRootField('createTodo');
const conn = ConnectionHandler.getConnection(store.get(payload.getDataID()), 'Todos_todoesConnection');
ConnectionHandler.insertEdgeAfter(conn, payload, cursor);
},
I have done a console.log(conn), and it is undefined.
Please help.
----Edit----
Thanks to Denis, I think one problem is solved - that of the ConnectionHandler.
But, I still can't get the ui to update. Here's what I've tried in the updater function:
const payload = store.getRootField('createTodo');
const clientRoot = store.get('client:root');
const conn = ConnectionHandler.getConnection(clientRoot, 'Todos_todoesConnection');
ConnectionHandler.createEdge(store, conn, payload, 'TodoEdge');
I've also tried this:
const payload = store.getRootField('createTodo');
const clientRoot = store.get('client:root');
const conn = ConnectionHandler.getConnection(clientRoot, 'Todos_todoesConnection');
ConnectionHandler.insertEdgeAfter(conn, payload);
My data shape is different from their example, as I don't have the 'todoEdge', and 'node' inside my returned data (see above).
todoEdge {
cursor
node {
complete
id
text
}
}
How do I getLinkedRecord, like this?
const newEdge = payload.getLinkedRecord('todoEdge');

If query is the parent, the parentID will be client:root.
Take a look at this: https://github.com/facebook/relay/blob/1d72862fa620a9db69d6219d5aa562054d9b93c7/packages/react-relay/classic/store/RelayStoreConstants.js#L18
Also at this issue: https://github.com/facebook/relay/issues/2157#issuecomment-385009482
create a const ROOT_ID = 'client:root'; and pass ROOT_ID as your parentID. Also, check the name of your connection on the updater, it has to be exactly equal to the name where you declared the query.
UPDATE:
Actually, you can import ROOT_ID of relay-runtime
import { ROOT_ID } from 'relay-runtime';
UPDATE 2:
Your edit was not very clear for me, but I will provide you an example of it should work ok? After your mutation is run, you first access its data by using getRootField just like you are doing. So, if I have a mutation like:
mutation UserAddMutation($input: UserAddInput!) {
UserAdd(input: $input) {
userEdge {
node {
name
id
age
}
}
error
}
}
You will do:
const newEdge = store.getRootField('UserAdd').getLinkedRecord('userEdge');
connectionUpdater({
store,
parentId: ROOT_ID,
connectionName: 'UserAdd_users',
edge: newEdge,
before: true,
});
This connectionUpdater is a helper function that looks likes this:
export function connectionUpdater({ store, parentId, connectionName, edge, before, filters }) {
if (edge) {
if (!parentId) {
// eslint-disable-next-line
console.log('maybe you forgot to pass a parentId: ');
return;
}
const parentProxy = store.get(parentId);
const connection = ConnectionHandler.getConnection(parentProxy, connectionName, filters);
if (!connection) {
// eslint-disable-next-line
console.log('maybe this connection is not in relay store yet:', connectionName);
return;
}
const newEndCursorOffset = connection.getValue('endCursorOffset');
connection.setValue(newEndCursorOffset + 1, 'endCursorOffset');
const newCount = connection.getValue('count');
connection.setValue(newCount + 1, 'count');
if (before) {
ConnectionHandler.insertEdgeBefore(connection, edge);
} else {
ConnectionHandler.insertEdgeAfter(connection, edge);
}
}
}
Hope it helps :)

Related

Pass an Array as a query String Parameter node.js

How can I pass an array as a query string parameter?
I've tried numerous ways including adding it to the path but i'm not able to pull the array on the back end.
If I hard code the array it works fine, but when I try to pass the array from my front end to the backend it does not work properly.
Can anyone point me in the right direction?
FrontEnd
function loadJob() {
return API.get("realtorPilot", "/myTable/ListJobs", {
'queryStringParameters': {
radius,
availableServices,
}
});
BackEnd
import * as dynamoDbLib from "./libs/dynamodb-lib";
import { success, failure } from "./libs/response-lib";
export async function main(event, context) {
const data = {
radius: event.queryStringParameters.radius,
availableServices: event.queryStringParameters.availableServices,
};
// These hold ExpressionAttributeValues
const zipcodes = {};
const services = {};
data.radius.forEach((zipcode, i) => {
zipcodes[`:zipcode${i}`] = zipcode;
});
data.availableServices.forEach((service, i) => {
services[`:services${i}`] = service;
});
// These hold FilterExpression attribute aliases
const zipcodex = Object.keys(zipcodes).toString();
const servicex = Object.keys(services).toString();
const params = {
TableName: "myTable",
IndexName: "zipCode-packageSelected-index",
FilterExpression: `zipCode IN (${zipcodex}) AND packageSelected IN (${servicex})`,
ExpressionAttributeValues : {...zipcodes, ...services},
};
try {
const result = await dynamoDbLib.call("scan", params);
// Return the matching list of items in response body
return success(result.Items);
} catch (e) {
return failure(e.message);
}
}
Pass a comma seperated string and split it in backend.
Example: https://example.com/apis/sample?radius=a,b,c,d&availableServices=x,y,z
And in the api defenition split the fields on comma.
const data = {
radius: event.queryStringParameters.radius.split(','),
availableServices: event.queryStringParameters.availableServices.split(',')
};

How to consume an Observable multiple times?

I am new to Angular and RxJS. I wonder what is the best/correct way to consume a value from an Observable multiple times.
My setup:
I have a component which calls a service which uses a REST service.
After the server returns the result I want to
use this result in the service at hand
AND return the result to the component.
// foo.component.ts
onEvent() {
this.fooService.foo()
.subscribe((data: FooStatus) => doSomething());
}
// foo.service.ts
private lastResult: FooStatus;
constructor(protected http: HttpClient) {
}
foo(): Observable<FooResult> {
return this.http.get('/foo')
.map((data: FooStatus) => {
this.lastResult = data; // use the data...
return data; // ... and simply pass it through
});
}
Using subscribe() multiple times will not work because the request would be send multiple times. This is wrong.
At the moment I use map() to intercept the result. But I am not comfortable with this because I introduce a side effect. Seems like a code smell to me.
I experimented with
foo(onSuccess: (result: FooResult) => void, onFailure: () => void): void {
...
}
but this looks even worse, I loose the Observable magic. And I do not want to have to write these callbacks in every service method myself.
As another way I considered the call to subscribe() in the service and then to create a fresh Observable I then can return to the component. But I could not get it work... seemed to complicated, too.
Is there a more elegant solution?
Is there a helpful method on Observable I did miss?
There are a number of ways to do this, and the answer will depend on your usage.
This codepen https://codepen.io/mikkel/pen/EowxjK?editors=0011
// interval observer
// click streams from 3 buttons
console.clear()
const startButton = document.querySelector('#start')
const stopButton = document.querySelector('#stop')
const resetButton = document.querySelector('#reset')
const start$ = Rx.Observable.fromEvent(startButton, 'click')
const stop$ = Rx.Observable.fromEvent(stopButton, 'click')
const reset$ = Rx.Observable.fromEvent(resetButton, 'click')
const minutes = document.querySelector('#minutes')
const seconds = document.querySelector('#seconds')
const milliseconds = document.querySelector('#milliseconds')
const toTime = (time) => ({
milliseconds: Math.floor(time % 100),
seconds: Math.floor((time/100) % 60),
minutes: Math.floor(time / 6000)
})
const pad = (number) => number <= 9 ? ('0' + number) : number.toString()
const render = (time) => {
minutes.innerHTML = pad(time.minutes)
seconds.innerHTML = pad(time.seconds)
milliseconds.innerHTML = pad(time.milliseconds)
}
const interval$ = Rx.Observable.interval(10)
const stopOrReset$ = Rx.Observable.merge(
stop$,
reset$
)
const pausible$ = interval$
.takeUntil(stopOrReset$)
const init = 0
const inc = acc => acc+1
const reset = acc => init
const incOrReset$ = Rx.Observable.merge(
pausible$.mapTo(inc),
reset$.mapTo(reset)
)
app$ = start$
.switchMapTo(incOrReset$)
.startWith(init)
.scan((acc, currFunc) => currFunc(acc))
.map(toTime)
.subscribe(val => render(val))
You will notice that the reset$ observable is used in two other observables, incOrReset$ and stopOrReset$
You can also introduce a .multicast() operator, which will explicitly allow you to subscribe multiple times. See description here: https://www.learnrxjs.io/operators/multicasting/
Take a look at share, it allows you to share a single subscription to the underlying source. You could do something like the following (it's not tested, but should give you the idea):
private lastResult: FooStatus;
private fooObs: Observable<FooStatus>;
constructor(protected http: HttpClient) {
this.fooObs = this.http.get('/foo').share();
this.fooObs.subscribe(((data: FooStatus) => this.lastResult = data);
}
foo(): Observable<FooResult> {
return this.fooObs.map(toFooResult);
}
as mentioned by #Mikkel in his answer that approach is cool however you can make use of following methods from Observable and achieve this.
these important methods are publishReplay and refCount,
use them as below,
private lastResult: FooStatus;
private myCachedObservable : Observble<FooResult> = null;
constructor(protected http: HttpClient) {
}
foo(): Observable<FooResult> {
if(!myCachedObservable){
this.myCachedObservable = this.http.get('/foo')
.map((data: FooStatus) => {
this.lastResult = data; // use the data...
return data; // ... and simply pass it through
})
.publishReplay(1)
.refCount();
return this.myObsResponse;
} else{
return this.myObsResponse;
}
}
Now in your component simply subscribe the observable returned from your method as above and notice the network request, you will see only one network request being made for this http call.

How to save Mobx state in sessionStorage

Trying to essentially accomplish this https://github.com/elgerlambert/redux-localstorage which is for Redux but do it for Mobx. And preferably would like to use sessionStorage. Is there an easy way to accomplish this with minimal boilerplate?
The easiest way to approach this would be to have a mobx "autorun" triggered whenever any observable property changes. To do that, you could follow my answer to this question.
I'll put some sample code here that should help you get started:
function autoSave(store, save) {
let firstRun = true;
mobx.autorun(() => {
// This code will run every time any observable property
// on the store is updated.
const json = JSON.stringify(mobx.toJS(store));
if (!firstRun) {
save(json);
}
firstRun = false;
});
}
class MyStore {
#mobx.observable prop1 = 999;
#mobx.observable prop2 = [100, 200];
constructor() {
this.load();
autoSave(this, this.save.bind(this));
}
load() {
if (/* there is data in sessionStorage */) {
const data = /* somehow get the data from sessionStorage or anywhere else */;
mobx.extendObservable(this, data);
}
}
save(json) {
// Now you can do whatever you want with `json`.
// e.g. save it to session storage.
alert(json);
}
}
Turns out you can do this in just a few lines of code:
const store = observable({
players: [
"Player 1",
"Player 2",
],
// ...
})
reaction(() => JSON.stringify(store), json => {
localStorage.setItem('store',json);
}, {
delay: 500,
});
let json = localStorage.getItem('store');
if(json) {
Object.assign(store, JSON.parse(json));
}
Boom. No state lost when I refresh the page. Saves every 500ms if there was a change.
Posting the example from here: https://mobx.js.org/best/store.html
This shows a cleaner method of detecting value changes, though not necessarily local storage.
import {observable, autorun} from 'mobx';
import uuid from 'node-uuid';
export class TodoStore {
authorStore;
transportLayer;
#observable todos = [];
#observable isLoading = true;
constructor(transportLayer, authorStore) {
this.authorStore = authorStore; // Store that can resolve authors for us
this.transportLayer = transportLayer; // Thing that can make server requests for us
this.transportLayer.onReceiveTodoUpdate(updatedTodo => this.updateTodoFromServer(updatedTodo));
this.loadTodos();
}
/**
* Fetches all todo's from the server
*/
loadTodos() {
this.isLoading = true;
this.transportLayer.fetchTodos().then(fetchedTodos => {
fetchedTodos.forEach(json => this.updateTodoFromServer(json));
this.isLoading = false;
});
}
/**
* Update a todo with information from the server. Guarantees a todo
* only exists once. Might either construct a new todo, update an existing one,
* or remove an todo if it has been deleted on the server.
*/
updateTodoFromServer(json) {
var todo = this.todos.find(todo => todo.id === json.id);
if (!todo) {
todo = new Todo(this, json.id);
this.todos.push(todo);
}
if (json.isDeleted) {
this.removeTodo(todo);
} else {
todo.updateFromJson(json);
}
}
/**
* Creates a fresh todo on the client and server
*/
createTodo() {
var todo = new Todo(this);
this.todos.push(todo);
return todo;
}
/**
* A todo was somehow deleted, clean it from the client memory
*/
removeTodo(todo) {
this.todos.splice(this.todos.indexOf(todo), 1);
todo.dispose();
}
}
export class Todo {
/**
* unique id of this todo, immutable.
*/
id = null;
#observable completed = false;
#observable task = "";
/**
* reference to an Author object (from the authorStore)
*/
#observable author = null;
store = null;
/**
* Indicates whether changes in this object
* should be submitted to the server
*/
autoSave = true;
/**
* Disposer for the side effect that automatically
* stores this Todo, see #dispose.
*/
saveHandler = null;
constructor(store, id=uuid.v4()) {
this.store = store;
this.id = id;
this.saveHandler = reaction(
// observe everything that is used in the JSON:
() => this.asJson,
// if autoSave is on, send json to server
(json) => {
if (this.autoSave) {
this.store.transportLayer.saveTodo(json);
}
}
);
}
/**
* Remove this todo from the client and server
*/
delete() {
this.store.transportLayer.deleteTodo(this.id);
this.store.removeTodo(this);
}
#computed get asJson() {
return {
id: this.id,
completed: this.completed,
task: this.task,
authorId: this.author ? this.author.id : null
};
}
/**
* Update this todo with information from the server
*/
updateFromJson(json) {
// make sure our changes aren't send back to the server
this.autoSave = false;
this.completed = json.completed;
this.task = json.task;
this.author = this.store.authorStore.resolveAuthor(json.authorId);
this.autoSave = true;
}
dispose() {
// clean up the observer
this.saveHandler();
}
}
Here, you can use my code, although it only supports localStorage you should be able to modify it quite easily.
https://github.com/nightwolfz/mobx-storage

some data is not being passed from container component to dumb component using react-redux

I am kinda bumped because it used to work automatically when you are using connect api from react-redux.
But now I am facing a situation where some data is not getting updated when something gets returned from reducer..
this is my pseudo code..
in the reducer file I have..
case "SORT_BY_DATE":
let startDateSort = state.startDateSort;
let endDateSort = state.endDateSort;
const sortByStartDateUp = function(a, b) {
return new Date(a.start_date) > new Date(b.start_date) ? 1 :-1;
}
const sortByStartDateDown = function(a, b) {
return new Date(b.start_date) > new Date(a.start_date) ? 1:-1;
}
const sortByEndDateUp = function(a, b) {
return new Date(a.stop_date) > new Date(b.stop_date) ? 1 : -1;
}
const sortByEndDateDown = function(a, b) {
return new Date(b.stop_date) > new Date(a.stop_date) ? 1 : -1;
}
let sortFunctionByDate = "";
if (action.time == "start"){
sortFunctionByDate = startDateSort == true ? sortByStartDateUp : sortByStartDateDown;
startDateSort = !startDateSort;
}else{
sortFunctionByDate = endDateSort == true ? sortByEndDateUp : sortByEndDateDown;
endDateSort = !endDateSort;
}
let filtered = state.status + "Banners";
let banners_filtered =state['banners'].sort(sortFunctionByDate)
state[filtered] = banners_filtered;
state["banners"] = banners_filtered
return Object.assign({}, state, {
startDateSort,
endDateSort,
showImageButton: false,
})
Up to here I was able to assert that I am getting correct info. from the banners_filtered variable.
this is my container component:
const mapStateToProps = (state) => {
let {liveBanners} = state.BannerReducer;
console.log("liveBanners", liveBanners)
return {
liveBanners
}
}
const BannerTableList = connect(mapStateToProps,mapDispatchToProps(BannerTable)
----I was able to log liveBanners here..as well---
But this value is not getting updated in the dumb compononent..like other variables I have been doing...Could it be because there is too much? computation inside the reducer? I hardly doubt that is the reason. But if you have any hunch what could have gone wrong please let me know. I am all ears at this point.. thanks.
--also there is no error shown in the console ---
---more code update--
const BannerTable = ({liveBanners}) => {
console.log("banners", liveBanners)
}
above is my dumb component using stateless function and liveBanners never logged in the console.
You're mutating your current state object in your reducer here:
state[filtered] = banners_filtered;
state["banners"] = banners_filtered
return Object.assign({}, state, {
startDateSort,
endDateSort,
showImageButton: false,
})
This can prevent react-redux from detecting changes, which will prevent expected rerenders. You probably just need to change this to:
return Object.assign({}, state, {
[filtered]:banners_filtered,
banners: banners_filtered,
startDateSort,
endDateSort,
showImageButton: false,
})

No match on any of the rangeBehaviors specified in RANGE_ADD config

I have the following mutation:
export default class AddTaskMutation extends Relay.Mutation {
static fragments = {
classroom: () => Relay.QL`
fragment on Classroom {
id,
}`,
};
getMutation() {
return Relay.QL`mutation { addTask }`;
}
getFatQuery() {
return Relay.QL`
fragment on AddTaskPayload {
classroom {
taskList,
},
taskEdge,
}
`;
}
getConfigs() {
let rangeBehaviors = {};
let member_id = 'hasNewMessages(true) member_id(' + Global.fromGlobalId(this.props.createdByMember)['id'] + ')';
rangeBehaviors[''] = 'append';
rangeBehaviors['hasToDo(true)'] = 'append';
rangeBehaviors['hasToDo(false)'] = 'append';
rangeBehaviors['isStart(true)'] = 'ignore';
rangeBehaviors['isStart(false)'] = 'append';
rangeBehaviors[`${member_id}`] = 'append';
rangeBehaviors['hasNewMessages(true) member_id(null)'] = 'ignore';
return [
{
type: 'RANGE_ADD',
parentName: 'classroom',
parentID: this.props.classroomId,
connectionName: 'taskList',
edgeName: 'taskEdge',
rangeBehaviors,
}
];
}
getVariables() {
return {
title: this.props.title,
instruction: this.props.instruction,
start_date: this.props.start_date,
end_date: this.props.end_date,
is_archived: this.props.is_archived,
is_published: this.props.is_published,
is_preview: this.props.is_preview,
productId: this.props.productId,
classroomId: this.props.classroomId,
createdByMember: this.props.createdByMember,
subTasks: this.props.subTasks,
students: this.props.students,
};
}
}
When running my application I get the following 2 warnings:
warning.js:44 Warning: RelayMutation: The connection taskList{hasNewMessages:true,member_id:null} on the mutation field classroom that corresponds to the ID Classroom:35 did not match any of the rangeBehaviors specified in your RANGE_ADD config. This means that the entire connection will be refetched. Configure a range behavior for this mutation in order to fetch only the new edge and to enable optimistic mutations or use refetch to squelch this warning.
and
warning.js:44 Warning: Using null as a rangeBehavior value is deprecated. Use ignore to avoid refetching a range.
Since the other rangeBehaviors work, I assume there must be a syntactical error when declaring 2 variables in one behavior - in this case hasNewMessages and memeber_id.
I've looked for an answer for this, but I just cannot find any. The docs don't seem to cover this edge case either.
EDIT: I also tried rangeBehaviors['hasNewMessages(true),member_id(null)'] = 'ignore'; (comma as a separator) but with no success.
After inspecting the source code of Relay (file RelayMutationQuery.js) I could see what array key it was searching for in the array of all rangeBehaviors. I could then update my code to the correct formatting.
Since I haven't found anything on the web about this edge case, I'll post my solution here - perhaps someone will find it helpful in the future.
When having 2 (or more) variables for a rangeBehavior, you need to separate them with a period (.). Also, when passing null you don't pass it explicitly - just omit it from its' variable.
For example:
Right:
rangeBehaviors['hasNewMessages(true).member_id()'] = 'ignore';
Wrong:
rangeBehaviors['hasNewMessages(true),member_id(null)'] = 'ignore';

Resources