I'm fairly new to Fauna so please forgive me. I'd like to make one query that returns a nested document within a document. I access the initial document that contains the nested document by the index I created below:
Get(Match(Index("account-user_by_sub"), "google-oauth2|10233470761")
That index returns this:
{
ref: Ref(Collection("account-users"), "325230990747238466"),
ts: 1646423292780000,
data: {
userAccount: Ref(Collection("user-accounts"), "325134359745003585"),
firstName: "firstName",
lastName: "lastName",
sub: "google-oauth2|10233470761",
}
}
I'd like to make one query that returns the above response along with the nested userAccount document. I found similar questions on Stackoverflow but didn't have any luck with their solutions. I tried this below but it just returned the block of code you see above:
Get(Match(Index("account-user_by_sub"), "google-oauth2|10233470761")),
Lambda("X",
Let(
{
accountUserDoc: Get(Var("X")),
userAccountRef: Get(Select(["data", "userAccount"], Var("accountUserDoc")))
},
{
accountUser: Var("accountUserDoc"),
userAccount: Get(Var("userAccountRef"))
}
)
)
The query you tried won't work: it tries to chain two FQL expressions together with a comma, and there's no connection between the two, so the Lambda doesn't receive a parameter.
You're pretty close to the solution though. Try this:
Let(
{
accountUserDoc: Get(
Match(
Index("account-user_by_sub"),
"google-oauth2|10233470761"
)
),
userAccountDoc: Get(
Select(["data", "userAccount"], Var("accountUserDoc"))
)
},
{
accountUser: Var("accountUserDoc"),
userAccount: Var("userAccountDoc")
}
)
Basically, this places the initial Get inside the Let, rather than trying to pass the document as a parameter.
Since you are calling Get on the value in the userAccount field, you didn't need to do it again when composing the desired result; that would have failed anyway because Get takes a reference, not a document.
Related
I'm bumbling my way through adding a back-end to my site and have decided to get acquainted with graphQL. I may be structuring things totally the wrong way, however from following some tutorials I have a React front-end (hosted on Vercel), so I have created an api folder in my app to make use of Vercel's serverless functions. I'm using Apollo server and I decided to go with Fauna as my database.
I've successfully been able to return an entire collection via my API. Now I wish to be able to return the collection sorted by my id field.
To do this I created an index which looks like this:
{
name: "sort_by_id",
unique: false,
serialized: true,
source: "my_first_collection",
values: [
{
field: ["data", "id"]
},
{
field: ["ref"]
}
]
}
I then was able to call this via my api and get back and array, which simply contained the ID + ref, rather than the associated documents. I also could only console log it, I assume because the resolver was expecting to be passed an array of objects with the same fields as my typedefs. I understand I need to use the ref in order to look up the documents, and here is where I'm stuck. An index record looks as follows:
[1, Ref(Collection("my_first_collection"), "352434683448919125")]
In my resolvers.js script, I am attempting to receive the documents of my sorted index list. I've tried this:
async users() {
const response = await client.query(
q.Map(
q.Paginate(
q.Match(
q.Index('sort_by_id')
)
),
q.Lambda((ref) => q.Get(ref))
)
)
const res = response.data.map(item => item.data);
return [... res]
}
I'm unsure if the problem is with how I've structured my index, or if it is with my code, I'd appreciate any advice.
It looks like you also asked this question on the Fauna discourse forums and got an answer there: https://forums.fauna.com/t/unable-to-return-a-list-of-documents-via-an-index/3511/2
Your index returns a tuple (just an array in Javascript) of the data.id field and the ref. You confirmed that with your example result
[
/* data.id */ 1,
/* ref */ Ref(Collection("my_first_collection"), "352434683448919125")
]
When you map over those results, you need to Get the Ref. Your query uses q.Lambda((ref) => q.Get(ref)) which passes the whole tuple to Get
Instead, use:
q.Lambda(["id", "ref"], q.Get(q.Var("ref")))
// or with JS arrow function
q.Lambda((id, ref) => q.Get(ref))
or this will work, too
q.Lambda("index_entry", q.Get(q.Select(1, q.Var("index_entry"))))
// or with JS arrow function
q.Lambda((index_entry) => q.Get(q.Select(1, index_entry)))
The point is, only pass the Ref to the Get function.
In Mongoose, I can use a query populate to populate additional fields after a query. I can also populate multiple paths, such as
Person.find({})
.populate('books movie', 'title pages director')
.exec()
However, this would generate a lookup on book gathering the fields for title, pages and director - and also a lookup on movie gathering the fields for title, pages and director as well. What I want is to get title and pages from books only, and director from movie. I could do something like this:
Person.find({})
.populate('books', 'title pages')
.populate('movie', 'director')
.exec()
which gives me the expected result and queries.
But is there any way to have the behavior of the second snippet using a similar "single line" syntax like the first snippet? The reason for that, is that I want to programmatically determine the arguments for the populate function and feed it in. I cannot do that for multiple populate calls.
After looking into the sourcecode of mongoose, I solved this with:
var populateQuery = [{path:'books', select:'title pages'}, {path:'movie', select:'director'}];
Person.find({})
.populate(populateQuery)
.execPopulate()
you can also do something like below:
{path:'user',select:['key1','key2']}
You achieve that by simply passing object or array of objects to populate() method.
const query = [
{
path:'books',
select:'title pages'
},
{
path:'movie',
select:'director'
}
];
const result = await Person.find().populate(query).lean();
Consider that lean() method is optional, it just returns raw json rather than mongoose object and makes code execution a little bit faster! Don't forget to make your function (callback) async!
This is how it's done based on the Mongoose JS documentation http://mongoosejs.com/docs/populate.html
Let's say you have a BookCollection schema which contains users and books
In order to perform a query and get all the BookCollections with its related users and books you would do this
models.BookCollection
.find({})
.populate('user')
.populate('books')
.lean()
.exec(function (err, bookcollection) {
if (err) return console.error(err);
try {
mongoose.connection.close();
res.render('viewbookcollection', { content: bookcollection});
} catch (e) {
console.log("errror getting bookcollection"+e);
}
//Your Schema must include path
let createdData =Person.create(dataYouWant)
await createdData.populate([{path:'books', select:'title pages'},{path:'movie', select:'director'}])
When I update a model, waterlock .update() always return an array of objects, even if I set on criteria a primaryKey.
on my code
Ad.update({ id: req.param('id') }, {
// desired attributed to be updated
}).exec(function(err, updatedRecord) {
// updatedRecord is always an array of objects
});
And in order to use the updatedRecord, I have to point out to 0 index like updatedRecord[0] which is something I consider not very clean. According to docs update() in sails, this is a common escenario.
Knowing that, I have 2 questions:
Wouldn't be better that when you find one model return just a updated object for that model, not an array?
If that is a convention, how could be overrided this function in order to return just an object instead of an array when .update() have only affected one record?
it is a convention that it will update all the records that matches the find criteria, but as you are probably using a unique validation on model, it will probably return an array of 1 or 0. You need to do it on hand.
You can override methods in model, by implementing a method with same name as waterline default. But as you will need to completely rewrite the code, it is not viable. Neither changing waterline underlying code.
A solution will be creating a new function on your Ad model:
module.exports = {
attributes: {
adid: {
unique: true,
required: true
},
updateMe: {
}
},
updateOne: function(adid, newUpdateMe, cb){
Ad.update({ id: req.param('id') }, {
// desired attributed to be updated
}).exec(function(err, updatedRecord) {
// updatedRecord is always an array of objects
if (updatedRecord.length == 1){
return cb(null, updatedRecord[0]);
}
return cb(null, {}); //also can error if not found.
});
}
};
Also. Avoid using id as an model attribute (use other name), as some databases like mongodb already add this attribute as default and may cause conflicts with your model.
I dont think its possible with waterline. Its because update method is a generalized one, passing a primary key in where condition is always not the case.
Let's say I have the following document schema in a collection called 'users':
{
name: 'John',
items: [ {}, {}, {}, ... ]
}
The 'items' array contains objects in the following format:
{
item_id: "1234",
name: "some item"
}
Each user can have multiple items embedded in the 'items' array.
Now, I want to be able to fetch an item by an item_id for a given user.
For example, I want to get the item with id "1234" that belong to the user with name "John".
Can I do this with mongoDB? I'd like to utilize its powerful array indexing, but I'm not sure if you can run queries on embedded arrays and return objects from the array instead of the document that contains it.
I know I can fetch users that have a certain item using {users.items.item_id: "1234"}. But I want to fetch the actual item from the array, not the user.
Alternatively, is there maybe a better way to organize this data so that I can easily get what I want? I'm still fairly new to mongodb.
Thanks for any help or advice you can provide.
The question is old, but the response has changed since the time. With MongoDB >= 2.2, you can do :
db.users.find( { name: "John"}, { items: { $elemMatch: { item_id: "1234" } } })
You will have :
{
name: "John",
items:
[
{
item_id: "1234",
name: "some item"
}
]
}
See Documentation of $elemMatch
There are a couple of things to note about this:
1) I find that the hardest thing for folks learning MongoDB is UN-learning the relational thinking that they're used to. Your data model looks to be the right one.
2) Normally, what you do with MongoDB is return the entire document into the client program, and then search for the portion of the document that you want on the client side using your client programming language.
In your example, you'd fetch the entire 'user' document and then iterate through the 'items[]' array on the client side.
3) If you want to return just the 'items[]' array, you can do so by using the 'Field Selection' syntax. See http://www.mongodb.org/display/DOCS/Querying#Querying-FieldSelection for details. Unfortunately, it will return the entire 'items[]' array, and not just one element of the array.
4) There is an existing Jira ticket to add this functionality: it is https://jira.mongodb.org/browse/SERVER-828 SERVER-828. It looks like it's been added to the latest 2.1 (development) branch: that means it will be available for production use when release 2.2 ships.
If this is an embedded array, then you can't retrieve its elements directly. The retrieved document will have form of a user (root document), although not all fields may be filled (depending on your query).
If you want to retrieve just that element, then you have to store it as a separate document in a separate collection. It will have one additional field, user_id (can be part of _id). Then it's trivial to do what you want.
A sample document might look like this:
{
_id: {user_id: ObjectId, item_id: "1234"},
name: "some item"
}
Note that this structure ensures uniqueness of item_id per user (I'm not sure you want this or not).
what seemed a simple task, came to be a challenge for me.
I have the following mongodb structure:
{
(...)
"services": {
"TCP80": {
"data": [{
"status": 1,
"delay": 3.87,
"ts": 1308056460
},{
"status": 1,
"delay": 2.83,
"ts": 1308058080
},{
"status": 1,
"delay": 5.77,
"ts": 1308060720
}]
}
}}
Now, the following query returns whole document:
{ 'services.TCP80.data.ts':{$gt:1308067020} }
I wonder - is it possible for me to receive only those "data" array entries matching $gt criteria (kind of shrinked doc)?
I was considering MapReduce, but could not locate even a single example on how to pass external arguments (timestamp) to Map() function. (This feature was added in 1.1.4 https://jira.mongodb.org/browse/SERVER-401)
Also, there's always an alternative to write storedJs function, but since we speak of large quantities of data, db-locks can't be tolerated here.
Most likely I'll have to redesign the structure to something 1-level deep, like:
{
status:1,delay:3.87,ts:138056460,service:TCP80
},{
status:1,delay:2.83,ts:1308058080,service:TCP80
},{
status:1,delay:5.77,ts:1308060720,service:TCP80
}
but DB will grow dramatically, since "service" is only one of many options which will append each document.
please advice!
thanks in advance
In version 2.1 with the aggregation framework you are now able to do this:
1: db.test.aggregate(
2: {$match : {}},
3: {$unwind: "$services.TCP80.data"},
4: {$match: {"services.TCP80.data.ts": {$gte: 1308060720}}}
5: );
You can use a custom criteria in line 2 to filter the parent documents. If you don't want to filter them, just leave line 2 out.
This is not currently supported. By default you will always receive the whole document/array unless you use field restrictions or the $slice operator. Currently these tools do not allow filtering the array elements based on the search criteria.
You should watch this request for a way to do this: https://jira.mongodb.org/browse/SERVER-828
I'm attempting to do something similar. I tried your suggestion of using the GROUP function, but I couldn't keep the embedded documents separate or was doing something incorrectly.
I needed to pull/get a subset of embedded documents by ID. Here's how I did it using Map/Reduce:
db.parent.mapReduce(
function(parent_id, child_ids){
if(this._id == parent_id)
emit(this._id, {children: this.children, ids: child_ids})
},
function(key, values){
var toReturn = [];
values[0].children.forEach(function(child){
if(values[0].ids.indexOf(product._id.toString()) != -1)
toReturn.push(child);
});
return {children: toReturn};
},
{
mapparams: [
"4d93b112c68c993eae000001", //example parent id
["4d97963ec68c99528d000007", "4debbfd5c68c991bba000014"] //example embedded children ids
]
}
).find()
I've abstracted my collection name to 'parent' and it's embedded documents to 'children'. I pass in two parameters: The parent document ID and an array of the embedded document IDs that I want to retrieve from the parent. Those parameters are passed in as the third parameter to the mapReduce function.
In the map function I find the parent document in the collection (which I'm pretty sure uses the _id index) and emit its id and children to the reduce function.
In the reduce function, I take the passed in document and loop through each of the children, collecting the ones with the desired ID. Looping through all the children is not ideal, but I don't know of another way to find by ID on an embedded document.
I also assume in the reduce function that there is only one document emitted since I'm searching by ID. If you expect more than one parent_id to match, than you will have to loop through the values array in the reduce function.
I hope this helps someone out there, as I googled everywhere with no results. Hopefully we'll see a built in feature soon from MongoDB, but until then I have to use this.
Fadi, as for "keeping embedded documents separate" - group should handle this with no issues
function getServiceData(collection, criteria) {
var res=db[collection].group({
cond: criteria,
initial: {vals:[],globalVar:0},
reduce: function(doc, out) {
if (out.globalVar%2==0)
out.vals.push({doc.whatever.kind.and.depth);
out.globalVar++;
},
finalize: function(out) {
if (vals.length==0)
out.vals='sorry, no data';
return out.vals;
}
});
return res[0];
};