Why is amplify saving username incorrectly? - reactjs

I have a function to create a vehicle in my amplify project:
const createNewVehicle = async () => {
const { year, make, model, vinNumber } = searchedVehicleInfo || {};
try {
await API.graphql({
query: createVehicle,
variables: { input: { year, make, model, vinNumber } },
authMode: 'AMAZON_COGNITO_USER_POOLS',
});
} catch (err) {
console.log(err);
return null;
}
};
This function creates a vehicle in my dynamoDB table utilizing the API module exposed by amplify. The authMode property saves the Cognito user's username in the API request as well. The vehicle's graphql model looks like this:
type Vehicle
#model
#auth(
rules: [
{
allow: owner
ownerField: "username"
operations: [create, read, update, delete]
}
{ allow: public, operations: [read] }
]
) {
id: ID!
year: Int!
make: String!
model: String!
vinNumber: String!
image: String
username: String
#index(name: "vehiclesByUsername", queryField: "vehiclesByUsername")
receipts: [Receipt] #hasMany(indexName: "byVehicle", fields: ["id"])
}
A team member created his own AWS account and set up his own environment to be used in amplify. This team member created an IAM user and applied the AdminAccess-Amplify role to his user. When he uses this function, the vehicle is created, but it saves the username in a weird way. If his username is test12 it will save the vehicle with a username like this 6e0b3347-5dae-4106-aed6-8ec5c87fde52::test12. So when I try to grab vehicles by username, none come up because his username is test12 and not 6e0b3347-5dae-4106-aed6-8ec5c87fde52::test12.
How can I get it to save just the username and not all the extra information?

Related

How to use upsert with Prisma?

I'm trying to either update or create a record in my Profile table. I'm using Prisma to define the schema and it looks like this:
model Profile {
id String #id #default(cuid())
username String?
about String?
url String?
company String?
userId String
user User #relation(fields: [userId], references: [id])
}
I'm calling the upsert function like this:
const createOrUpdateProfile = await prisma.profile.upsert({
where: { id: id },
update: {
username: username,
about: about,
url: url,
company: company,
},
create: {
username: username,
about: about,
url: url,
company: company,
user: { connect: { id: userId } },
},
});
I'm getting userId from session:
const userId = session.user.id;
I'm getting id, username, about, url, and company from:
//Passed on from await fetch in the form
const { id, username, about, url, company } = req.body;
The issue I'm having is, whenever I'm trying to provide the id, as in where: { id: id }, and it is not already in the database, it doesn't create a new record.
If the id is not in the database, and I do a console.log(id), it gets back as undefined.
If I manually add a record in Profile and connect the user, it updates the record when calling the upsert function.
Can you help me spot what I'm doing wrong?
If I understand your question correctly, the request body may optionally contain an id or be undefined. If the id is present in the request, you want to update the corresponding record. If it isn't, you want to create a new record.
upsert functionality requires a unique identifier that can be used by the database to check whether there is a matching record. The database will then decide what to do. However with an undefined value, prisma cannot work.
You could use a key that will not exist in the database in case the value of id is undefined to make upsert insert the record in this case.
await prisma.profile.upsert({
where: { id: id || '' },
// ...
});
Or you could differentiate the cases and treat them appropriately:
if (id) {
await prisma.profile.update({
where: { id },
data: {
username,
about,
// ...
},
});
} else {
await prisma.profile.create({
data: {
username,
about,
// ...
},
});
}
You might want to think about the case that there is an id given in the body, but it does exist in the database. Do you want the update to fail and reply with an error? Or do you want the database to silently insert? Then use upsert instead of update above.

"No current user" when making unauth Appsync API call for public DB tables

