Create a Notification System Based on User Preferences - mongodb

I am trying to develop a notification system, but I am not sure if I do certain parts correctly. To simplify the case I will use some generic naming.
How the system should work:
A registered user can subscribe for notifications based on chosen filters from a data table grid. ( for example, notify me when the quantity of an item is X, or have multiple filters set up like set1.slug.quantity > X, some_value = false and some_int = 52)
How I store such preferences:
Example object
"O:8:"stdClass":2:{s:9:"set1.slug";a:1:{s:3:"$eq";s:10:"item_slug1";}s:13:"set1.quantity";a:1:{s:4:"$gte";i:1;}}"
Generation simplified
$object = new \stdClass();
$object->{'set1.slug'} = ['$eq' => 'item_slug1'];
$object->{'set1.quantity'} = ['$gte' => 1];
$object = serialize($object);
It attaches also the user_id and all the data serialized from the form to a partial MongoDB raw query.
Database stored object - predefined filter set for a user. Md is an md5 hash of the object for easier access and edit.
+----+-----------------------------------------------------------------------------------------------------------------+---------+----------------------------------+
| id | object | user_id | md |
+----+-----------------------------------------------------------------------------------------------------------------+---------+----------------------------------+
| 1 | O:8:"stdClass":2:{s:9:"set1.slug";a:1:{s:3:"$eq";s:10:"item_slug1";}s:13:"set1.quantity";a:1:{s:4:"$gte";i:1;}} | 22 | d5003ba3227c4db3189827329815b053 |
+----+-----------------------------------------------------------------------------------------------------------------+---------+----------------------------------+
This is how I would use it - my vision of how it would work.
I would call findByFilterMD in the API parser in a loop, while the table Items is populated.
// MongoDB query ( items database )
protected function executeUserFilter($array)
{
$list = Items::raw(function ($collection) use (&$array) {
return $collection->find(
$array, ["typeMap" => ['root' => 'array', 'document' => 'array']])
->toArray();
});
return $list;
}
// MySQL query - stored filters
protected function findByFilterMD($id)
{
$user = Auth::user();
$filter = PredefinedFilters::where('md', '=', $id)->first();
$deserialize = unserialize($filter->object);
$results = $this->executeUserFilter($deserialize);
// here would probably be a notification function like pusher or onesignal
}
I am aware that my attempt of achieving this might be totally wrong and I might reinvent the wheel since some tools might do that already.
Here is an example Item MongoDB object
{
"_id": {
"$oid": "5c0406abdc04e7007f17f4ef"
},
"name": "myObject",
"inner_ID": "0db0b19a-9c01-4c21-8e10-6879dbcb37f1",
"some_value": false,
"some_int": 52,
"set1": [
{
"slug": "item_slug1",
"quantity": 88,
"extra": {
"value": 0
}
},
],
"set2": [
{
"slug": "item_slug2",
"quantity": 88,
"extra": {
"value": 0
}
},
{
"slug": "item_slug3",
"quantity": 88,
"extra": {
"value": 0
}
}
],
"expires": "2018-12-02 22:21:30"
}
Here comes my questions
Is this way of doing it proper?
Where should the notification system kick in? I assume it might be in the place where I parse the api of items, then I should loop over the user filter data and run the stored object query - or should it be a separate system called with cron?
I am open to any suggestions, redesigns.

I developed an app kinda like this. My approach is to make use of Laravel notification found here:
https://laravel.com/docs/5.7/notifications#creating-notifications
Let's say, in your case, if someone modify/create data, the other users who subscribe will get notification.
Create notification
php artisan make:notification UserUpdateQuantity
Make User model notifiable, also create scope that subscribe for something
<?php
namespace App;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable
{
use Notifiable;
public function scopeSubscribe($query)
{
return $query->where('subscribe', true);
}
}
the $query in method scopeSubscribe needs to be adjusted based on your business logic
Send notification
$subscriber = User::subscribe()->get();
Notification::send($subscriber, new UserUpdateQuantity());
Create event & listener
You can find the event and listener here https://laravel.com/docs/5.7/events
In the EventServiceProvider
protected $listen = [
'App\Events\QuantityUpdated' => [
'App\Listeners\SendUpdateQuantiryNotification',
],
];
Then run the command php artisan event:generate
Event listener
In the event listener, we send notification
public function handle(QuantityUpdated $event)
{
$subscriber = User::subscribe()->get();
Notification::send($subscriber, new UserUpdateQuantity());
}
Eloquent event
Add event on your eloquent model, so when someone update quantity, it triggers event and the listener will send notification to subscribed users
// In your model
protected $dispatchesEvents = [
'updated' => App\Events\QuantityUpdated::class
];

