I'm new to web (and everything asynchronous). I'm trying to complete a two step process in an Angular + Firebase app:
Query a Firestore collection to find the ID of a document that matches a filter (name == 'theName').
Use the ID to then update the document.
I'm coming from the embedded world where I can do something like this (context of the app - I'm trying to keep track of results in a combat robotics competition).
// Update the winning robot's doc with results
// Find the ID of the robot we're looking for (ex. winningRobotName = "Some Name")
this.firestore.collection('robots', ref => ref.where('name', '==', winningRobotName)).snapshotChanges()
.subscribe(data => {
this.bots = data.map(e => {
return {
id: e.payload.doc.id,
name: e.payload.doc.data()['name'],
// other fields
} as Robot;
})
});
let robot = this.bots[0]; <--------- This doesn't work because I reach here before the above call returns.
// Update this robot with new fields
this.firestore.doc('robots/' + robot.id)
.update({
fightCount : robot.fightCount + 1,
winCount : robot.winCount + 1,
// other updates
});
How does one wait for one subscription to return before executing another command? Do you nest the subscriptions? Is there something really basic I just don't know about yet?
Thanks.
I don't think AngularFire helps you here, and would instead do this directly on the regular JavaScript SDK:
ref.where('name', '==', winningRobotName))
.get()
.then((snapshots) => {
snapshots.forEach((doc) =>
doc.ref.update({
id: doc.id,
name: doc.data().name
})
})
})
I'm not exactly sure what name: doc.data().name is supposed to do (as it's an identity operation, delivering the same result as its input), but left it in just in case this matters to you.
Related
I am working with a PostgreSQL database using Prisma. I have a bulk update command which I want to fail if any of the records have changed since my last read.
My schema:
model OrderItem {
id String #id #default(uuid()) #db.Uuid
quantity Int
lastUpdated DateTime #updatedAt #map("last_updated")
##map("order_item")
}
I have written a query which works, but I built the query manually rather than using Prisma's safe query builder tools.
My query:
type OrderItemType = {
id: string;
quantity: number;
lastUpdated: Date;
}
type OrderItemUpdateDataType = {
quantity: number;
}
const updateByIds = async (
orderItemIdLastUpdatedTuples: ([OrderItemType['id'], OrderItemType['lastUpdated']])[],
orderItemUpdateData: OrderItemUpdateDataType,
) => {
// Optimistic concurrency - try updating based on last known "last updated" state. If mismatch, fail.
await prisma.$transaction(async (prisma) => {
// TODO: Prefer prisma.$queryRaw. Prisma.join() works on id[], but not on [id, lastUpdated][]
const idLastUpdatedPairs = orderItemIdLastUpdatedTuples
.map(([id, lastUpdated]) => `(uuid('${id}'), '${lastUpdated.toISOString()}')`)
.join(', ');
const query = `SELECT * FROM order_item WHERE (id, last_updated) in ( ${idLastUpdatedPairs} )`;
const items = await prisma.$queryRawUnsafe<OrderItem[]>(query);
// If query doesn't match expected update count then another query has outraced and updated since last read.
const itemIds = orderItemIdLastUpdatedTuples.map(([id]) => id);
if (items.length !== orderItemIdLastUpdatedTuples.length) {
throw new ConcurrentUpdateError(`Order Items ${itemIds.join(', ')} were stale. Failed to update.`);
}
await prisma.orderItem.updateMany({
where: { id: { in: itemIds } },
data: orderItemUpdateData,
});
});
};
This function wants to update a set of items. It accepts a list of tuples - id/lastUpdated pairs. It starts an explicit transaction, then performs an unsafe SELECT query to confirm the items to affect haven't been updated, then updates. This is following the guidance of Prisma's docs here - https://www.prisma.io/docs/concepts/components/prisma-client/transactions#interactive-transactions-in-preview
I was hoping to achieve the same results using prisma.$queryRaw rather than prisma.$queryRawUnsafe or even using implicit transactions rather than an explicit transaction wrapper. I wasn't able to find a syntax for expressing "where in tuple" using either of these approaches, though.
I am able to express what I want using implicit transactions when updating a single record. An example here would look like:
const { count } = await prisma.orderItem.updateMany({
where: { id, lastUpdated },
data: orderItemUpdateData,
});
and when using an explicit, safe query I stumbled on joining the array of tuples properly.
From the Prisma documentation, https://www.prisma.io/docs/concepts/components/prisma-client/raw-database-access#tagged-template-helpers, there exists a Prisma.join command (which happens implicitly when using their tagged template helper syntax) but I wasn't able to generate a valid output when feeding it an array of tuples.
Did I miss anything? Does Prisma support joining a tuple using their safe query template syntax?
My firestore structure
Subwork Package (collection)
- UID (document)
- Subwork Registration (subcollection)
- Doc unique ID (document)
- data etc etc
- UID (document)
I have a table that displays all the documents that I have, inside the "Subwork Registration" subcollection.
//LOAD DATA TO TABLE IS SUCCESS
initializeTable() {
this.SubworkTable = [];
firestore
.collectionGroup("Subwork Registration")
.get()
.then(querySnapshot => {
querySnapshot.forEach(doc => {
this.SubworkTable.push({ ...doc.data(), id: doc.id })
})
})
}
Then, I will pick one of the docs and edit/update its content from the list of docs that shows on the table. But when I do as below, I got into an error .collectionGroup("").doc("") is not a function kind of error
data: () => ({
editedItem: {
v-model: '',
v-model: '',
// etc etc
}
})
//METHODS: {}
UpdateTable() {
var data = {
//
};
firestore
.collectionGroup("Subwork Registration")
.doc(this.editedItem.id)
.update(data)
.then(() => {
//
})
}
I do take note that we cannot update or set if we do .collectionGroup("Subwork Registration").doc() but Im not sure how to do it the right way (to edit/update document inside a subcollection).
Any advice?
To write a document in Firestore you will need to know the complete path to that document.
I recommend either keeping the DocumentReference if you can (as that already contains the entire path to the document), or alternative store the doc.ref.path for each document instead of doc.id as you do now.
I have a set of automated tests that is working fine for my "base case". I have some different defaults (e.g. escalation contacts) that are displayed for users accessing the bot via certain URLs. This works by setting values in user and/or conversation state when a user connects to the bot. I can spoof these values in Emulator to test different cases. This all works fine for manual tests.
However, when I try to test via Mocha using TestAdapter, it seems that user state isn't being retained. I can see in my first step that I am getting the spoofed state values based on my value for channelId in the activity. However, when I send a second activity, the state values have reverted to null.
I think this is due to the processActivity function always creating a new TurnContext, though I'm a little unclear why that doesn't retain at least the user state.
So the question is, how can I modify this test, or is there a different test approach I can take, so that I can keep the values I set in user state across all activities? I can provide the escalationActivity file if needed, but it's just an object with the Test Cases and Test Steps so that I don't have to define them in my test.js file (and so that I can potentially reuse the same framework for other tests).
Here is my test.js file in its entirety.
const { TestAdapter, ActivityTypes, TurnContext, ConversationState, MemoryStorage, UserState } = require('botbuilder');
const { DispatchBot } = require('../../bots/dispatchBot');
const assert = require('assert');
const nock = require('nock');
require('dotenv').config({ path: './.env' });
const { escalationActivity } = require('../testData/escalationDialogTestDataPC');
const memoryStorage = new MemoryStorage();
const userState = new UserState(memoryStorage);
var userDialogStateAccessor = userState.createProperty('userDialogStateProperty');
const conversationState = new ConversationState(memoryStorage);
var dialogState = conversationState.createProperty('dialogState');
const appInsights = require('applicationinsights');
appInsights.setup('dummyKey').start();
const appInsightsClient = appInsights.defaultClient;
// Static texts
const myEatonMessage = `For more information on products and orders, please go to [My.Eaton.com](https://my.eaton.com). If you do not have a My.Eaton account, you can click "Request Access" at the top right.`;
describe('Project Center', async () => {
describe('Escalation', async () => {
const testAdapter = new TestAdapter();
async function processActivity(activity, bot) {
const context = new TurnContext(testAdapter, activity);
await bot.run(context);
}
let bot = new DispatchBot(new ConversationState(memoryStorage), new UserState(memoryStorage), appInsightsClient);
it('Welcome message', async () => {
const conversationUpdateActivity = {
type: ActivityTypes.ConversationUpdate,
channelId: 'projectCenter',
conversation: {
id: 'someId'
},
membersAdded: [
{ id: 'theUser' }
],
from: { id: 'theBot' },
recipient: { id: 'theUser' }
};
await processActivity(conversationUpdateActivity, bot);
let reply = testAdapter.activityBuffer.shift();
assert.strictEqual(reply.attachments[0].content.id, 'menuCard', 'Reply does not match.')
});
// BOT IS LOSING USER STATE ON SUBSEQUENT STEPS
Object.keys(escalationActivity).map((key) => {
describe(key, async () => {
let conversationData = escalationActivity[key].conversation;
//let channel = escalationActivity[key].channel;
//let intent = escalationActivity[key].intialOptions.topIntent;
conversationData.map((interaction, idx) => {
it(idx + '. ' + interaction.type, async () => {
// Create message activity
const messageActivity = {
type: ActivityTypes.Message,
channelId: 'projectCenter',
conversation: {
id: 'someId'
},
from: { id: 'theUser' },
recipient: { id: 'theBot' },
text: interaction.input
};
// Send the conversation update activity to the bot.
await processActivity(messageActivity, bot);
let reply = testAdapter.activityBuffer.shift();
if (idx == 0) { // First index has two extra activities that need to be skipped
reply = testAdapter.activityBuffer.shift();
reply = testAdapter.activityBuffer.shift();
}
assert.strictEqual(reply.text, interaction.reply, 'Reply does not match.');
//assertStrictEqual(1,1);
});
});
});
});
});
});
This isn't really an answer per say, but I was able to get the tests working by "re-spoofing" my projectCenter channel on every turn. For the actual bot function, this information is set in either onEvent via webchat/join event in directline or in onMembersAdded for all other channels. As mentioned in my question, user/conversation state is not being retained after my first Mocha test. I have not fixed that, thus I am not considering this issue completely resolved.
However, from the standpoint of at least being able to automate tests for this functionality, I accomplished that via onMessage handler. I simply look for my test channel, projectCenter, on every message, and reset all of the user and conversation state attributes.
I don't like this because it is a piece of code solely for testing and it is overwriting state every turn which is a bad practice, but as of yet I can't find any better way to get the information which I need to have in user state to persist through all of my tests. I am still hoping that someone can come up with a better solution to this issue... That said, this is at least working for my purposes so I wanted to present it as an answer here.
I have an array saved on my firebase, like this:
matches:[ {match:{id:1,data:...}}]
I want to save just one item on firebase at this array. For example, my match have id:32. I want to find it on the array saved in firebase, and change it.
Im trying to make this. But Im thinking that this is VERY UGLY to make a request to the firebase, copy the array, and save the entire array again.
const ref = `/users/${currentUser.uid}/matches`;
var list = [];
firebase.database().ref(ref).on('value', function (snap) { list = snap.val(); });
if (list.length > 0) {
const indexToUpdate = list.findIndex(k => k.name == match.name)
if (indexToUpdate >= 0) {
list[indexToUpdate] = match;
return dispatch => {
firebase
.database()
.ref(`/users/${currentUser.uid}/matches`)
.set(list)
.then(() => {
dispatch({ type: MATCH_UPDATE, payload: match });
});
};
}
}
Any light?
This line of code:
const indexToUpdate = list.findIndex(k => k.name == match.name)
Is a dead giveaway that your data structure is not ideal.
Consider storing the matches under their name, prefixing it with a string to prevent the array coercion that Kenneth mentioned. So:
matches
match1
date: "2018-06-14..."
...
match2
date: "2018-06-16..."
...
With this you can look up the node for a match without needing a query. It also prevents using an array, which is an anti-pattern in Firebase precisely because of the reason you mention.
For more on this, see the classic Firebase blog post on Best Practices: Arrays in Firebase.
Firebase stores arrays as object and converts it back to array when it comes back to the client if the keys are ordered numerically correctly
But basically it should be able to work the same where you make your path up to the object you want to update.
firebase
.database()
.ref(`/users/${currentUser.uid}/matches/${indexToUpdate}`)
.set(match)
.then(() => {
dispatch({ type: MATCH_UPDATE, payload: match });
});
You need to reference the ID of the item you are trying to set by ID in the URL
firebase
.database()
.ref(`/users/${currentUser.uid}/matches/32`)
.set(list)
.then(() => {
dispatch({ type: MATCH_UPDATE, payload: match });
});
Also, use snap.key to get the ID you need if index isn't working.
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.