There really isn't enough documentation on this either in the AWS docs or in the Github, so hopefully someone here has tackled a similar issue.
I have a react app with backend api hosted on AWS, using appsync, dynamoDB, and cognito-user-pools. My IAM policies are set up to allow unauth users read-only permission to some public tables. I tried the public api key but that didn't do anything. I'm trying to get the IAM unauth role permissions set up but even when I experimentally added literally every service and every action to the unauth role, I still get "no current user" when attempting to make the API call without logging in.
Use case is for public author pages, where information about an author along with their currently available books is listed. Users should not have to sign in to see this page, an author should be able to drop a link to the page to anyone, whether they have a login for the app or not.
This is my graphql schema for the relevant types, it gets no errors:
type PublicBook #model #auth(rules: [{ allow: owner, operations: [create, update, delete], provider: userPools },
{allow: public, operations: [read], provider: iam}])
#key(name:"byPublicWorld", fields: ["publicWorldId", "indexOrder"])
#key(name:"byPublicSeries", fields: ["publicSeriesId", "indexOrder"]){
id: ID!
publicWorldId: ID
publicSeriesId: ID
indexOrder: Int!
cover: FileObject #connection
description: String
amazon: String
ibooks: String
smashwords: String
kobo: String
goodreads: String
audible: String
barnesnoble: String
sample: String
}
type PublicSeries #model #auth(rules: [{ allow: owner, operations: [create, update, delete], provider: userPools },
{allow: public, operations: [read], provider: iam}])
#key(name:"byPublicWorld", fields: ["publicWorldId", "indexOrder"]){
id: ID!
publicWorldId: ID!
indexOrder: Int!
logo: FileObject #connection
description: String
genre: String
books: [PublicBook]#connection(keyName:"byPublicSeries", fields: ["id"])
}
type PublicWorld #model #auth(rules: [{ allow: owner, operations: [create, update, delete], provider: userPools },
{allow: public, operations: [read], provider: iam}])
#key(name:"byAuthorPage", fields: ["authorPageId", "indexOrder"]){
id: ID!
authorPageId: ID!
logo: FileObject #connection
description: String
genre: String
indexOrder: Int!
series: [PublicSeries]#connection(keyName:"byPublicWorld", fields: ["id"])
books: [PublicBook]#connection(keyName:"byPublicWorld", fields: ["id"])
}
type AuthorPage #model #auth(rules: [{ allow: owner, operations: [create, update, delete], provider: userPools },
{allow: public, operations: [read], provider: iam}])
#key(name:"byPenName", fields: ["penId"])
#key(name:"byPenDisplayName", fields: ["penDisplayName"], queryField: "authorPageByPen"){
id: ID!
authorName: String
penDisplayName: String
penId: ID!
bio: String
photo: FileObject #connection
logo: FileObject #connection
penFBProfile: String
penFBGroup: String
penFBPage: String
penTwitter: String
penInstagram: String
penAmazon: String
penWebsite: String
penNewsletter: String
penGoodreads: String
penPatreon: String
posts: [AuthorPost]#connection(keyName:"byAuthorPage", fields: ["id"])
worlds: [PublicWorld]#connection(keyName:"byAuthorPage", fields: ["id"])
}
type AuthorPost #model #auth(rules: [{ allow: owner, operations: [create, update, delete], provider: userPools },
{allow: public, operations: [read], provider: iam}])
#key(name:"byAuthorPage", fields: ["authorPageId", "timeCreated"]){
id: ID!
authorPageId: ID!
timeCreated: AWSTimestamp!
content: String!
title: String!
subtitle: String
type: PostType!
}
Each of these types is set to owner/cognito permissions for creating, updating, and deleting, and then there is a public auth using iam to read. Seems straight forward enough.
The main type here is Author page, and I have the query set up to pull all the connected relevant cascading information. When logged in, this works fine and shows an author page with all the bits and bobs:
export const authorPageByPen = /* GraphQL */ `
query AuthorPageByPen(
$penDisplayName: String
$sortDirection: ModelSortDirection
$filter: ModelAuthorPageFilterInput
$limit: Int
$nextToken: String
) {
authorPageByPen(
penDisplayName: $penDisplayName
sortDirection: $sortDirection
filter: $filter
limit: $limit
nextToken: $nextToken
) {
items {
id
authorName
penDisplayName
penId
bio
photo {
location
}
logo {
location
}
penFBProfile
penFBGroup
penFBPage
penTwitter
penInstagram
penAmazon
penWebsite
penNewsletter
penGoodreads
penPatreon
posts {
nextToken
startedAt
}
worlds {
nextToken
startedAt
}
_version
_deleted
_lastChangedAt
createdAt
updatedAt
owner
}
nextToken
startedAt
}
}
`;
On the page itself (although in production this just happens at app.js and persists throughout the app), I'm pulling current credentials and logging them to make sure that some kind of IAM identity is being created, and it appears to be:
accessKeyId: "BUNCHANUMBERSKEY"
authenticated: false
expiration: Thu Mar 04 2021 13:18:04 GMT-0700 (Mountain Standard Time) {}
identityId: "us-west-2:48cd766c-4854-4cc6-811a-f82127670041"
secretAccessKey: "SecretKeyBunchanumbers"
sessionToken:"xxxxxbunchanumbers"
That identityId on line 4 is present in my identity pool as an unauth identity, so it is getting back to the pool, which seems to be what's supposed to happen.
So, this identity pool has two roles associated with it, which is standard: auth and unauth, and my Unauthenticated Identities Setting has the box for Enable Access to Unauthenticated Identities checked.
In my unauth role, I've got the following as the inline policy json:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"appsync:GraphQL"
],
"Resource": [
"arn:aws:appsync:us-west-2:MyAccountID:apis/MyAppsyncApiId/types/Mutation/fields/authorPageByPen"
]
}
]
}
I wasn't sure if this needed to be mutation, or query, or what, so I've tried them all. I tried them in combination with 'fields' and with 'index', I've tried writing the JSON, and adding the policies from the inline editor, which gives me the following which also does not work:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": "appsync:GraphQL",
"Resource": [
"arn:aws:appsync:us-west-2:MyAccountID:apis/MyAppSyncAPIId/types/AuthorPage/fields/authorPageByPen",
"arn:aws:appsync:us-west-2:MyAccountID:apis/MyAppSyncAPIID"
]
}
]
}
What bit am I missing here? I could understand getting some error about not being allowed to access a resource, but the only error that logs is No Current User, and that happens immediately after the log showing the user.
Update:
Running the query from the Appsync console works fine with IAM and no logged in user. In the page itself, I'm using the following function to call the author page (I'm using routes):
const pullAuthorPage = async () => {
try{
const authorPageData = await API.graphql(graphqlOperation(authorPageByPen, { penDisplayName: props.match.params.id.toLowerCase() }))
console.log(JSON.stringify(authorPageData, null, 2));
setState({...authorPageData.data.authorPageByPen.items[0]})
} catch (error) {
console.log(error);
}
}
What I thought would happen with this is that if there is no authenticated user logged in, this will run using the unauth user credentials. Is that not the case? And if so, how should I change it?

