RethinkDB - Updating nested array - javascript

I have a survey table that looks like so:
{
id: Id,
date: Date,
clients: [{
client_id: Id,
contacts: [{
contact_id: Id,
score: Number,
feedback: String,
email: String
}]
}]
}
I need to updated the score and feedback fields under a specific contact. Currently, I am running the update like this:
function saveScore(obj){
var dfd = q.defer();
var survey = surveys.get(obj.survey_id);
survey
.pluck({ clients: 'contacts' })
.run()
.then(results => {
results.clients.forEach((item, outerIndex) => {
item.contacts.forEach((item, index, array) => {
if(Number(item.contact_id) === Number(obj.contact_id)) {
array[index].score = obj.score;
console.log(outerIndex, index);
}
});
});
return survey.update(results).run()
})
.then(results => dfd.resolve(results))
.catch(err => dfd.resolve(err));
return dfd.promise;
};
When I look at the update method, it specifies how to update nested key:value pairs. However, I can't find any examples to update an individual item in an array.
Is there a better and hopefully cleaner way to update items in a nested array?

You might need to get the array, filter out the desired value in the array and then append it again to the array. Then you can pass the updated array to the update method.
Example
Let's say you have a document with two clients that both have a name and a score and you want to update the score in one of them:
{
"clients": [
{
"name": "jacob" ,
"score": 200
} ,
{
"name": "jorge" ,
"score": 57
}
] ,
"id": "70589f08-284c-495a-b089-005812ec589f"
}
You can get that specific document, run the update command with an annonymous function and then pass in the new, updated array into the clients property.
r.table('jacob').get("70589f08-284c-495a-b089-005812ec589f")
.update(function (row) {
return {
// Get all the clients, expect the one we want to update
clients: row('clients').filter(function (client) {
return client('name').ne('jorge')
})
// Append a new client, with the update information
.append({ name: 'jorge', score: 57 })
};
});
I do think this is a bit cumbersome and there's probably a nicer, more elegant way of doing this, but this should solve your problem.
Database Schema
Maybe it's worth it to create a contacts table for all your contacts and then do a some sort of join on you data. Then your contacts property in your clients array would look something like:
{
id: Id,
date: Date,
clients: [{
client_id: Id,
contact_scores: {
Id: score(Number)
},
contact_feedbacks: {
Id: feedback(String)
}
}]
}

database schema
{
"clients": [
{
"name": "jacob" ,
"score": 200
} ,
{
"name": "jorge" ,
"score": 57
}
] ,
"id": "70589f08-284c-495a-b089-005812ec589f"
}
then you can do like this using map and branch query .
r.db('users').table('participants').get('70589f08-284c-495a-b089-005812ec589f')
.update({"clients": r.row('clients').map(function(elem){
return r.branch(
elem('name').eq("jacob"),
elem.merge({ "score": 100 }),
elem)})
})

it works for me
r.table(...).get(...).update({
contacts: r.row('Contacts').changeAt(0,
r.row('Contacts').nth(0).merge({feedback: "NICE"}))
})

