Level 2 (related model) scope over REST - strongloop api - node.js

I found in the documentation that scopes enable you to specify commonly-used queries that you can reference as method calls on a model. Below i have a categories model. I am trying to create scopes that applies to the relation with model games. Unfortunately the below does nothing. How can I get scopes to apply to relation as shown below?
GET /Categories/{id}/games - This gets all games
common/models/category.json
"relations": {
"categories": {
"type": "hasMany",
"model": "game",
"foreignKey": ""
}
},
/common/models/game.json
"scopes": {
"mature": {"where": {"mature": true}}
},
"validations": [],
"relations": {
"category": {
"type": "belongsTo",
"model": "category",
"foreignKey": ""
}
}
I want to be able to get the data through endpoing: /Categories/{id}/games/mature
Table schema:
catgories
category_name category_id
------------- -----------
fighting 1001
racing 1002
sports 1003
games
game_id game_name category_id mature
----------- ------------ ----------- --------------
13KXZ74XL8M Tekken 10001 true
138XZ5LPJgM Forza 10002 false

Loopback api is based on swagger and scopes is a new concept in loopback.
Currently it doesn't have support for related methods i.e. you cannot access it from a related model i.e category (in your case) but only from model where the scope is defined i.e. game.
Thus you can achieve what you want right now using remote methods.
By Using Remote Methods. Loopback Remote Methods
common/models/category.js add the following lines.
module.exports = function(Category) {
Category.mature = function(id, filter, callback) {
var app = this.app;
var Game = app.models.Game;
if(filter === undefined){
filter = {};
}
filter.where = filter.where || {};
filter.where.categoryId = id;
filter.where.mature = true;
Game.find(filter, function(err, gameArr) {
if (err) return callback(err);
console.log(gameArr);
callback(null, gameArr);
});
}
Category.remoteMethod(
'mature', {
accepts: [{
arg: 'id',
type: 'number',
required: true
},
{
arg: 'filter',
type: 'object',
required: false
}
],
// mixing ':id' into the rest url allows $owner to be determined and used for access control
http: {
path: '/:id/games/mature',
verb: 'get'
},
returns: {
arg: 'games',
type: 'array'
}
}
);
};
Now in your common/models/category.json
Add this to your ACL property.
.....
.....
"acls": [
{
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "ALLOW",
"property": "mature"
}
]
Now you can get all your game of mature type by get method
http://0.0.0.0:3000/api/categories/:id/games/mature
Or you can also try the API from loopback-explorer now.

Related

Right outer join in aggregation pipeline

