PUT/Update/Delete item in nested Appsync array. React, useReducer, Apollo - reactjs

I have an object in my schema which has an array like so:
type Foo #aws_cognito_user_pool {
id: ID!
name: String
comments: [Comment]
createdAt: AWSDateTime
}
the input and Comment Type look like so:
input FooInput #aws_cognito_user_pool {
name: String
comments: [CommentInput]
}
type Comment #aws_cognito_user_pool {
id: String
body: String
parent: String
createdAt: String
}
input CommentInput {
id: String
body: String
parent: String
createdAt: String
}
In my app I am using useReducer to manage state and apollo.
My Foo reducer looks like so
const INITIAL_FOO_STATE = () => {
name: "",
comments: [],
}
export const FooReducer = (state, action) => {
switch(action.field) {
case "name":
case "comments":
return {
...state, [action.field]: action.payload,
}
default:
return
}
}
in my component i have the add, update and delete functions like so:
const addComment = (body, parent) => {
CreateComment(
body,
parent,
)
.then((comment) => {
setComments([comment, ...comments]);
setActiveComment(null);
});
};
const updateComment = (body, commentId) => {
UpdateComment(body)
.then(() => {
const updatedComments = comments.map((item) => {
if (item.id === commentId) {
return {
...item,
body: body,
};
}
return item;
})
setComments(updatedComments);
setActiveComments(null);
})
};
const deleteComment = (commentId) => {
if (window.confirm("Are you sure you want to remove this comment?")) {
DeleteComment()
.then(() => {
const updatedComments = comments.filter((comment) => comment.id !== commentId);
setComments(updatedComments)
})
}
};
This works just fine on the frontend, but now want to dispatch these actions to my Foo.comments array on the backend and looking for some help as to the best way without the need of another table.
I was thinking of placing the dispatch's within their respective .then()

For anyone interested, the solution was simple.
I had to modify the comment type in schema to:
type Comment #aws_cognito_user_pool {
id: ID!
sourceId: ID!
body: String
parent: String
createdAt: String
}
input CommentInput {
body: String
parent: String
createdAt: String
}
then add the mutations and resolvers for create, update and delete of the Comment.
In the Foo comment field, a query resolver was needed.
This also meant a new table was needed for comments.
The table needed to have the sourceId as primary key and ID as sort key, with a global index setup on the sourceId
I swapped out the CRUD functions in my component with the appropriate Apollo mutations and done.

Related

Pushing data to an array in already existing object with axios