ReQL solution
Creating a query to update a JSON array of objects in-place, is a rather complicated process in ReThinkDB (and most query languages). The best (and only) solution in ReQL that I know about, is to use a combination of update,offsetsOf,do,changeAt, and merge functions. This solution will retain the order of objects in the array, and only modify values on objects which match in the offsetsOf methods.
The following code (or something similar) can be used to update an array of objects (i.e. clients) which contain an array of objects (i.e. contracts).
Where '%_databaseName_%', '%_tableName_%', '%_documentUUID_%', %_clientValue_%, and %_contractValue_% must be provided.
r.db('%_databaseName_%').table('%_tableName_%').get('%_documentUUID_%').update(row =>
row('clients')
.offsetsOf(clients => client('client_id').eq('%_clientValue_%'))(0)
.do(clientIndex => ({
clients: row('clients')(clientIndex)
.offsetsOf(contacts => contact('contact_id').eq('%_contactValue_%')))(0)
.do(contactIndex => ({
contacts: row(clientIndex)
.changeAt(contractIndex, row(clientIndex)(contractIndex).merge({
'score': 0,
'feedback': 'xyz'
}))
})
}))
)
Why go through the trouble of forming this into ReQL?
survey
.pluck({ clients: 'contacts' }).run()
.then(results => {
results.clients.forEach((item, outerIndex) => {
item.contacts.forEach((item, index, array) => {
if(Number(item.contact_id) === Number(obj.contact_id)) {
array[index].score = obj.score;
console.log(outerIndex, index);
}
});
});
return survey.update(results).run()
})
While the code provided by Jacob (the user who asked the question here on Stack Overflow - shown above) might look simpler to write, the performance is probably not as good as the ReQL solution.
1) The ReQL solution runs on the query-server (i.e. database side) and therefore the code is optimized during the database write (higher performance). Whereas the code above, does not make full use of the query-server, and makes a read and write request pluck().run() and update().run(), and data is processed on the client-request side (i.e. NodeJs side) after the pluck() query is run (lower performance).
2) The above code requires the query-server to send back all the data to the client-request side (i.e. NodeJs side) and therefore the response payload (internet bandwidth usage / download size) can be several megabytes. Whereas the ReQL solution is processed on the query-server, and therefore the response payload typically just confirms that the write was completed, in other words only a few bytes are sent back to the client-request side. Which is done in a single request.
ReQL is too complicated
However, ReQL (and especially SQL) seem overly complicated when working with JSON, and it seems to me that JSON should be used when working with JSON.
I've also proposed that the ReThinkDB community adopt an alternative to ReQL that uses JSON instead (https://github.com/rethinkdb/rethinkdb/issues/6736).
The solution to updating nested JSON arrays should be as simple as...
r('database.table').update({
clients: [{
client_id: 0,
contacts: [{
contact_id: 0,
score: 0,
feedback: 'xyz',
}]
}]
});

tfmontague is on the right path but I think his answer can be improved a lot. Because he uses ...(0) there's a possibility for his answer to throw errors.
zabusa also provides a ReQL solution using map and branch but doesn't show the complete nested update. I will expand on this technique.
ReQL expressions are composable so we can isolate complexity and avoid repetition. This keeps the code flat and clean.
First write a simple function mapIf
const mapIf = (rexpr, test, f) =>
rexpr.map(x => r.branch(test(x), f(x), x));
Now we can write the simplified updateClientContact function
const updateClientContact = (doc, clientId, contactId, patch) =>
doc.merge
( { clients:
mapIf
( doc('clients')
, c => c('client_id').eq(clientId)
, c =>
mapIf
( c('contacts')
, c => c('contact_id').eq(contactId)
, c =>
c.merge(patch)
)
)
}
);
Use it like this
// fetch the document to update
const someDoc =
r.db(...).table(...).get(...);
// create patch for client id [1] and contact id [12]
const patch =
updateClientContact(someDoc, 1, 12, { name: 'x', feedback: 'z' });
// apply the patch
someDoc.update(patch);
Here's a concrete example you can run in reql> ...
const testDoc =
{ clients:
[ { client_id: 1
, contacts:
[ { contact_id: 11, name: 'a' }
, { contact_id: 12, name: 'b' }
, { contact_id: 13, name: 'c' }
]
}
, { client_id: 2
, contacts:
[ { contact_id: 21, name: 'd' }
, { contact_id: 22, name: 'e' }
, { contact_id: 23, name: 'f' }
]
}
, { client_id: 3
, contacts:
[ { contact_id: 31, name: 'g' }
, { contact_id: 32, name: 'h' }
, { contact_id: 33, name: 'i' }
]
}
]
};
updateClientContact(r.expr(testDoc), 2, 23, { name: 'x', feedback: 'z' });
The result will be
{ clients:
[ { client_id: 1
, contacts:
[ { contact_id: 11, name: 'a' }
, { contact_id: 12, name: 'b' }
, { contact_id: 13, name: 'c' }
]
}
, { client_id: 2
, contacts:
[ { contact_id: 21, name: 'd' }
, { contact_id: 22, name: 'e' }
, { contact_id: 23, name: 'x', feedback: 'z' } // <--
]
}
, { client_id: 3
, contacts:
[ { contact_id: 31, name: 'g' }
, { contact_id: 32, name: 'h' }
, { contact_id: 33, name: 'i' }
]
}
]
}