I have two collections, let's call them Cats and Parties, with the following schemas:
Cat
{ name: String }
Party
{ date: Date, attendants: [{ cat: { ref: 'Cat' }, role: String }] }
where role symbolizes some other attribute, say, whether the attending cat is a VIP member.
Now I want to get a list of all cats that exist (even those poor kitties who never attended any party) and for each cat, I want a list of all the roles it ever had for at least one party. Furthermore, I want this entire list to be sorted by the (per cat) last attended party's date with cats who never attended any party being last.
This raises the following problems for me:
Aggregrating over Parties excludes party-pooper kitties who never joined a party.
Aggregating over Cats sort of goes »the wrong way« because I cannot $lookup parties the cat attended because that information is in a subdocument array.
The pipeline I currently have gives me all cats who attended at least one party with a list of their roles, but doesn't sort by the last attended party. In fact, I could live with excluding cats who never attended a party, but the sorting is crucial for me:
Party.aggregate([
{ $unwind: '$attendants' },
{ $project: { role: '$attendants.role', cat: '$attendants.cat' } },
{
$group: {
_id: '$cat',
roles: { $addToSet: '$role' }
}
},
{
$lookup: {
from: 'cats',
localField: '_id',
foreignField: '_id',
as: 'cat'
}
},
{ $unwind: '$cat' },
// (*)
{ $addFields: { 'cat.roles': '$roles' } },
{ $replaceRoot: { newRoot: '$cat' } }
])
My current idea would basically be a right outer join at (*) to add a list of parties the cat has attended, $project that to the party's date and then $group using $max to get the latest date. Then I can $unwind that now one-element array and $sort over it in the end.
The problem is that right outer joins don't exist in mongo, AFAIK, and I don't know how to get that list of parties per cat within the pipeline.
To clarify, the expected output should be something like
[
{
"_id": "59982d3c7ca25936f8c327c8",
"name": "Mr. Kitty",
"roles": ["vip", "birthday cat"],
"dateOfLastParty": "2017-06-02"
},
{
"_id": "59982d3c7ca25936f8c327c9",
"name": "Snuffles",
"roles": ["best looking cat"],
"dateOfLastParty": "2017-06-01"
},
...
{
"_id": "59982d3c7ca25936f8c327c4",
"name": "Sad Face McLazytown",
"roles": [],
"dateOfLastParty": null
},
]
As stated, you want the "cats" so use the Cat model and do the "left outer join" that is actually inherent to $lookup, rather than asking for a "right outer join" from the opposing collection, since a "right outer join" is not possible with MongoDB at this time.
It's also far more practical as a "left join", because you want "cats" as your primary source of output. The only thing to consider when linking to "Party" is that each "Cat" is listed in an array, and therefore you get the whole document back. So all that needs to be done is in "post processing" after the $lookup, you simply "filter" the array content for the matching entry of the current cat.
Fortunately we get good features with $arrayElemAt and $indexOfArray, that allow us to do that exact extraction:
let kitties = await Cat.aggregate([
{ '$lookup': {
'from': Party.collection.name,
'localField': '_id',
'foreignField': 'attendants.cat',
'as': 'parties'
}},
{ '$replaceRoot': {
'newRoot': {
'$let': {
'vars': {
'parties': {
'$map': {
'input': '$parties',
'as': 'p',
'in': {
'date': '$$p.date',
'role': {
'$arrayElemAt': [
'$$p.attendants.role',
{ '$indexOfArray': [ '$$p.attendants.cat', '$_id' ] }
]
}
}
}
}
},
'in': {
'_id': '$_id',
'name': '$name',
'roles': '$$parties.role',
'dateOfLastParty': { '$max': '$$parties.date' }
}
}
}
}}
]);
So my concept of "optimal" processing here actually uses $replaceRoot here because you can define the whole document under a $let statement. The reason I'm doing that is so we can take the "parties" array output from the previous $lookup and reshape each entry extracting the matching "role" data for the current "kitty" at that given party. This we can actually make a variable itself.
The reason for the "array variable" is because we can then use $max to extract the "largest/last" date property as "singular" and still extract the "role" values as an "array" from that reshaped content. This makes it easy to define the fields you wanted.
And since it was a "left join" started from Cat in the first place, then those poor kitties that missed out on all parties are still there, and still have the desired output.
Two aggregation pipeline stages. What could be more simple!
As a full listing:
const mongoose = require('mongoose'),
Schema = mongoose.Schema;
mongoose.Promise = global.Promise;
mongoose.set('debug',true);
const uri = 'mongodb://localhost/catparty',
options = { useMongoClient: true };
const catSchema = new Schema({
name: String
});
const partySchema = new Schema({
date: Date,
attendants: [{
cat: { type: Schema.Types.ObjectId, ref: 'Cat' },
role: String
}]
});
const Cat = mongoose.model('Cat', catSchema);
const Party = mongoose.model('Party', partySchema);
function log(data) {
console.log(JSON.stringify(data,undefined,2))
}
(async function() {
try {
const conn = await mongoose.connect(uri,options);
// Clean collections
await Promise.all(
Object.keys(conn.models).map( m => conn.models[m].remove({}) )
);
var cats = await Cat.insertMany(
['Fluffy', 'Snuggles', 'Whiskers', 'Socks'].map( name => ({ name }) )
);
cats.shift();
cats = cats.map( (cat,idx) =>
({ cat: cat._id, role: (idx === 0) ? 'Host' : 'Guest' })
);
log(cats);
let party = await Party.create({
date: new Date(),
attendants: cats
});
log(party);
let kitties = await Cat.aggregate([
{ '$lookup': {
'from': Party.collection.name,
'localField': '_id',
'foreignField': 'attendants.cat',
'as': 'parties'
}},
{ '$replaceRoot': {
'newRoot': {
'$let': {
'vars': {
'parties': {
'$map': {
'input': '$parties',
'as': 'p',
'in': {
'date': '$$p.date',
'role': {
'$arrayElemAt': [
'$$p.attendants.role',
{ '$indexOfArray': [ '$$p.attendants.cat', '$_id' ] }
]
}
}
}
}
},
'in': {
'_id': '$_id',
'name': '$name',
'roles': '$$parties.role',
'dateOfLastParty': { '$max': '$$parties.date' }
}
}
}
}}
]);
log(kitties);
} catch(e) {
console.error(e);
} finally {
mongoose.disconnect();
}
})();
And example output:
[
{
"_id": "59a00d9528683e0f59e53460",
"name": "Fluffy",
"roles": [],
"dateOfLastParty": null
},
{
"_id": "59a00d9528683e0f59e53461",
"name": "Snuggles",
"roles": [
"Host"
],
"dateOfLastParty": "2017-08-25T11:44:21.903Z"
},
{
"_id": "59a00d9528683e0f59e53462",
"name": "Whiskers",
"roles": [
"Guest"
],
"dateOfLastParty": "2017-08-25T11:44:21.903Z"
},
{
"_id": "59a00d9528683e0f59e53463",
"name": "Socks",
"roles": [
"Guest"
],
"dateOfLastParty": "2017-08-25T11:44:21.903Z"
}
]
And you should be able to see how those "roles" values actually become an array with more data. And if you need that to be a "unique list", then simply wrap with $setDifference as in:
'roles': { '$setDifference': [ '$$parties.role', [] ] },
And that is also covered