Related

Eloquent Model with nested model issue

I have got a couple of tables looking like this:
1) article
id
name
2) article_options
id
article_id
option_id
3) option
id
name
An article can have none, one or many options and I want to return in the RestAPI a JSON structure that looks like this:
[{
"id": 1,
"name": "Article1",
"options": [10]
},
{
"id": 2,
"name": "Article2",
"options": [3, 10]
},
{
"id": 3,
"name": "Article3",
"options": []
}]
whereby 3 and 10 would be the option ids.
I tried a few solutions but none worked. Here is what I currently have:
Article Model:
class Article extends Model
{
# protected $with = ['Article_Option'];
public function options()
{
return $this->hasMany('App\Article_Option');
}
}
Article Controller:
class ArticleController extends Controller
{
public function index()
{
$articles = Article::all();
return response()->json(
$articles
, 200);
}
}
I just can't get my head around how the model and the controller should be configured to achieve this.
Please help.
Thanks
Goppi
[UPDATE]
Thanks to Hides help. With some modification I got it to work the way I wanted. Here is all the code...
Article model:
namespace App;
use Illuminate\Database\Eloquent\Model;
class Article extends Model
{
public function options()
{
return $this->belongsToMany('App\Option');
}
}
ArticleController:
namespace App\Http\Controllers;
use App\Article;
use App\Http\Resources\Article as ArticleResource;
use Illuminate\Http\Request;
class ArticleController extends Controller
{
public function index()
{
$articles = ArticleResource::collection(Article::all());
return response()->json ($articles, 200);
}
}
and the Resource:
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class Article extends JsonResource
{
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'options' => $this->options->pluck('id')->all()
];
}
}
article_options is your pivot table and the relationship between article and options is a many to many relationship which laravel provides a function for here. So all you will have to do is add the relationship to your article model.
public function options() {
return $this->belongsToMany(Option::class);
}
then when getting the articles you can call that relationship.
$articles = Article::with('options')->get();
This will grab all articles with all option relations and format it in the way that you desired. Laravel can automatically work out which fields it needs to use to form the relationship between the tables so don't worry about that though it does have an option to supply them in the belongsToMany function.
If you are only wanting the id you can pass specific columns to the with function as below.
$articles = Articles::with('options:id')->get()
To then return in a json format you can use resources. Resources help better format the collection of the model which can be used to achieve what you want. You will need to create a resource for articles. In the article resource you can format it how you wanted and to format the options you can use collection methods to transform it into an array of ids by plucking the field from the collection.
Article Resource
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'options' => $this->options->pluck('id')->all(),
];
}
So once you have got the articles you can return them like so
return new ArticlesResource::collection($articles);
If later on you can wanting to pass additional columns through for options you can create an options resource and pass that to the options key in you article resource.
'options' => new OptionsResource::collection($this->options)

Inconsistent results with Meteor's pub/sub feature