i have a object which looks like this:
{
"title": "675756",
"release_date": "2022-01-16",
"series": "Better Call Saul",
"img": "https://upload.wikimedia.org/wikipedia/en/0/03/Walter_White_S5B.png",
"characters": [],
"id": 1
}
to an characters array i want to add the id of characters.
I do it by form and then i handle submit like this:
const handleSubmit = (values) => {
console.log("dodano aktora do filmu!");
console.log(values);
addActorToMovie(values);
history.goBack();
};
the addActorToMovie action:
export const addActorToMovie = (resp) => ({
type: types.ADD_CHAR_TO_MOVIE,
payload: resp,
});
and the reducer:
case types.ADD_CHAR_TO_MOVIE:
console.log(action.payload);
return {
...state,
...state.episodes.map(function (item) {
return item.id === action.payload.episodeId
? {
id: item.id,
title: item.title,
release_date: item.release_date,
series: item.series,
img: item.img,
characters: [...item.characters, action.payload.actor],
}
: { ...item };
}),
};
It all works, but the problem is that i dont want to do it loccaly. Im using an database with json-server, and I want to do an Axios Request so that it would add a data to the database.
And i don't know how to do this, when i use axios.post it adds an object to my episodes array, if im using axios.put it changes an object. Is there any possibility to push the data to an array as i do it with the code above, but with axios so that it would be added to database?
My approach looked like this:
export const addActorToMovieAxios = (value) => {
console.log(value);
return async (dispatch) => {
try {
const response = await axios.post(
`http://localhost:3000/episodes/`,
value
);
console.log(response);
dispatch(addActorToMovie(response.data));
} catch (ex) {
console.log(ex);
}
};
};
but as I said this does add a new object to an array.....
"episodes": [
{
"title": "675756",
"release_date": "2022-01-16",
"series": "Better Call Saul",
"img": "https://upload.wikimedia.org/wikipedia/en/0/03/Walter_White_S5B.png",
"characters": [],
"id": 1
},
{
"episodeId": 1,
"actor": "1",
"id": 2
}
]
So just to be clear I understand your question, you have an object that already exists in your DB, and you want to push something onto the 'characters' array in that existing object, without creating a new object, correct?
To do this, I would use Mongo for your DB and define two Mongoose Schemas, one for the existing object (let's call it TVShow) and one for the Characters within that object. Your two Schemas will look like this:
TVShowModel.js:
const mongoose = require('mongoose');
const CharacterModel = require('./CharacterModel')
const TVShowScheme = new mongoose.Schema({
title: {
type: String,
},
release_date: {
type: Date,
},
series: {
type: String,
},
img: {
type: String,
},
characters:[
{
type: mongoose.Schema.Types.ObjectId,
ref: 'Student'
},
],
examQuestions: [
{
type: mongoose.Schema.Types.ObjectId,
ref: 'CharacterModel'
}
]
})
module.exports = mongoose.model('TVShowModel', TVShowScheme )
CharacterModel.js:
const mongoose = require('mongoose');
const CharacterModel= new mongoose.Schema({
characterName: {
type: String,
},
actorName: {
type: String,
},
}) // add any other fields you want here
module.exports = mongoose.model('CharacterModel', CharactModelScheme )
Then, create your Axios post request. Make sure you send when you send the 'value' variable to your server, it contains the id (or perhaps the unique title) of the object you'll be 'pushing' to. Push won't work in axios/react, so we'll use the 'spread' opperator instead.
Your router will look like this:
const CharacterModel= require ('../models/CharacterModel');
const TVShowModel= require ('../models/TVShowModel');
const router = express.Router();
router.post('/episodes', async function(req,res){
try{
const tvshow = await TVShowModel.find({title: req.body.title})
// edit as needed
console.log("FOUND TV Show: "+tvshow )
const characterName= req.body.characterName
const actorName = req.body.actorName
const newCharacter = new CharacterModel({
characterName,
actorName,
})
console.log("new character created: "+newCharacter)
tvshow[0].CharacterModel = [...tvshow[0].CharacterModel,newCharacter];
await tvshow[0].save()
.then(()=>res.json('New Character Added to DB'))
.catch(err=>res.status(400).json('Error: ' + err))
} catch(e){
console.log(e)
}
})
Hope this was clear!

Apollo Client delete Item from cache

Hy I'm using the Apollo Client with React. I query the posts with many different variables. So I have one post in different "caches". Now I want to delete a post. So I need to delete this specific post from all "caches".
const client = new ApolloClient({
link: errorLink.concat(authLink.concat(httpLink)),
cache: new InMemoryCache()
});
Postquery:
export const POSTS = gql`
query posts(
$after: String
$orderBy: PostOrderByInput
$tags: JSONObject
$search: String
$orderByTime: Int
) {
posts(
after: $after
orderBy: $orderBy
tags: $tags
search: $search
orderByTime: $orderByTime
) {
id
title
...
}
}
`;
I tried it with the cache.modify(), which is undefined in my mutation([https://www.apollographql.com/docs/react/caching/cache-interaction/#cachemodify][1])
const [deletePost] = useMutation(DELETE_POST, {
onError: (er) => {
console.log(er);
},
update(cache, data) {
console.log(cache.modify())//UNDEFINED!!!
cache.modify({
id: cache.identify(thread), //identify is UNDEFINED + what is thread
fields: {
posts(existingPosts = []) {
return existingPosts.filter(
postRef => idToRemove !== readField('id', postRef)
);
}
}
})
}
});
I also used the useApolloClient() with the same result.
THX for any help.
Instead of using cache.modify you can use cache.evict, which makes the code much shorter:
deletePost({
variables: { id },
update(cache) {
const normalizedId = cache.identify({ id, __typename: 'Post' });
cache.evict({ id: normalizedId });
cache.gc();
}
});
this option worked for me
const GET_TASKS = gql`
query tasks($listID: String!) {
tasks(listID: $listID) {
_id
title
sort
}
}
`;
const REMOVE_TASK = gql`
mutation removeTask($_id: String) {
removeTask(_id: $_id) {
_id
}
}
`;
const Tasks = () => {
const { loading, error, data } = useQuery(GET_TASKS, {
variables: { listID: '1' },
});
сonst [removeTask] = useMutation(REMOVE_TASK);
const handleRemoveItem = _id => {
removeTask({
variables: { _id },
update(cache) {
cache.modify({
fields: {
tasks(existingTaskRefs, { readField }) {
return existingTaskRefs.filter(
taskRef => _id !== readField('_id', taskRef),
);
},
},
});
},
});
};
return (...);
};
You can pass your updater to the useMutation or to the deletePost. It should be easier with deletePost since it probably knows what it tries to delete:
deletePost({
variables: { idToRemove },
update(cache) {
cache.modify({
fields: {
posts(existingPosts = []) {
return existingPosts.filter(
postRef => idToRemove !== readField('id', postRef)
);
},
},
});
},
});
You should change variables to match your mutation. This should work since posts is at top level of your query. With deeper fields you'll need a way to get the id of the parent object. readQuery or a chain of readField from the top might help you with that.

Modifying state for specific item in array in redux reducer

I can't quite wrap my head around the boilerplate of redux. I looked up common patterns for immutable modifying of state but issue is, all these patterns simply push towards the end and not for a specific index.
Before I'll go into actual code, here's what the structure of the state looks like for better imagination (pseudo-code):
state = {
quizMenu: {...},
quizEditor: Array<Question>,
> type Question = {
id: number,
question: string,
questionOptions: Array<QuestionOption>,
}
> type QuestionOption = {
id: number,
optionText: string,
isValid: boolean,
}
}
Hopefully it makes sense. I have created an action for adding questions, which works fine. Now I'm trying to create an action for adding option to an already existing question, but I can't wrap my head around how to in the nested arrays of objects.
Here's how my action in question is defined:
const AQO = 'ADD_QUESTION_OPTION';
/*
* #param questionId - ID of the question we're accesssing in quizEditor array
* #param id - id of the option we're adding (handled in component)
*/
const actionAddQuestionOption = createAction(
AQO,
(questionId: number, id: number) => ({
payload: {
id,
optionText: 'New option',
isValid: false,
questionId,
},
})
);
Now my reducer is the following way:
const reducer = createReducer({//...}, {
[actionAddQuestionOption.type]: (state, action) => ({
...state,
quizEditor: [...state.quizEditor][action.payload.questionId].questionOptions.push({
id: action.payload.id,
optionText: action.payload.optionText,
isValid: action.payload.isValid,
})
})
}
This just ends up in this monster type-error: https://pastebin.com/raw/pBbnxcQp
But I'm pretty sure I'm accessing the Array inside the array of objects incorrectly.
quizEditor: [...state.quizEditor][action.payload.questionId].questionOptions
Does anyone know what would be the proper way of going about accessing it? Much appreciated!
Since you are using redux-toolkit which has immer built in you can just mutate the state directly and it will transform it into an immutable update internally
const reducer = createReducer({
[actionAddQuestionOption.type]: (state, { payload: { questionId, ...option }}) => {
const question = state.questionquizEditor(question => question.id === questionId)
question.questionOptions.push(option)
}
})
The way to make it an immuable update is like this
const reducer = createReducer({
[actionAddQuestionOption.type]: (state, { payload: { questionId, ...option } }) => ({
...state,
quizEditor: state.quizEditor.map(question =>
(question.id === questionId
? {
...question,
questionOptions: [...question.questionOptions, option],
}
: question)),
}),
})
the push method of Array returns the new length of the array not the array itself. What you can do is just concat the new object to the array which in turn will return the new array with the new question option.
[...state.quizEditor][action.payload.questionId].questionOptions.concat({
id: action.payload.id,
optionText: action.payload.optionText,
isValid: action.payload.isValid,
})
Furthermore, we have to modify only that property in the state with our new array:
const reducer = createReducer({
//...}, {
[actionAddQuestionOption.type]: (state, action) => {
const quizEditor = [...state.quizEditor];
quizEditor[action.payload.questionId].questionOptions = quizEditor[
action.payload.questionId
].questionOptions.concat({
id: action.payload.id,
optionText: action.payload.optionText,
isValid: action.payload.isValid
});
return {
...state,
quizEditor
};
}
});
Thanks to immer in redux toolkit we can make it more readable:
const reducer = createReducer({
//...}, {
[actionAddQuestionOption.type]: (state, action) => {
const question = state.quizEditor[action.payload.questionId];
question.questionOptions = [
...question.questionOptions,
{
id: action.payload.id,
optionText: action.payload.optionText,
isValid: action.payload.isValid
}
];
return state;
}
});

Redux state is being updated without dispatching any action

I should start off by saying this is not a duplicate of this question, which happened to have the same title.
I'm simply getting a customers object of arrays from props inside a componentDidMount method like this;
componentDidMount() {
const { customers } = this.props
const expiringCustomers = getExpiringCustomers(customers)
console.log('Expiring customers ', expiringCustomers)
}
Inside another file, I have that getExpiringCustomers function which takes the customers passed and is suppose to return a newly modified list of customers like this;
function numbersOnly(value) {
if(_.isString(value)) {
value = Number(value.replace(/[^\d]/g, ''))
}
return value
}
function normalizeNumber(collection, field) {
return collection.map(obj => {
obj[field] = numbersOnly(obj[field])
return obj
})
}
export function getExpiringCustomers(customers) {
const expiringCustomers = customers.filter(customer => {
const daysLeft = Number(new Date(customer.endDate)) - _.now()
if(daysLeft <= (dateInMonth * 3)) {
return customer
}
})
return normalizeNumber(expiringCustomers, 'rent')
}
I'm connecting my react component with redux state like this;
const mapStateToProps = state => ({
customers: state.customers.filter(customer => customer && !customer.deleted)
})
export default connect(mapStateToProps)(Accounting)
Problem
After the functions run and log results, customers' state is changed in redux store.
This is very confusing as customers_edit action has to pass through some procedures but none of them are called/logged.
Snapshot of the affected object:
Ps. The data is just boilerplate.
//- Focus on rent property
const customers = [
...,
{
id: 'o91wukyfsq36qidkld02a0voo93rna5w',
cardId: 'GD-1101010111',
id_type: 'Driving License',
firstName: 'Maalim',
lastName: 'Guruguja',
names: 'Maalim Guruguja',
property: '5iaprurefg3v3uhad688mypo9kqf6xk3',
rent: '250,000',
email: 'tonimarikapi#yahoo.com',
phone: '239-288-3838-38',
noticePeriod: '3',
status: '2 months remain',
startDate: '2018-07-09',
endDate: '2018-08-17',
createdAt: 1530623480772,
updatedAt: 1531213159147
},
...
]
//- After the functions run, log and edit customers array
const customers = [
...,
{
id: 'o91wukyfsq36qidkld02a0voo93rna5w',
cardId: 'GD-1101010111',
id_type: 'Driving License',
firstName: 'Maalim',
lastName: 'Guruguja',
names: 'Maalim Guruguja',
property: '5iaprurefg3v3uhad688mypo9kqf6xk3',
rent: 250000,
email: 'tonimarikapi#yahoo.com',
phone: '239-288-3838-38',
noticePeriod: '3',
status: '2 months remain',
startDate: '2018-07-09',
endDate: '2018-08-17',
createdAt: 1530623480772,
updatedAt: 1531213159147
},
...
]
From the linked question (possible duplicate one) the guy who answered stated that it's some mutation issue that may cause this. I'm not sure if that applies on props that are suppose to be read-only.
How can I stop these functions from updating my redux store, please help.
You mutate the objects in normalizeNumber, since all the array methods you use don't clone the array's objects.
Change normalizeNumber callback to return a new object with the updated field:
function normalizeNumber(collection, field) {
return collection.map(obj => ({
...obj,
[field]: numbersOnly(obj[field])
}))
}
It looks like you're modifying the customers array unintentionally.
Try:
componentDidMount() {
const { customers } = { ...this.props };
const expiringCustomers = getExpiringCustomers(customers)
console.log('Expiring customers ', expiringCustomers)
}

My query is failing in relay and I don't know why?

I have this simple query which works fine in my Graphql but I cannot pass data using relay to components and I don't know why :(
{
todolist { // todolist returns array of objects of todo
id
text
done
}
}
this is my code in an attempt to pass data in components using relay:
class TodoList extends React.Component {
render() {
return <ul>
{this.props.todos.todolist.map((todo) => {
<Todo todo={todo} />
})}
</ul>;
}
}
export default Relay.createContainer(TodoList, {
fragments: {
todos: () => Relay.QL`
fragment on Query {
todolist {
id
text
done
}
}
`,
},
});
And lastly my schema
const Todo = new GraphQLObjectType({
name: 'Todo',
description: 'This contains list of todos which belong to its\' (Persons)users',
fields: () => {
return {
id: {
type: GraphQLInt,
resolve: (todo) => {
return todo.id;
}
},
text: {
type: GraphQLString,
resolve: (todo) => {
return todo.text;
}
},
done: {
type: GraphQLBoolean,
resolve: (todo) => {
return todo.done;
}
},
}
}
});
const Query = new GraphQLObjectType({
name: 'Query',
description: 'This is the root query',
fields: () => {
return {
todolist: {
type: new GraphQLList(Todo),
resolve: (root, args) => {
return Conn.models.todo.findAll({ where: args})
}
}
}
}
});
This code looks simple and I cannot see why this won't work and I have this error Uncaught TypeError: Cannot read property 'todolist' of undefined, but I configure todolist and I can query in my graphql, you can see the structure of the query is same, I don't know why this is not working?
todolist should be a connection type on Query. Also, your ids should be Relay global IDs. You will not have access to your objects' raw native id fields in Relay.
import {
connectionArgs,
connectionDefinitions,
globalIdField,
} from 'graphql-relay';
// I'm renaming Todo to TodoType
const TodoType = new GraphQLObjectType({
...,
fields: {
id: uidGlobalIdField('Todo'),
...
},
});
const {
connectionType: TodoConnection,
} = connectionDefinitions({ name: 'Todo', nodeType: TodoType });
// Also renaming Query to QueryType
const QueryType = new GraphQLObjectType({
...,
fields: {
id: globalIdField('Query', $queryId), // hard-code queryId if you only have one Query concept (Facebook thinks of this top level field as being a user, so the $queryId would be the user id in their world)
todos: { // Better than todoList; generally if it's plural in Relay it's assumed to be a connection or list
type: TodoConnection,
args: connectionArgs,
},
},
});
// Now, to be able to query off of QueryType
const viewerDefaultField = {
query: { // Normally this is called `viewer`, but `query` is ok (I think)
query: QueryType,
resolve: () => ({}),
description: 'The entry point into the graph',
}
};
export { viewerDefaultField };
The above is not fully complete (you'll likely also need to setup a node interface on one or more of your types, which will require node definitions), but it should answer your basic question and get you started.
It's a huge, huge pain to learn, but once you struggle through it it starts to make sense and you'll begin to love it over RESTful calls.

Resources