How to create many to many relationship between models in loopback?

I might be missing something obvious here but have spent hours on this and unable to find a solution.
Suppose I have an employee model
"properties": {
"name": {
"type": "string",
"required": true
},
"age": {
"type": "number",
"required": true
}
};
and its child model
"properties": {
"Code": {
"type": "string",
"required": true
},
"Desc": {
"type": "string",
"required": true
}
}
How do I create a many to many relationship between them?
You can add accepted properties passing accepts fields to remoteMethod options.
Read this page in the documentation.
module.exports = function(Task) {
fs.readdir(PATH_PROCESS+PATH_API, (err, o) => {
for(var c in o){
var i = o[c];
Task[i] = require(PATH_PROCESS+i+".js").lpb;
Task.remoteMethod(
    i, {
http: {path: ('/'+i), verb: 'post'},
returns: {arg: 'result', type: 'object'},
accepts: [
{arg: 'params', type: 'object', required: true, http: {source: 'body'},
description: (i+' params.')}
]
    }
    );
}
});
};
From the docs:
A hasManyThrough relation sets up a many-to-many connection with another model. This relation indicates that the declaring model can be matched with zero or more instances of another model by proceeding through a third model.
Use slc loopback:relation command to create relations between models. Don't forget to add through model when prompted (also explained in docs).
After you create relation between your models you will have to synchronize your changes with database using automigrate() or autoupdate().
Be careful when using automigrate because it will create or re-create your database which means that you can potentially loose your data.

MongoDB: Query model and check if document contains object or not, then mark / group result

I have a Model called Post, witch contains an property array with user-ids for users that have liked this post.
Now, i need to query the post model, and mark the returned results with likedBySelf true/false for use in by client - is this possible?
I dont have to store the likedBySelf property in the database, just modify the results to have that property.
A temporary solution i found was to do 2 queries, one that finds the posts that is liked by user x, and the ones that have not been liked by user x, and en map (setting likedBySelf true/false) and combine the 2 arrays and return the combined array. But this gives some limitations to other query functions such as limit and skip.
So now my queries looks like this:
var notLikedByQuery = Post.find({likedBy: {$ne: req.body.user._id}})
var likedByQuery = Post.find({likedBy: req.body.user._id})
(I'm using the Mongoose lib)
PS. A typical post can look like this (JSON):
{
"_id": {
"$oid": "55fc463c83b2d2501f563544"
},
"__t": "Post",
"groupId": {
"$oid": "55fc463c83b2d2501f563545"
},
"inactiveAfter": {
"$date": "2015-09-25T17:13:32.426Z"
},
"imageUrl": "https://hootappprodstorage.blob.core.windows.net/devphotos/55fc463b83b2d2501f563543.jpeg",
"createdBy": {
"$oid": "55c49e2d40b3b5b80cbe9a03"
},
"inactive": false,
"recentComments": [],
"likes": 8,
"likedBy": [
{
"$oid": "558b2ce70553f7e807f636c7"
},
{
"$oid": "559e8573ed7c830c0a677c36"
},
{
"$oid": "559e85bced7c830c0a677c43"
},
{
"$oid": "559e854bed7c830c0a677c32"
},
{
"$oid": "559e85abed7c830c0a677c40"
},
{
"$oid": "55911104be2f86e81d0fb573"
},
{
"$oid": "559e858fed7c830c0a677c3b"
},
{
"$oid": "559e8586ed7c830c0a677c3a"
}
],
"location": {
"type": "Point",
"coordinates": [
10.01941398718396,
60.96738099591897
]
},
"updatedAt": {
"$date": "2015-09-22T08:45:41.480Z"
},
"createdAt": {
"$date": "2015-09-18T17:13:32.426Z"
},
"__v": 8
}
#tskippe you can use a method like following to process whether the post is liked by the user himself and call the function anywhere you want.
var processIsLiked = function(postId, userId, doc, next){
var q = Post.find({post_id: postId});
q.lean().exec(function(err,res){
if(err) return utils.handleErr(err, res);
else {
if(_.find(doc.post.likedBy,userId)){ //if LikedBy array contains the user
doc.post.isLiked = true;
} else {
doc.post.isLiked = false;
}
});
next(doc);
}
});
}
Because you are using q.lean() you dont need to actually persist the data. You need to just process it , add isLiked field in the post and send back the response. **note that we are manuplating doc directly. Also you chan tweek it to accept doc containing array of posts and iterating it and attach an isLiked field to each post.
I found that MongoDB's aggregation with $project tequnique was my best bet. So i wrote up an aggregation like this.
Explanation:
Since i want to keep the entire document, but $project purpose is to modify the docs, thus you have to specify the properties you want to keep. A simple way of keeping all the properties is to use "$$ROOT".
So i define a $project, set all my original properties to doc: "$$ROOT", then create a new property "likedBySelf", which is marked true / false if a specified USERID is in the $likedBy set.
I think that this is more clean and simple, than querying every single model after a query to set a likedBySelf flag. It may not be faster, but its cleaner.
Model.aggregate([
{ $project: {
doc: "$$ROOT",
likedBySelf: {
$cond: {
"if": { "$setIsSubset": [
[USERID],
"$likedBy"
]},
"then": true,
"else": false
}
}
}}
]);

