Firebase NodeJS SDK: Query by value on nested object - node.js

I have a collection/table that is structured like this:
{
UsedPromos: {
"userid_1": {
"product_1_1": "promo_1_1",
"product_1_2": "promo_1_2",
...
"product_1_n": "promo_1_n",
},
"userid_2": {
"product_2": "promo_2"
},
...
"userid_m": {
...
}
}
}
How can I query an exact match to some "promo_x_y"? I have tried this so far:
const admin = require("firebase-admin");
admin.initializeApp(...);
const ref = admin.database().ref("/UsedPromos");
ref.orderByValue()
.equalTo("promo_x_y")
.once("child_added")
.then((snapshot) => {
console.log(`{key: ${snapshot.key}, value: ${snapshot.val()}}`);
return snapshot
});
But it didn't return anything.

If you are looking for all products that are part of the promotion promo_x_y, you need to adjust your query one level deeper than what you are currently doing.
Currently you are comparing the values of user_1, user_2, and so on to the value "promo_x_y". You get no results, because no entry /UsedPromos/user_1 = "promo_x_y" exists.
/UsedPromosByUser/{user}/{product} = {promo} (your current structure)
To fix this, you will need to search an individual user's list of products. Using the below snippet will log each product that has a value of "promo_x_y".
const admin = require("firebase-admin");
admin.initializeApp(/* ... */);
const ref = admin.database().ref("/UsedPromos");
const userToSearch = "user_1";
ref.child(userToSearch)
.orderByValue()
.equalTo("promo_x_y")
.once("child_added")
.then((snapshot) => {
// snapshot.val() will always be "promo_x_y", so don't bother logging it.
console.log(`${snapshot.key} uses "promo_x_y"`);
return snapshot;
});
Depending on your use case, it may be better to use a "value" event instead.
const admin = require("firebase-admin");
admin.initializeApp(/* ... */);
const ref = admin.database().ref("/UsedPromos");
const userToSearch = "user_1";
ref.child(userToSearch)
.orderByValue()
.equalTo("promo_x_y")
.once("value")
.then((querySnapshot) => {
const productKeys = [];
// productSnapshot.val() will always be "promo_x_y", so don't bother logging it.
querySnapshot.forEach((productSnapshot) => productKeys.push(productSnapshot.key));
console.log(`The following products have a value of "promo_x_y" for user "${userToSearch}": ${productKeys.join(", "}`);
return productKeys;
});
If you are looking to find all products, across all users, that use "promo_x_y", you should create an index in your database instead of using a query.
/UsedPromosByPromo/{promo}/{user}/{product} = true
OR
/UsedPromosByPromo/{promo}/{product}/{user} = true
Instead of using true in the above structure, you could store a timestamp (time of purchase, time promo expires, etc)

Related

Best way to organize firebase writes on update trigger

