Upsert an array of objects using Supabase and Reactjs - javascript

I am new to the SQL scene and I am trying to use Supabase for a web app that I am building.
I am trying to push an array objects to the DB, but what I am finding is that it only grabs the first element in the array and adds that to the DB. The end goal functionality is that it grabs the whole array of objects and pushes that and if it detects a change (why I am using upsert) then add that object to the array.
ownedNFTs is an array of objects.
const nakedDB = () => {
try {
setLoading(true);
ownedNFTs.forEach(async (nft) => {
let { data, error, status } = await supabase.from("Users").upsert({
wallet_address: wallet?.accounts[0].address,
NFTS: {
name: nft?.metadata?.name || "",
description: nft?.metadata?.description || "",
image: nft?.metadata?.image || "",
externalUrl: nft?.metadata?.external_url || "",
contractAddress: nft?.contract.address || "",
tokenId: nft?.id.tokenId || "",
blockchain: "Ethereum",
metaverse: nft?.metadata?.name.split(" ")[0] || "",
metadata: nft?.metadata || "",
symbol: nft?.contract.symbol || "",
},
}).eq('wallet_address', wallet?.accounts[0].address);
console.log(data)
if (error && status !== 406) {
throw error;
}
});
} catch (error) {
console.table(error);
} finally {
setLoading(false);
}
};
I have tried following the Supabase documentation on bulkupserting, but that is to create multiple rows at once. I have also tried creating a standard for loop, but that didnt work as well. My current implementation is using forEach() and that is not working as expected.

The problem why it was selecting the first element (I believe) is because the .foreach() function was not running multiple times. This was not a clean way to do a DB call in any case. In addition to that I do think that the data I was sending to the DB was not in the correct format as a pure array of objects. For example when you use .map() it returns two separate objects. Supabase (in my configuration) was not expecting this format.
The solution is below.
I created an empty array
I used .map() on my array of objects
Saved the result of the .map() to the array I created.
Sent the new array to the DB in one call.
const nakedDB = async () => {
try {
ownedNFTs.map((nft) => {
// console.log(nft)
nftArray.push(nft);
});
setLoading(true);
let { data, error, status } = await supabase
.from("Users")
.upsert({
wallet_address: wallet?.accounts[0].address,
NFTS: nftArray,
})
.eq("wallet_address", wallet?.accounts[0].address);
if (error && status !== 406) {
throw error;
}
} catch (error) {
console.table(error);
} finally {
setLoading(false);
}
};
** Updated Solution **
ownedNFTs is already an array of objects in the proper format. I removed the .map() and directly saved ownedNFTs to the DB and it saves all elements. The .map() solution was me over-engineering the issue.

Related

Duplicate array items in Vuex state (using Socket.io)