nodejs/loopback: defining relationships doesn't seem to be reflected on database?

I am creating an API with strongloop loopback.
I have defined my models, and basically all is good there.
But I have a problem understanding how loopback deals with relationships.
Not all of my relationships I defined seem to really be reflected in the database and the interface.
For example, I have a model song, it
hasAndBelongsToMany albums
hasAndBelongsToMany playlists
hasAndBelongsToMany userplaylists
belongsTo artist
Here is /common/models/song.json
{
"name": "song",
"plural": "song",
"base": "PersistedModel",
"idInjection": true,
"properties": {
//some more properties of song
},
"validations": [],
"relations": {
"albums": {
"type": "hasAndBelongsToMany",
"model": "album",
"foreignKey": ""
},
"artist": {
"type": "belongsTo",
"model": "artist",
"foreignKey": ""
},
"playlists": {
"type": "hasAndBelongsToMany",
"model": "playlist",
"foreignKey": ""
},
"userplaylists": {
"type": "hasAndBelongsToMany",
"model": "userplaylist",
"foreignKey": ""
}
},
"acls": [],
"methods": []
}
But when I look at the postgresql table generated, I see:
title | character varying(1024) | not null
id | integer | not null default nextval('song_id_seq'::regclass)
#some other properties of song
artistid | integer |
Accordingly, the interface in loopbacks explorer at localhost:3000/explorer says:
post /song
Response Class
Model
Model Schema
{
"title": "",
//some other properties of song
"id": 0,
"artistId": 0
}
The question: Shouldn't there also be a songs, a playlists and a userplaylists variable??? Or have I been working too much in the NoSql world and now I forgot how to handle relationships?
BTW. I have a migrate script which I executed when adding the relationships to the models:
var path = require('path');
var app = require(path.resolve(__dirname, '../server'));
var dataSource = app.dataSources.cantoalegre_ps_DS;
dataSource.automigrate(function(err) {
if (err) {
console.log("Error migrating models: " + err);
}
else {
console.log("Successfully migrated models");
}
process.exit();
});
usually related data has the foreign key:
customer,
order belongs to customer so order has a column that contains the customer id.
Take care that at every change models have to be synchronized to the db by an autoupdate script.
module.exports = function(app) {
var ds = app.dataSources.pg;
ds.isActual('mymodel', function(err, actual) {
if (!actual) {
ds.autoupdate('mymodel', function(err, result) {
console.log("AUTOUPDATE mymodel",err,result);
});
}
});
};

Strongloop: how to fetch a related model using code (not REST API)

Having trouble getting a related model on a User object. Users have a to-many relation with Customers.
Can I not just say User.customers to grab the customers associated with a User?
I have tried
User.find({include:'customers'}, function(err, user) {
//now what?
//user.customers does not work; there is no Customer array returned.
});
Happy to look in the docs for this but I can't find where this is written.
Thank you
In the loopback examples they often create a "user" model as an extension of loopbacks "User" model.
Note the lower case u.
I had trouble accessing the model when using "User" not "user"
user.json
{
"name": "user",
"base": "User",
"idInjection": true,
"emailVerificationRequired": false,
"properties": {
"createdAt": {
"type": "date"
},
"updatedAt": {
"type": "date"
},
.......
user.js
module.exports = function(user) {
user.observe('before save', function(ctx, next){
if (ctx.instance) {
//If created at is defined
if(ctx.instance.createdAt){
ctx.instance.updatedAt = new Date();
}
else{
ctx.instance.createdAt = ctx.instance.updatedAt = new Date();
}
} else {
ctx.data.updatedAt = new Date();
}
next();
})`

Resources