I'm experiencing inconsistent results with Meteor's pub/sub feature, and I suspect it's a source of confusion for a lot of developers hitting the threshold of an MVP built in Meteor becoming a production app.
Maybe this is a limitation of MergeBox:
Let's say I have a collection called Events, in which I have document-oriented structures, ie, nested Array, Objects. An Events document might look like so:
// an Events document //
{
_id: 'abc',
name: 'Some Event',
participation: {
'userOneId': {
games: {
'gameOneId': {
score: 100,
bonus: 10
}
},
{
'gameTwoId': : {
score: 100,
bonus: 10
}
}
}
},
'userTwoId': {
games: {
'gameOneId': {
score: 70,
bonus: 15
}
},
contests: {
'contestOneId': [2, 3, 6, 7, 4],
'contestTwoId': [9, 3, 7, 2, 1],
}
}
},
}
}
So at these events, users can optionally participate in games of certain types and contests of certain types.
Now, I want to restrict subscriptions to the Events collection based on the user (show only this user's participation), and, sometimes I'm only interested in changes to one subset of the data (like, show only the user's scores on 'gameOneId').
So I've created a publication like so:
Meteor.publish("events.participant", function(eventId, userId) {
if(!Meteor.users.findOne({_id: this.userId})) return this.ready();
check(eventId, String);
check(userId, String);
const includeFields = {
name: 1,
[`participation.${userId}`]: 1
};
return Events.find({_id: eventId}, {fields: includeFields});
});
This publication seems to work fine on the client if I do:
// in onCreated of events template //
template.autorun(function() {
const
eventId = FlowRouter.getParam('id'),
userId = Meteor.userId(),
subscription = template.subscribe('events.participant', eventId, userId);
if (subscription.ready()) {
const event = Events.findOne({_id: eventId}, parseIncludeFields(['name', `participation.${userId}`]));
template.props.event.set(event);
}
});
Happily, I can use the Event document returned that includes only the name field and all of the user's participation data.
But, later, in another template if I do:
// in onCreated of games template //
template.autorun(function() {
const
eventId = FlowRouter.getParam('id')
gameId = FlowRouter.getParam('gameId'),
userId = Meteor.userId(),
subscription = template.subscribe('events.participant', eventId, userId);
if(subscription.ready()) {
const event = Events.findOne({_id: eventId}, {fields: {[`participation.${userId}.games.${gameId}`]: 1}});
template.props.event.set(event);
}
});
I sometimes get back the data at event.participation[userId].games[gameId], and sometimes I don't - the Object that's suppose to be at gameId is non-existent, even the it exists in the Mongo document, and the subscription should include it. Why?
The only difference is between the two calls to Events.findOne() is that in the latter, I'm not requesting the name field. But, if this is a problem, why?. If minimongo already has the document, who cares if I request parts of it?
The subscriptions in both templates are identical - I'm doing this because the games template is available at a route, so the user could go straight to the games url, by-passing the events template altogether, so I want to be sure the client has the document it needs to render correctly.
The only way I've gotten around this is to make a straight Meteor method call to the server in the games template to fetch the subset of interest, but this seems like a cop-out.
If you've read this far, you're a champ!

How to implement Role-based REST API

I have two different roles in my project: ROLE_USER and ROLE_ADMIN.
I want to get list of all users through REST API's url '/users', but some fields (for example email) can see only those person, who authenticated with ROLE_ADMIN.
So, I have generally 2 questions:
1) On which abstraction level (in MVC pattern) should I decide which information can be returned based on ROLE
2) Which is the best way to implement such a Role-based REST API in Symfony?
Thanks
If you are using JMSSerializer you can use groups to decide what can be seen or not. Then in your controller, or where ever, you could set the group based on the role.
For example with the mapping (in YAML)..
Fully\Qualified\Class\Name:
exclusion_policy: ALL
properties:
id:
groups: [user]
userAndAdmin:
groups: [user]
adminOnly:
groups: [admin]
And then in your controller you would set the group like...
public function getUsersAction(Request $request)
{
$users = $this->getRepository()->findAll();
$serializer = $this->get('jms_serializer.serializer');
$json = $serializer->serialize(
$users,
'json',
SerializationContext::create()->setGroups($this->generateGroups())
);
return new Response($json);
// If you are using FOSRestBundle, which I would recommend, then you would just need to do...
$view = $this
->view($this->getRepository()->findAll();)
->setExclusionGroups($this->generateGroups())
;
return $this->handleView($view);
}
private function generateGroups()
{
$securityContext = $this->get('security.context');
$groups = array();
if ($securityContext->isGranted('ROLE_USER')) {
$groups[] = 'user';
}
if ($securityContext->isGranted('ROLE_ADMIN')) {
$groups[] = 'admin';
}
return $groups;
}
Although the whole "generateGroups" and setting the groups would be better placed in a customer view handler or response generator.
Assuming your hierarchy has ROLE_ADMIN as a parent of ROLE_USER you would get the following results.
ROLE_USER
{
"users": [
{
"id": 1,
"userAndAdmin": "val"
}
]
}
ROLE_ADMIN
{
"users": [
{
"id": 1,
"userAndAdmin": "val",
"adminOnly": "val"
}
]
}
Since the API is dependent on user who is making the request, each request will have to carry the information about the current user. Usually all authorization related tasks are processed within the controller. So, answer to your first question is that you should process the roles in the controller and based on the roles, you should filter out the fields from the data returned from the repository. For example,
//users is the array of user objects returned by your repository
data = [];
if ($this->get('security.context')->isGranted('ROLE_ADMIN')) {
foreach($users as $user){
// Add ROLE_ADMIN specific data
data[][] = array(
'name' => $user->getName(),
'email' => $user->getEmail(),
);
}
}
if ($this->get('security.context')->isGranted('ROLE_USER')) {
foreach($users as $user){
// Add ROLE_USER specific data
data[][] = array(
'name' => $user->getName(),
'address' => $user->getAddress(),
);
}
}
then, JSON encode the data array and return as the response. To reduce the number of queries, you can add a __toArray method in your User class.