Better late than never
I had your same problem and i could solve it with two ways:
With specific client_id
r.db('nameDB').table('nameTable').get('idRegister')
.update({'clients': r.row('clients')
.map(elem=>{
return r.branch(
elem('client_id').eq('your_specific_client_id'),
elem.merge({
contacts: elem('contacts').map(elem2=>
r.branch(
elem2('contact_id').eq('idContact'),
elem2.merge({
score: 99999,
feedback: 'yourString'
}),
elem2
)
)
}),
elem
)
})
})
Without specific client_id
r.db('nameDB').table('nameTable').get('idRegister')
.update({'clients': r.row('clients')
.map(elem=>
elem.merge({
contacts: elem('contacts').map(elem2=>
r.branch(
elem2('contact_id').eq('idContact'),
elem2.merge({
score: 99999,
feedback: 'yourString'
}),
elem2
)
)
})
)
})
I hope that it works for you, even when happened much time ago

Related

Edit multiple objects in array using mongoose (MongoDB)

So I tried several ways, but I can't, I can modify several objects with the same key but I can't modify any with different keys, if anyone can help me is quite a complex problem
{
id: 123,
"infos": [
{ name: 'Joe', value: 'Disabled', id: 0 },
{ name: 'Adam', value: 'Enabled', id: 0 }
]
};
In my database I have a collection with an array and several objects inside which gives this.
I want to modify these objects, filter by their name and modify the value.
To give you a better example, my site returns me an object with the new data, and I want to modify the database object with the new object, without clearing the array, the name key never changes.
const object = [
{ name: 'Joe', value: 'Hey', id: 1 },
{ name: 'Adam', value: 'None', id: 1 }
];
for(const obj in object) {
Schema.findOneAndUpdate({ id: 123 }, {
$set: {
[`infos.${obj}.value`]: "Test"
}
})
}
This code works but it is not optimized, it makes several requests, I would like to do everything in one request, and also it doesn't update the id, only the value.
If anyone can help me that would be great, I've looked everywhere and can't find anything
My schema structure
new Schema({
id: { "type": String, "required": true, "unique": true },
infos: []
})
I use the $addToSet method to insert objects into the infos array
Try This :
db.collection.update({
id: 123,
},
{
$set: {
"infos.$[x].value": "Value",
"infos.$[x].name": "User"
}
},
{
arrayFilters: [
{
"x.id": {
$in: [
1
]
}
},
],
multi: true
})
The all positional $[] operator acts as a placeholder for all elements in the array field.
In $in you can use dynamic array of id.
Ex :
const ids = [1,2,..n]
db.collection.update(
//Same code as it is...
{
arrayFilters: [
{
"x.id": {
$in: ids
}
},
],
multi: true
})
MongoPlayGround Link : https://mongoplayground.net/p/Tuz831lkPqk
Maybe you look for something like this:
db.collection.update({},
{
$set: {
"infos.$[x].value": "test1",
"infos.$[x].id": 10,
"infos.$[y].value": "test2",
"infos.$[y].id": 20
}
},
{
arrayFilters: [
{
"x.name": "Adam"
},
{
"y.name": "Joe"
}
],
multi: true
})
Explained:
You define arrayFilters for all names in objects you have and update the values & id in all documents ...
playground

Recursive function to create a list from another deep list

Another question about recursive function, I cant get my head arround them.
I have a list with groups that can have any depth, an example:
{
Id: 1,
Name:"Root",
Children: [
{
Id: 1,
Name:"",
Children: [
{
Id: 1,
Name:"",
Children: [
{
Id: 1,
Name:"",
Children: []
},
]
},
]
},
{
Id: 2,
Name:"",
Children: []
},
{
Id: 3,
Name:"",
Children: []
},
]
}
I show these groups in a dropdown that the user can select.
What I need to do is when the user clicks on any group, I need to show all users that are a part of that group AND its subgroups.
The information about which users belong to the group and its subgroups is hold by the userlist. That list is flat and every user has an prop that contains an membership array.
I have re-written this method below several times, this is the closest I get, but this more than doubles the expected lenght because I get dublicates.
const getAllUsersInGroup = (group, usersFiltered) => {
if (!group.Children.length) return usersFiltered.flat();
return group.Children.flatMap((g) => {
return getAllUsersInGroup(
g,
[...usersFiltered, users.filter((u) => u.Memberships.some((m) => m.GroupId === g.Id))]
);
});
};
Another test returns almost all but there is missing users on bigger groups with many subgroups.
const getAllUsersInGroup = (group, userss) => {
if (!group.Children.length) return [...userss].flat();
return group.Children.flatMap((g) => {
return getAllUsersInGroup(g,
users.filter((u) => u.Memberships.some((m) => m.GroupId === g.Id)),
);
});
};
I must be stuck in some wrong thinking or just pure stupid..
Maybe I dont need to check the Children lenght and just go thro them all, but as I understand it you need some statment that stops the method.
A little help would be much appreciated!
Regards