So I have this app built in Vue and using Vuex. I connect to a Node/Express backend with Socket.Io to be able to push data from the server to client instantly when needed.
The data pushed to the clients are in the form of an object which is then stored in an array in VUEX. Each data object pushed into the array has a unique string attached to it.
This string is used to compare the objects already pushed into the array in VUEX. If there are duplicates they won't be stored. If not equal = they are stored.
I then use ...mapGetters to get the array in Vuex and loop through it. For each object a component is rendered.
HOWEVER - sometimes the same object is rendered twice even though the array in VUEX clearly only shows one copy.
Here is the code in the VUEX Store:
export default new Vuex.Store({
state: {
insiderTrades: [],
},
mutations: {
ADD_INSIDER_TRADE(state, insiderObject) {
if (state.insiderTrades.length === 0) {
// push object into array
state.insiderTrades.unshift(insiderObject);
} else {
state.insiderTrades.forEach((trade) => {
// check if the insider trade is in the store
if (trade.isin === insiderObject.isin) {
// return if it already exists
return;
} else {
// push object into array
state.insiderTrades.unshift(insiderObject);
}
});
}
},
},
getters: {
insiderTrades(state) {
return state.insiderTrades;
},
},
Here is the some of the code in App.vue
mounted() {
// //establish connection to server
this.$socket.on('connect', () => {
this.connectedState = 'ansluten';
this.connectedStateColor = 'green';
console.log('Connected to server');
});
//if disconnected swap to "disconneted state"
this.$socket.on('disconnect', () => {
this.connectedState = 'ej ansluten';
this.connectedStateColor = 'red';
console.log('Disconnected to server');
});
// recieve an insider trade and add to store
this.$socket.on('insiderTrade', (insiderObject) => {
this.$store.commit('ADD_INSIDER_TRADE', insiderObject);
});
},
Your forEach iterates the existing items and adds the new item once for every existing item. Use Array.find:
ADD_INSIDER_TRADE(state, insiderObject) {
const exists = state.insiderTrades.find(trade => trade.isin === insiderObject.isin);
if (!exists) state.insiderTrades.unshift(insiderObject);
},
You don't need the initial length check

forEach not a function when index starts at anything but 0

When inside an object there isn't an index that starts with 0, it returns:
TypeError: data.forEach is not a function
This is how the database looks:
If I add an object with the index 0 to the database, like so (my formatting doesn't matter, this is just to illustrate the hierarchy):
0: {
email: "testmail",
uid: "testuid"
}
Suddenly the forEach function works and also retrieves the users with index 3 and 4. How can I make the forEach loop start at index 3 for example? Or is there a different method that I should be using instead? My code:
useEffect(() => {
if(props.klasData.gebruikers !== undefined) {
var data = props.klasData.gebruikers;
data.forEach(function (user) {
if(!emails.hasOwnProperty(user.email)) {
addEmail(oldArray => [...oldArray, user.email]);
}
setPending(false)
})
}
}, []);
Edit
props.klasData.gebruikers returns all keys within the "gebruikers" object with their children.
It looks like your data is being interpreted as an array by the Firebase SDK, since it starts with sequential numeric keys. If you print the value of your gebruikers snapshot, you'll see that it's:
[null, null, null, {email: "test", uid: "test"}, {email: "wouter#...", uid: "..."}]
These null values are added by the Firebase SDK to turn the keys into a proper array.
If you want to keep the Firebase SDK from converting the data into an array, prefix the keys with a non-numeric character. For example:
"key2": {email: "test", uid: "test"},
"key3": {email: "wouter#...", uid: "..."}
In general, it is more idiomatic to use the UID of the users as the keys in a collection of users. That way, you won't need to query for the UID, and you're automatically guaranteed that each user/UID can only be present in the collection one.
I changed the database like you can see here, as Frank van Puffelen suggested. As Frank also predicted, the root of my problem was coming from the function I didn't post.
By transforming all the indexes of UIDs I was fetching from the database to sequential numeric keys, I managed to get the forEach function working. I did this using users = Object.values(users).filter((el) => {return el != null}). The full effect hook can be found below:
useEffect(() => {
var refCodes = firebase.database().ref("userdata/" + currentUser.uid + "/docentCodes").orderByKey();
refCodes.once("value").then(function (snapshotCodes) {
snapshotCodes.val().forEach(function (code) {
var refCodeData = firebase.database().ref("klassencodes/" + code).orderByKey();
refCodeData.once("value").then(function (snapshotCodeData) {
var users = snapshotCodeData.val().gebruikers;
users = Object.values(users).filter((el) => {return el != null})
if(snapshotCodeData.val() !== null) {
setUsercount(oldArray => [...oldArray, users.length]);
setKlasData(oldArray => [...oldArray, snapshotCodeData.val()]);
setUserdata(oldArray => [...oldArray, users]);
addCode(oldArray => [...oldArray, code])
}
setPending(false);
})
})
})
}, []);
In the function where this useEffect is used, I added const [userdata, setUserdata] = React.useState([]); to acommodate this new information stripped down from indexes of UIDs to indexes made of numeric keys. This userdata is exported to another function, which has the effect hook as stated in the original question. I changed this up to be:
useEffect(() => {
if(props.userData !== undefined) {
var data = props.userData;
if(data !== undefined) {
data.forEach(function (user) {
if(!emails.hasOwnProperty(user.email)) {
addEmail(oldArray => [...oldArray, user.email]);
addUID(oldArray => [...oldArray, Object.keys(props.klasData.gebruikers)]);
}
setPending(false)
})
}
}
}, []);
Summary
In retrospect, I should've gone with a seperate const for just the userdata (snapshotCodeData.val().gebruikers), seperate from the other data returned from the snapshot (snapshotCodeData.val()).
I hope this may help you. The golden line of code is users = Object.values(users).filter((el) => {return el != null}).

Meteor Collection.insert() not returning Id

I'm trying to get the Id of the new insert so I can push the Id onto another collection.
According to this post =>
Meteor collection.insert callback to return new id and this post => Meteor collection.insert callback issues, I should be able to
return Collection.insert(obj);
and it will return the ID of the newly inserted data to my client.
Instead I'm getting an Observable like this:
{_isScalar: false}
_isScalar: false
__proto__:
constructor: f Object()
hasOwnProperty: f hasOwnProperty()
//many other object properties
The documentation seems pretty clear that I should be getting the ID in return. Is this a bug? https://docs.meteor.com/api/collections.html#Mongo-Collection-insert]
My version of meteor is 1.4.4.5...
I've been working on this issue for a few days and I've tried getting the ID many different ways, nothing I've tried results in the ID.
Here's my full code for reference:
Server:
submitStuff: function(data): string{
var loggedInUser = Meteor.user();
if (!loggedInUser || !Roles.userIsInRole(loggedInUser,['user','admin'])){
throw new Meteor.Error("M504", "Access denied");
} else {
try{
let insertedData = null;
const model: MyModel[] = [{
data.stuff //bunch of data being inserted
}];
model.forEach((obj: MyModel) => {
insertedData = Collection.insert(obj);
});
return insertedData;
} catch(e) {
throw new Meteor.Error(e + e.reason, "|Throw new error|");
}
}
},
Client:
Meteor.call('submitData', data, (error, value) => {
if(error){
this.errors.push(error + error.reason + "|new Error code|");
}
else{
Meteor.call('userPushId', value, (error) => { //this pushes Id onto the second collection
if(error){
this.errors.push(error + error.reason + "|new Error code|");
}
});
}
Server
// ...
try {
const myModels = [{ ...whatever }];
// I'm using map so I return an array of id's.
//your forEach about technically should end up with only one id,
// which is the last insertion
const insertedDataIds = myModels.map(model => Collection.insert(model));
// you can also write to the secondary location here with something like:
const secondaryWrite = SecondaryCollection.insert({ something: insertedDataIds });
return insertedDataId's
}
//...
Client
also I don't know if this is just a typo on stack but your Meteor.call('submitData') should be Meteor.call('submitStuff') but that is probably not your actual issue.
Alright, so after searching around I realized I'm using angular-meteors rxjs package to create a MongoObservable when creating a collection instead of Mongo's regular collection.
Because I'm using MongoObservable and trying to invoke .insert() on that, the return is a bit different then a regular Mongo Collection and will return an Observable instead of the Id. Documentation Here
If you add a .collection after the collection name you will get the Id in return like so:
submitStuff: function(data): string{
var loggedInUser = Meteor.user();
if (!loggedInUser || !Roles.userIsInRole(loggedInUser,['user','admin'])){
throw new Meteor.Error("M504", "Access denied");
} else {
try{
let id = new Mongo.ObjectID();
const model: MyModel[] = [{
data.stuff //bunch of data being inserted
_id:
}];
model.forEach((obj: MyModel) => {
insertedData = Collection.collection.insert(obj); //added .collection
});
return insertedData;
} catch(e) {
throw new Meteor.Error(e + e.reason, "|Throw new error|");
}
}
},

How to update Array field?

Im using Firebase Firestore and want to update an array field under a userprofile with the latest chat thread's id.. Im guessing that I have to pull the entire array (if it exists) from the chat node under that user, then I need to append the new id (if it doesnt exist) and update the array.. It works when theres only 1 value in the array then it fails after that with the following error:
Transaction failed: { Error: Cannot convert an array value in an array value.
at /user_code/node_modules/firebase-admin/node_modules/grpc/src/node/src/client.js:554:15 code: 3, metadata: Metadata { _internal_repr: {} } }
and here is my firebase cloud function, can anyone tell me where im going wrong ?
exports.updateMessages = functions.firestore.document('messages/{messageId}/conversation/{msgkey}').onCreate( (event) => {
/// console.log('function started');
const messagePayload = event.data.data();
const userA = messagePayload.userA;
const userB = messagePayload.userB;
// console.log("userA " + userA);
// console.log("userB " + userB);
// console.log("messagePayload " + JSON.stringify(messagePayload, null, 2) );
const sfDocRef = admin.firestore().doc(`users/${userB}`);
return admin.firestore().runTransaction( (transaction) => {
return transaction.get(sfDocRef).then( (sfDoc) => {
const array = [];
array.push(...[event.params.messageId, sfDoc.get('chats') ]);
transaction.update(sfDocRef, { chats: array } );
});
}).then( () => {
console.log("Transaction successfully committed!");
}).catch( (error) => {
console.log("Transaction failed: ", error);
});
});
You're nesting arrays in your code here:
const array = [];
array.push(...[event.params.messageId, sfDoc.get('chats') ]);
This leads to an array with two values, the first one being the new messageId and the second value contains an array all of your previous values, e.g.
[ "new message id", ["previous id", "older id"] ]
This type of nested array is something that Firestore (apparently) doesn't allow to be stored.
The solution is simple:
const array = [event.params.messageId, ...sfDoc.get('chats')];
The fact that you have to first load the array to then add a single element to it is one of reasons Firebasers recommend not storing data in arrays. Your current data looks like it'd be better off as a set, as shown in the Firestore documenation:
{
"new message id": true,
"previous id": true,
"older id": true
}
That way adding a chat ID is as simple as:
sfDocRef.update({ "chats."+event.params.messageId, true })
I have looked further into the matter, and I would follow the advice that Frank gave you in his post; allocate the data in collections rather than with arrays as they have greater versatility for Firebase 1. Researching under the examples listed in the Firebase website looking for anything related to a chat, I’ve found the data structure and code for messages that are used by Firechat as they might be of use for you.
In the source code, they use a collection for the their message-id -userId pair with the following topology 2 :
The exact way how the saving is executed at the repository is 3 :
It executes an append of the message into the Room-id collection. Instead of this structure, you could use an userID - messageID pair as it might fit you better.

Return type of Database.query(Query) isn't Array?

While working on Skygear JS SDK, is the query returning an Array?
readDummy: function(){
const Test = skygear.Record.extend('test_test');
const Query = new skygear.Query(Test);
skygear.publicDB.query(Query).then((records) => {
console.log(records.constructor === Array); // return false
console.log(JSON.stringify(records[0])); //do display correctly
//{"_id":"test_test/b9633d1a-ff3c-491b-82f3-93c8cefb5313","_access":[{"public":true,"level":"read"}],"content":"Hello World"}
}, (error) => {
console.error(error);
});
},
Appears the object is actually a QueryResult which extends Array.
Whatever you're passing the object to appears to be too particular about the exact type.
You could try this
Array.from(records)
to make it a native array.

Categories

Resources