Posting to mongoDb with ObjectId Many to one relationship

Mongoose/MongoDB Question
I have an Owners model containing basic profile data.
I have a secondary model: OwnersImages
e.g
{
owner: {
type: Schema.Types.ObjectId,
ref: 'Owners'
},
name: String,
imageUrl: String,
},
);
From the client I want to post the imageUrl and the name to the OwnersImages table.
e.g
let values = {
owner: this.state.user._id,
name: this.state.field,
imageUrl: this.state.url
}
axios.post(`${serverPath}/api/addFieldImage`, values)
However Im unsure how best to go about this, link it etc.
I can do a GET request on the Owners table to get the Owner data, but then posting this as part of the values to OwnerImages doesn't successfully link the two tables.
Do i need to just store a string reference to the Owner id in OwnerImages or is there a smarter way of doing this?
Or should I just post the string of the user Id to mongoose and then do a map to the Owner table from within there?
Tried to explain this best way I could but the eyes are tired so please ask if any confusion!
Many thanks
Without seeing your exact setup, I think you could modify this to fit your needs:
// In the Schema/Model files
const ownersSchema = Schema({
// other fields above...
images: [{ type: Schema.Types.ObjectId, ref: 'OwnersImages' }]
});
const ownersImagesSchema = Schema({
// other fields above...
owner: { type: Schema.Types.ObjectId, ref: 'Owners' },
});
// in the route-handler
Owners.findById(req.body.owner, async (err, owner) => {
const ownersImage = new OwnersImages(req.body);
owner.images.push(ownersImage._id);
await ownersImage.save();
await owner.save();
});
As a side-note, I think the Models generally have singular names, so Owner and OwnerImage. The collection will then automatically take on the plural form. Just food for thought.
When you want to load these, you can link them with populate(). Consider loading all of the OwnersImages associated with an Owners in some route-handler where the /:id param is the Owners id:
Owners
.findOne({ _id: req.params.id })
.populate('images')
.exec(function (err, images) {
if (err) return handleError(err);
// do something with the images...
});