There may be more than one correct answer to this question, but here's my issue: I have a user document in firebase with many fields that can be updated and which interact in different ways. If one field is updated, it may require a change to another field on the backend. Is it better to have a whole bunch of if statements each with their own write action if the condition is met or, or do single write at the end of the function for all the fields that might change. If a field does not change, I would have to write its original value back to itself. That seems clunky, but so does the other option. Am I missing a third option? What is the best practice here?
Here's an example of what I'm talking about. Updating fields one at a time is what I have now, which looks like this:
export const userUpdate = functions.firestore
.document("users/{userID}")
.onUpdate(async (change) => {
const beforeData = change.before.data();
const afterData = change.after.data();
// user levels up and gets more HP
if(beforeData.userLevel != afterData.userLevel){
const newMaxHP = 15 + 5 * afterData.userLevel;
change.after.ref.update({
maxHp: newMaxHP
})
}
//update user rating
if (beforeData.numberOfRatings != afterData.numberOfRatings) {
const newRating = placerRating(beforeData.userRating, beforeData.numberOfRatings, afterData.latestRating);
change.after.ref.update({
userRating: newRating
})
}
//replenish user funds from zero
if (afterData.money == 0){
change.after.ref.update({
money: 20
})
}
If I did it all in a single write, the if statements would assign a value to a variable, but not update the firestore document. Each if statement would include an else statement assigning the variable to the field's original value. There would be a single write at the end like this:
change.after.ref.update({
maxHp: newMaxHP,
userRating: newRating,
money: 20
})
I hope that helps.
[edit to add follow-up question about updating a map value]
#Dharmaraj's answer works great, but I'm struggling to apply it when updating a map value. BTW - I'm using Typescript.
Before using #Dharmaraj's solution, I was doing this:
admin.firestore().collection("users").doc(lastPlayerAttacker).update({
"equipped.weapon.usesLeft": admin.firestore.FieldValue.increment(-1)
});
Using the update object, I'm trying it like this, but I get the error "Object is of type 'unknown'"
const lastPlayerUpdates:{[key:string]:unknown} = {};
lastPlayerUpdates.equipped.weapon.usesLeft = admin.firestore.FieldValue.increment(-1);
admin.firestore().collection("users").doc(lastPlayerAttacker).update(lastPlayerUpdates);
Any advice on how to fix it?
Every time you call update(), you are being charged for 1 write operation. It'll be best to accumulate all updated fields in an object and then update the document only once as it'll be more efficient too. Try refactoring the code as shown below:
export const userUpdate = functions.firestore
.document("users/{userID}")
.onUpdate(async (change) => {
const beforeData = change.before.data();
const afterData = change.after.data();
const updatedData = {};
// user levels up and gets more HP
if (beforeData.userLevel != afterData.userLevel) {
const newMaxHP = 15 + 5 * afterData.userLevel;
updatedData.maxHp = newMaxHP;
}
//update user rating
if (beforeData.numberOfRatings != afterData.numberOfRatings) {
const newRating = placerRating(beforeData.userRating, beforeData.numberOfRatings, afterData.latestRating);
updatedData.userRating = newRating;
}
//replenish user funds from zero
if (afterData.money == 0) {
updatedData.money = 20;
}
await change.after.ref.update(updatedData);
console.log("Data updated");
return null;
})

Filter items in Redux state with searchbox

I have a list of objects in my redux state. I am trying to filter them using a searchbox and dispatching an action on every change. My state does update when I type but doesn't go back (doesn't show all contents) when I delete. I believe that I'm modifying the state and so when search bar is empty again, there is nothing left to filter.
header.js
export const Header = () =>{
const locationsDropdown = useSelector(selectAllLocations);
const departmentsDropdown = useSelector(selectAllDepartments);
const employees = useSelector(selectAllEmployees)
const dispatch = useDispatch()
const [searchField, setSearchField] = useState("")
const handleChange = (e) => {
setSearchField(e.target.value)
dispatch(listFiltered(searchField))
console.log(employees)
}
snippet from employeeSlice.js (reducer action)
listFiltered:{
reducer(state, action) {
//filters the array but when search is deleted, items don't come back
return state.filter(item => item.fname.includes(action.payload))
}
}
If I try to use this function in header.js (where search field is located), everything works well.
const filtered = employees.filter(item => {
const itemName = item.fname
return itemName.includes(searchField)
})
Problem is that header.js component is not responsible for rendering items where needed and I don't how (if possible) to export 'filtered' result to other components

Array list with 2 values and doing it to a top 10 list

im working on a Discord bot and have a reputation system with fs (npm package) and saving peoples reps in a file and doing the file name as they discord id
now im working on a top 10 command and would need some help here, i currently have this as code:
let users = [];
let reps = [];
fs.readdirSync('./data/reps/').forEach(obj => {
users.push(obj.replace('.json', ''))
let file = fs.readFileSync(`./data/reps/${obj}`)
let data = JSON.parse(file)
reps.push(data.reps)
})
let top = [...users, ...reps]
top.sort((a,b) => {a - b})
console.log(top)
the files form the users are like this:
{
"users": [
"437762415275278337"
],
"reps": 1
}
users are the current users that can't rep the persion anymore and don't need to use it in the command
i wan to get the top 10 of reps so that i can get the user id and how many reps they have, how could i do it with the code above?
You could try this
const topTen = fs.readdirSync('./data/reps/').map(obj => {
const file = fs.readFileSync(`./data/reps/${obj}`);
const data = JSON.parse(file);
return { ...data, name: obj.replace('.json', '') };
}).sort((a, b) => a.reps - b.reps).slice(0, 10);
console.log(topTen);
I would change how you push the data
const users = [];
fs.readdirSync('./data/reps/').forEach(obj => {
let file = fs.readFileSync(`./data/reps/${obj}`)
let data = JSON.parse(file)
reps.push({ reps: data.reps, id: obj.replace(".json", "") });
})
That way when you sort the array the id goes along with
//define this after the fs.readdirSync.forEach method
const top = users.sort((a,b)=> a.reps-b.reps).slice(0,10);
If you want an array of top ids
const topIds = top.map(e => e.id);
If you want a quick string of it:
const str = top.map(e => `${e.id}: ${e.reps}`).join("\n");
Also you should probably just have one or two json files, one would be the array of user id's and their reps and then the other could be of user id's and who they can't rep anymore

ES6 : Object restructuration for mailchimp api

I want to construct a object base on an array and another object.
The goal is to send to mailchimp api my users interests, for that, I've got :
//Array of skills for one user
const skillsUser1 = ["SKILL1", "SKILL3"]
//List of all my skills match to mailchimp interest group
const skillsMailchimpId = {
'SKILL1': 'list_id_1',
'SKILL2': 'list_id_2',
'SKILL3': 'list_id_3',
}
//Mapping of user skill to all skills
const outputSkills = skillsUser1.map((skill) => skillsMailchimpId[skill]);
console.log(outputSkills);
The problem is after, outputSkill get me an array :
["ID1", "ID3"]
But what the mailchimp api need, and so what I need : :
{ "list_id_1": true,
"list_id_2": false, //or empty
"list_id_3" : true
}
A simple way would be this (see comments in code for explanation):
// Array of skills for one user
const skillsUser1 = ["SKILL1", "SKILL3"]
// List of all my skills match to mailchimp interest group
const skillsMailchimpId = {
'SKILL1': 'list_id_1',
'SKILL2': 'list_id_2',
'SKILL3': 'list_id_3',
}
// Create an output object
const outputSkills = {};
// Use `Object.entries` to transform `skillsMailchimpId` to array
Object.entries(skillsMailchimpId)
// Use `.forEach` to add properties to `outputSkills`
.forEach(keyValuePair => {
const [key, val] = keyValuePair;
outputSkills[val] = skillsUser1.includes(key);
});
console.log(outputSkills);
The basic idea is to loop over skillsMailchimpId instead of skillsUser.
But that is not very dynamic. For your production code, you probably want to refactor it to be more flexible.
// Array of skills for one user
const skillsUser1 = ["SKILL1", "SKILL3"]
// List of all my skills match to mailchimp interest group
const skillsMailchimpId = {
'SKILL1': 'list_id_1',
'SKILL2': 'list_id_2',
'SKILL3': 'list_id_3',
}
// Use `Object.entries` to transform `skillsMailchimpId` to array
const skillsMailchimpIdEntries = Object.entries(skillsMailchimpId);
const parseUserSkills = userSkills => {
// Create an output object
const outputSkills = {};
// Use `.forEach` to add properties to `outputSkills`
skillsMailchimpIdEntries.forEach(([key, val]) => {
outputSkills[val] = userSkills.includes(key);
});
return outputSkills;
}
// Now you can use the function with any user
console.log(parseUserSkills(skillsUser1));

Google Cloud Talent Solution fetch a job by requisitionId

I am wondering if it is possible to fetch a job by requisitionId in Google Cloud Talent Solution. requisitionId has to be unique across jobs so it seems like a natural candidate for looking a job up.
When a job is created the api returns a job name that can be used to look the job up:
You can retrieve the details of a previously inserted job by sending a GET request to the Cloud Talent Solution. The URI should include the previously inserted job name returned by the original create request, as a URL parameter.
I'd like to avoid storing these names if possible. In my view storing them adds unnecessary complexity since I already have a unique requisitionId. To be clear the API does not let you add jobs with a duplicate requisitionId:
Job projects/{my_app_id}/jobs/{google_assigned_id} already exists. Request ID for tracking: ... Related Job requisition ID: ...
So can I look up jobs by requisitionId?
I could parse the error message that's returned to get the job name..but that seems pretty brittle.
It turns out the list method takes requisitionId so for a full, read-create-update cycle we can do:
const listRequest = {
parent: `projects/${projectId}`,
'filter': `companyName="${companyName}" AND requisitionId="${requisitionId}"`
}
const listResult = await jobService.projects.jobs.list(listRequest)
const existingJobs = listResult.data.jobs || [];
let existingJob = null
if (existingJobs && existingJobs.length > 0) {
existingJob = existingJobs[0]
}
let googleJob
if (!existingJob) {
const createRequest = {
'parent': `projects/${projectId}`,
'resource': {
'job': {
companyName,
requisitionId,
title,
description,
applicationInfo: {
emails: ['email#example.com']
}
}
}
}
googleJob = await jobService.projects.jobs.create(createRequest)
.then(result => result)
.catch(resp => {
console.error("ERROR")
console.error(resp)
})
} else {
const patchRequest = {
'name': existingJob.name,
'resource': {
'job': {
companyName,
requisitionId,
title,
description,
applicationInfo: {
emails: ['email#example.com']
}
}
}
}
googleJob = await jobService.projects.jobs.patch(patchRequest)
.then(result => result)
.catch(resp => {
console.error("ERROR")
console.error(resp)
})
}
Docs: https://cloud.google.com/talent-solution/job-search/docs/reference/rest/v3/projects.jobs/list?authuser=0&hl=de
Notes:
The double quotes in the filter parameter are important. It will not accept single quotes and will give a cryptic error message.
The patch request cannot take a parent parameter even though everything else requires a parent parameter...
one can add it as custom attribute:
Map<String, CustomAttribute> attributes = new HashMap<>();
attributes
.put("requisitionId", new CustomAttribute().setStringValue(requisitionId)
.setFilterable(true));
Job job = new Job()
...
.setCustomAttributes(attributes);
Job jobCreated = createJob(job);
String jobName = jobCreated.getName();
and then search for requisitionId with a custom attribute filter:
JobQuery jobQuery = new JobQuery().setCustomAttributeFilter(filter);
this is a little redundant, but JobQuery has no method .setRequisitionId().
here's the documentation.

Resources