How do I create a Lithium - form->select from controller output

I want to create a dropdown for types collection from MongoDB, in my MongoDB collection:
section has types and controller code with view code. I want to create a form->select from the output of the controller.
types
{
"_id": ObjectId("5082c6109d5d0c640c000000"),
"name": "JACKET CLUSTER FRONT"
}
{
"_id": ObjectId("5082c62b9d5d0c440c00006e"),
"name": "JACKET CLUSTER FRONT"
}
{
"_id": ObjectId("5082c62b9d5d0c440c00006f"),
"name": "TITLE WITHOUT SYMBOL"
}
{
"_id": ObjectId("5082c62b9d5d0c440c000070"),
"name": "FRONTISPIECE"
}
*/
// in my controller
// -----------
$types = Types::all(array('order'=>'_id'));
$vtype = array($types)
return compact('vtypes');
// in my view
// ------------------
echo $this->form->select('types',$vtypes);
find('list') returns a key/value array, useful for any use where you would want a list such as for populating input select boxes.
$types = Types::find('list')
//returns
Array
(
[5082c6109d5d0c640c000000] => 'JACKET CLUSTER FRONT',
[5082c62b9d5d0c440c00006e] => 'JACKET CLUSTER FRONT',
[5082c62b9d5d0c440c00006f] => 'TITLE WITHOUT SYMBOL',
...
)
This finder looks for $_meta['title'] of your model, which is by default name if this field is available, and $_meta['key'] for the id, which should be _id in your case if your schema is correct
Finally, I used this to achieve the result.
Types::meta('key', '_id');
Types::meta('title', 'filename');
$types = Types::find('list',array(
'fields'=>array('id','filename'),
'order'=>'id'));

Auto increment in MongoDB to store sequence of Unique User ID