How to search for partial match using index in fauna db

I have a faunadb collection of users. The data is as follows:
{
"username": "Hermione Granger",
"fullName": "Hermione Jean Granger",
"DOB": "19-September-1979",
"bloodStatus": "Muggle-Born",
"gender": "Female",
"parents": [
"Wendell Wilkins",
"Monica Wilkins"
]
}
when I use an index I have to search for the whole phrase i.e. Hermione Granger. But I want to search for just Hermione and get the result.
I came across a solution that seems to work.
The below uses the faunadb client.
"all-items" is an index setup on a collection in Fauna that returns all items in the collection
The lambda is searching on the title field
This will return any document with a title that partially matches the search term.
I know this is a bit late; I hope it helps anyone else who may be looking to do this.
const response = await faunaClient.query(
q.Map(
q.Filter(
q.Paginate(q.Match(q.Index("all_items"))),
q.Lambda((ref) =>
q.ContainsStr(
q.LowerCase(
q.Select(["data", "title"], q.Get(ref))
),
title // <= this is your search term
)
)
),
q.Lambda((ref) => q.Get(ref))
)
The Match function only applies an exact comparison. Partial matches are not supported.
One approach that might work for you is to store fields that would contain multiple values that need to be indexed as arrays.
When you index a field whose value is an array, the index creates multiple index entries for the document so that any one of the array items can be used to match entries. Note that this strategy increases the read and write operations involved.
Here's an example:
> CreateCollection({ name: "u" })
{
ref: Collection("u"),
ts: 1618532727920000,
history_days: 30,
name: 'u'
}
> Create(Collection("u"), { data: { n: ["Hermione", "Granger"] }})
{
ref: Ref(Collection("u"), "295985674342892032"),
ts: 1618532785650000,
data: { n: [ 'Hermione', 'Granger' ] }
}
> Create(Collection("u"), { data: { n: ["Harry", "Potter"] }})
{
ref: Ref(Collection("u"), "295985684233060864"),
ts: 1618532795080000,
data: { n: [ 'Harry', 'Potter' ] }
}
> Create(Collection("u"), { data: { n: ["Ginny", "Potter"] }})
{
ref: Ref(Collection("u"), "295985689713967616"),
ts: 1618532800300000,
data: { n: [ 'Ginny', 'Potter' ] }
}
> CreateIndex({
name: "u_by_n",
source: Collection("u"),
terms: [
{ field: ["data", "n"] }
]
})
{
ref: Index("u_by_n"),
ts: 1618533007000000,
active: true,
serialized: true,
name: 'u_by_n3',
source: Collection("u"),
terms: [ { field: [ 'data', 'n' ] } ],
partitions: 1
}
> Paginate(Match(Index("u_by_n"), ["Potter"]))
{
data: [
Ref(Collection("u"), "295985684233060864"),
Ref(Collection("u"), "295985689713967616")
]
}
Note that you cannot query for multiple array items in a single field:
> Paginate(Match(Index("u_by_n"), ["Harry", "Potter"]))
{ data: [] }
The reason is that the index has only one field defined in terms, and successful matches require sending an array having the same structure as terms to Match.
To be able to search for the full username and the username as an array, I'd suggest storing both the string and array version of the username field in your documents, e.g. username: 'Hermione Granger' and username_items: ['Hermione', 'Granger']. Then create one index for searching the string field, and another for the array field, then you can search either way,

Generic id-referenced json parsing in javascript

In a project I'm working on I'm calling an rest-api that returns json as follows:
[
{
"id":0,
"username":"someone0",
"areas":[
],
"role":{
"id":2,
"name":"somerole2",
"users":null
}
},
{
"id":1,
"username":"someone1",
"areas":[
],
"role":{
"id":1,
"name":"somerole1",
"users":null
}
},
{
"id":3,
"username":"someone3",
"areas":[
],
"role":1
}
]
As you can see user 'someone1' and 'someone3' both carry the same role object, but the second time it's referenced (at someone3) it is only referenced by it's id.
Because every response from this api is formatted this way, I'm looking for a generic way to fix this (replace the id-reference with the full object) in pure javascript.
Any common solutions to the seemingly common issue?
PS: I'm sorry for the title, I don't know the right term (suggestions are welcome).
You could simply map a function that makes these corrections to the initial value. Something like this should work if I understand what you are going for:
response = response.map( function(x){
if( (typeof x.role) == "number" )
x.role = { id : x.role };
return x;
});
This however will not give you the names and users field. If this is important, I recommend we iterate through the data once and make a list of roles, then go back and fill in the gaps. This would look like:
roles = {};
response.forEach( function(x){
if( (typeof x.role) != "number" )
roles[x.role.id] = x.role;
});
response = response.map( function(x){
if( (typeof x.role) == "number" )
x.role = roles[x.role];
return x;
});
The output of running that on your data is:
[
{ id: 0,
username: 'someone0',
areas: [],
role: { id: 2, name: 'somerole2', users: null }
},
{ id: 1,
username: 'someone1',
areas: [],
role: { id: 1, name: 'somerole1', users: null }
},
{ id: 3,
username: 'someone3',
areas: [],
role: { id: 1, name: 'somerole1', users: null }
}
]

Rethinkdb append to array if exists

In RethinkDB, I have a table authors with the following layout:
{
id: 12,
videos: [1,2,3]
}
Now I get new authors with objects like this:
{
id: 12,
videos: [4,5]
}
If the author now already exists, I want to append the new videos 4 and 5 to the list of videos.
If author not exists, just insert the document like it is.
My approach was the following, but it didn't work.
r.table('authors').getAll(4, 3, 2, 1, {index: 'id'})
.replace(function(author, index) {
return r.branch(
author.eq(null),
{
id: index,
videos: [1,2,3]
},
author
);
})
-> Response:
{
"deleted": 0 ,
"errors": 3 ,
"first_error": "Expected 2 arguments but found 1." ,
"inserted": 0 ,
"replaced": 0 ,
"skipped": 0 ,
"unchanged": 0
}
Thanks!
Your logic is very good. It is just some syntax issue.
Given an author, if the author isn't existed, insert it, otherwise, append the video array, here is what I think of, using your logic:
var author = {
id: 12,
videos: [9, 10]
};
r.table('authors').insert(author).do(
function (doc) {
return r.branch(doc('inserted').ne(0),
r.expr({inserted: 1}),
r.table('authors').get(author["id"]).update(function(doc) {
return {videos: doc('videos').union(author["videos"])}
})
)
}
)
If the insert is sucesfully, it means we have no document with the same id, we don't have to do anything. Otherwise, we will update the document and append the videos into it.
To updare an array of multiple author, we can use foreach and expr to turn array into ReQL object. However, in this case, we use bracket to get field instead of using [] as in JavaScript object
var authors = [{
id: 12,
videos: [90, 91]
},{
id: 14,
videos: [1, 2]
}];
r.expr(authors).forEach(function(author) {
return r.table('authors').insert(author).do(
function (doc) {
return r.branch(doc('inserted').ne(0),
r.expr({inserted: 1}),
r.table('authors').get(author("id")).update(function(doc) {
return {videos: doc('videos').union(author("videos"))}
})
)
}
)
})
Something like this should do it:
r([4, 3, 2, 1]).foreach(function(id) {
return r.table('authors').get(id).replace(function(row) {
return r.branch(row.eq(null), {id: id, videos: [1, 2, 3]}, row);
});
});
lambda for replace method has only 1 argument. But you are trying too pass 2 args: author, index.

Categories

Resources