AWS AppSync - Implement many to many connections using 1-M #connections and a joining #model

Following AWS documentation (https://aws-amplify.github.io/docs/cli-toolchain/graphql > Many-To-Many Connections), I try to understand the workaround example they provide for many to many connections (which seems not supported yet by Amplify).
The schema is:
type Post #model {
id: ID!
title: String!
editors: [PostEditor] #connection(name: "PostEditors")
}
# Create a join model and disable queries as you don't need them
# and can query through Post.editors and User.posts
type PostEditor #model(queries: null) {
id: ID!
post: Post! #connection(name: "PostEditors")
editor: User! #connection(name: "UserEditors")
}
type User #model {
id: ID!
username: String!
posts: [PostEditor] #connection(name: "UserEditors")
}
Using AWS AppSync Console, so far I'm able to:
Create a user using this mutation:
mutation {
createUser(input:{
username: "theUserName"
}){
username
}
}
Create a post using this mutation:
mutation {
createPost(input: {
title: "second post"
}){
title
}
}
But I don't understand how to add multiple editors to a post? So far, I'm able to add editors to a post using PostEditor join, but in their example, there is this statement (which I don't understand very well), so I don't think this is the good approach:
# Create a join model and disable queries as you don't need them
# and can query through Post.editors and User.posts
So I guess that using this join model to perform mutation is not what I have to do. Nevertheless, to be able to create this relation between a post and an editor, I created a mutation (retrieving "postEditorPostId" and "postEditorEditorId" from both previous mutations):
mutation {
createPostEditor(input:{
postEditorPostId: "XXX-XXX-XXX"
postEditorEditorId: "YYY-YYY-YYY"
}){
post {
title
}
editor {
username
posts {
items {
post {
title
}
}
}
}
}
}
Do I need to perform this previous mutation everytime I add a new editor (so the mutation will remain the same but "postEditorEditorId" will change? it seems obviously not a scalable approach, if for example the UI allows an admin to add 50 or more new editors (so it will need 50 mutations).
Finally I can get the information I need using this query:
query{
getUser(id: "YYY-YYY-YYY"){
username
posts {
items {
post {
title
}
}
}
}
}
Is there a better way (I suppose) to add editors to a post?
edit:
Using a promise, I am able to add multiple editors to a post, but it involves to execute as mutation as mutations as there are users:
const users = [{id: "U1", username: "user1"}, {id: "U2", username: "user2"}];
const post = { id: "P1", title: "Post 1" };
/*
After creating two users and a post using the approriate mutations
Using the CreatePost join below to make user1 and user2 editor on Post 1
*/
function graphqlCreatePostEditor(editorID) {
return new Promise((resolve, reject) => {
resolve(
API.graphql(graphqlOperation(createPostEditor, {
input: {
postID: post.id,
}
}))
)
})
}
let promises = users.map(user=> {
return graphqlCreatePostEditor(user.id)
.then(e => {
console.log(e)
return e;
})
});
Promise.all(promises)
.then(results => {
console.log(results)
})
.catch(e => {
console.error(e);
})
Still looking if there is a way to pass an array in a sigle mutation.
For simplicity sake, I'm lets go with a User model and a Project model where a user can have many projects and belong to many projects.
Note: The creation of join table as I've described it here is for the Amplify JS API for React / React Native / JavaScript
User model
type User #model {
id: ID!
username: String!
projects: [UserProject] #connection(name: "UserProject")
}
Project model
type Project #model {
id: ID!
project_title: String!
users: [UserProject] #connection(name: "ProjectUser")
}
Join table
type UserProject #model {
id: ID!
user: User #connection(name: "UserProject")
project: Project #connection(name: "ProjectUser")
}
Creation of Join table
Prerequisite: Fetch both user.id and project.id however you want to do that.
const UserProjectDetails = {
userProjectUserId: user.id
userProjectProjectId: project.id
};
API.graphql({ query: mutations.createUserProject, variables: {input: UserProjectDetails}})
And there you have it.
This article on dev.to was also pretty straight to the point:
https://dev.to/norrischebl/modeling-relationships-join-table-graphql-aws-amplify-appsync-1n5f