I am making a analytics system, the API call would provide a Unique User ID, but it's not in sequence and too sparse.
I need to give each Unique User ID an auto increment id to mark a analytics datapoint in a bitarray/bitset. So the first user encounters would corresponding to the first bit of the bitarray, second user would be the second bit in the bitarray, etc.
So is there a solid and fast way to generate incremental Unique User IDs in MongoDB?
As selected answer says you can use findAndModify to generate sequential IDs.
But I strongly disagree with opinion that you should not do that. It all depends on your business needs. Having 12-byte ID may be very resource consuming and cause significant scalability issues in future.
I have detailed answer here.
You can, but you should not
https://web.archive.org/web/20151009224806/http://docs.mongodb.org/manual/tutorial/create-an-auto-incrementing-field/
Each object in mongo already has an id, and they are sortable in insertion order. What is wrong with getting collection of user objects, iterating over it and use this as incremented ID? Er go for kind of map-reduce job entirely
I know this is an old question, but I shall post my answer for posterity...
It depends on the system that you are building and the particular business rules in place.
I am building a moderate to large scale CRM in MongoDb, C# (Backend API), and Angular (Frontend web app) and found ObjectId utterly terrible for use in Angular Routing for selecting particular entities. Same with API Controller routing.
The suggestion above worked perfectly for my project.
db.contacts.insert({
"id":db.contacts.find().Count()+1,
"name":"John Doe",
"emails":[
"john#doe.com",
"john.doe#business.com"
],
"phone":"555111322",
"status":"Active"
});
The reason it is perfect for my case, but not all cases is that as the above comment states, if you delete 3 records from the collection, you will get collisions.
My business rules state that due to our in house SLA's, we are not allowed to delete correspondence data or clients records for longer than the potential lifespan of the application I'm writing, and therefor, I simply mark records with an enum "Status" which is either "Active" or "Deleted". You can delete something from the UI, and it will say "Contact has been deleted" but all the application has done is change the status of the contact to "Deleted" and when the app calls the respository for a list of contacts, I filter out deleted records before pushing the data to the client app.
Therefore, db.collection.find().count() + 1 is a perfect solution for me...
It won't work for everyone, but if you will not be deleting data, it works fine.
Edit
latest versions of pymongo:
db.contacts.count() + 1
First Record should be add
"_id" = 1 in your db
$database = "demo";
$collections ="democollaction";
echo getnextid($database,$collections);
function getnextid($database,$collections){
$m = new MongoClient();
$db = $m->selectDB($database);
$cursor = $collection->find()->sort(array("_id" => -1))->limit(1);
$array = iterator_to_array($cursor);
foreach($array as $value){
return $value["_id"] + 1;
}
}
I had a similar issue, namely I was interested in generating unique numbers, which can be used as identifiers, but doesn't have to. I came up with the following solution. First to initialize the collection:
fun create(mongo: MongoTemplate) {
mongo.db.getCollection("sequence")
.insertOne(Document(mapOf("_id" to "globalCounter", "sequenceValue" to 0L)))
}
An then a service that return unique (and ascending) numbers:
#Service
class IdCounter(val mongoTemplate: MongoTemplate) {
companion object {
const val collection = "sequence"
}
private val idField = "_id"
private val idValue = "globalCounter"
private val sequence = "sequenceValue"
fun nextValue(): Long {
val filter = Document(mapOf(idField to idValue))
val update = Document("\$inc", Document(mapOf(sequence to 1)))
val updated: Document = mongoTemplate.db.getCollection(collection).findOneAndUpdate(filter, update)!!
return updated[sequence] as Long
}
}
I believe that id doesn't have the weaknesses related to concurrent environment that some of the other solutions may suffer from.
// await collection.insertOne({ autoIncrementId: 1 });
const { value: { autoIncrementId } } = await collection.findOneAndUpdate(
{ autoIncrementId: { $exists: true } },
{
$inc: { autoIncrementId: 1 },
},
);
return collection.insertOne({ id: autoIncrementId, ...data });
I used something like nested queries in MySQL to simulate auto increment, which worked for me. To get the latest id and increment one to it you can use:
lastContact = db.contacts.find().sort({$natural:-1}).limit(1)[0];
db.contacts.insert({
"id":lastContact ?lastContact ["id"] + 1 : 1,
"name":"John Doe",
"emails": ["john#doe.com", "john.doe#business.com"],
"phone":"555111322",
"status":"Active"
})
It solves the removal issue of Alex's answer. So no duplicate id will appear if any record is removed.
More explanation: I just get the id of the latest inserted document, add one to it, and then set it as the id of the new record. And ternary is for cases that we don't have any records yet or all of the records are removed.
this could be another approach
const mongoose = require("mongoose");
const contractSchema = mongoose.Schema(
{
account: {
type: mongoose.Schema.Types.ObjectId,
required: true,
},
idContract: {
type: Number,
default: 0,
},
},
{ timestamps: true }
);
contractSchema.pre("save", function (next) {
var docs = this;
mongoose
.model("contract", contractSchema)
.countDocuments({ account: docs.account }, function (error, counter) {
if (error) return next(error);
docs.idContract = counter + 1;
next();
});
});
module.exports = mongoose.model("contract", contractSchema);
// First check the table length
const data = await table.find()
if(data.length === 0){
const id = 1
// then post your query along with your id
}
else{
// find last item and then its id
const length = data.length
const lastItem = data[length-1]
const lastItemId = lastItem.id // or { id } = lastItem
const id = lastItemId + 1
// now apply new id to your new item
// even if you delete any item from middle also this work
}