Handling Users with MongoDB Stitch App within Atlas Cluster

I have an MongoDB Stitch app, that users the Email/Password authentication. This creates users within the Stitch App that I can authenticate on the page. I also have an MongoDB Atlas Cluster for my database. In the cluster I have a DB with the name of the project, then a collection underneath that for 'Matches'. So when I insert the 'Matches' into the collection, I can send the authenticated user id from Stitch, so that I have a way to query all Matches for a particular User. But how can I add additional values to the 'User' collection in stitch? That user section is sort of prepackaged in Stitch with whatever authentication type you choose (email/password). But for my app I want to be able to store something like a 'MatchesWon' or 'GamePreference' field on the 'User' collection.
Should I create a collection for 'Users' the same way I did for 'Matches' in my Cluster and just insert the user id that is supplied from Stitch and handle the fields in that collection? Seems like I would be duplicating the User data, but I'm not sure I understand another way to do it. Still learning, I appreciate any feedback/advice.
There isn't currently a way to store your own data on the internal user objects. Instead, you can use authentication triggers to manage users. The following snippet is taken from these docs.
exports = function(authEvent){
// Only run if this event is for a newly created user.
if (authEvent.operationType !== "CREATE") { return }
// Get the internal `user` document
const { user } = authEvent;
const users = context.services.get("mongodb-atlas")
.db("myApplication")
.collection("users");
const isLinkedUser = user.identities.length > 1;
if (isLinkedUser) {
const { identities } = user;
return users.updateOne(
{ id: user.id },
{ $set: { identities } }
)
} else {
return users.insertOne({ _id: user.id, ...user })
.catch(console.error)
}
};
MongoDB innovates at a very fast pace - and while in 2019 there wasn't a way to do this elegantly, now there is. You can now enable custom user data on MongoDB realm! (https://docs.mongodb.com/realm/users/enable-custom-user-data/)
https://docs.mongodb.com/realm/sdk/node/advanced/access-custom-user-data
const user = context.user;
user.custom_data.primaryLanguage == "English";
--
{
id: '5f1f216e82df4a7979f9da93',
type: 'normal',
custom_data: {
_id: '5f20d083a37057d55edbdd57',
userID: '5f1f216e82df4a7979f9da93',
primaryLanguage: 'English',
},
data: { email: 'test#test.com' },
identities: [
{ id: '5f1f216e82df4a7979f9da90', provider_type: 'local-userpass' }
]
}
--
const customUserData = await user.refreshCustomData()
console.log(customUserData);

Resources