Skip to content

Commit

Permalink
Add HasNotifications module and default to json for sqlite
Browse files Browse the repository at this point in the history
  • Loading branch information
excid3 committed Mar 13, 2021
1 parent bc1f4c6 commit 31d4fb7
Show file tree
Hide file tree
Showing 9 changed files with 99 additions and 44 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
### Unreleased

* Add Ruby 3.0 to CI
* Add `has_noticed_notifications` helper for models - @excid3
* Use `json` column for params on SQLite by default instead of text - @excid3
* Add Ruby 3.0 to CI - @excid3

### 1.2.21

Expand Down
58 changes: 18 additions & 40 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,7 @@ end

In this scenario, you can create an escalating notification that starts with a ping in Slack, then emails the team, and then finally sends an SMS to the on-call phone.

You can mix and match the options and delivery methods to suit your application specific needs.
You can mix and match the options and delivery methods to suit your application specific needs.

### 🚚 Custom Delivery Methods

Expand Down Expand Up @@ -534,59 +534,37 @@ Adding notification associations to your models makes querying and deleting noti

For example, in most cases, you'll want to delete notifications for records that are destroyed.

##### JSON Columns
We'll need two associations for this:

If you're using MySQL or Postgresql, the `params` column on the notifications table is in `json` or `jsonb` format and can be queried against directly.
1. Notifications where the record is the recipient
2. Notifications where the record is in the notification params

For example, we can query the notifications and delete them on destroy like so:

```ruby
class Post < ApplicationRecord
def notifications
# Exact match
@notifications ||= Notification.where(params: { post: self })
# Standard association for deleting notifications when you're the recipient
has_many :notifications, as: :recipient, dependent: :destroy

# Or Postgres syntax to query the post key in the JSON column
# @notifications ||= Notification.where("params->'post' = ?", Noticed::Coder.dump(self).to_json)
end

before_destroy :destroy_notifications
# Helper for associating and destroying Notification records where(params: {post: self})
has_noticed_notifications

def destroy_notifications
notifications.destroy_all
end
# You can override the param_name, the notification model name, or disable the before_destroy callback
has_noticed_notifications param_name: :parent, destroy: false, model: "Notification"
end
```

##### Polymorphic Association

If your notification is only associated with one model or you're using a `text` column for your params column , then a polymorphic association is what you'll want to use.
# Create a CommentNotification with a post param
CommentNotification.with(post: @post).deliver(user)
# Lookup Notifications where params: {post: @post}
@post.notifications_as_post

1. Generate a polymorphic association for the Notification model. `rails g migration AddNotifiableToNotifications notifiable:belongs_to{polymorphic}`

a. Make sure to add the association to the model: `belongs_to :notifiable, polymorphic: true`

2. Add `has_many :notifications, as: :notifiable, dependent: :destroy` to each model

3. Customize database `format: ` option to write the `notifiable` attribute(s) when saving the notification

```ruby
class ExampleNotification < Noticed::Base
deliver_by :database, format: :format_for_database

def format_for_database
{
notifiable: params[:post],
type: self.class.name,
params: params.except(:post)
}
end
end
```
CommentNotification.with(parent: @post).deliver(user)
@post.notifications_as_parent
```

#### Handling Deleted Records

If you create a notification but delete the associated record, the jobs for sending the notification will not be able to find the record when ActiveJob deserializes. You can discord the job on these errors by adding the following to `ApplicationJob`:
If you create a notification but delete the associated record and forgot `has_noticed_notifications` on the model, the jobs for sending the notification will not be able to find the record when ActiveJob deserializes. You can discord the job on these errors by adding the following to `ApplicationJob`:

```ruby
class ApplicationJob < ActiveJob::Base
Expand Down
5 changes: 2 additions & 3 deletions lib/generators/noticed/model_generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,11 @@ def model_path

def params_column
case ActiveRecord::Base.configurations.configs_for(spec_name: "primary").config["adapter"]
when "mysql2"
"params:json"
when "postgresql"
"params:jsonb"
else
"params:text"
# MySQL and SQLite both support json
"params:json"
end
end
end
Expand Down
1 change: 1 addition & 0 deletions lib/noticed.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
module Noticed
autoload :Base, "noticed/base"
autoload :Coder, "noticed/coder"
autoload :HasNotifications, "noticed/has_notifications"
autoload :Model, "noticed/model"
autoload :TextCoder, "noticed/text_coder"
autoload :Translation, "noticed/translation"
Expand Down
5 changes: 5 additions & 0 deletions lib/noticed/engine.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
module Noticed
class Engine < ::Rails::Engine
initializer "noticed.has_notifications" do
ActiveSupport.on_load(:active_record) do
include Noticed::HasNotifications
end
end
end
end
32 changes: 32 additions & 0 deletions lib/noticed/has_notifications.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
module Noticed
module HasNotifications
# Defines a method for the association and a before_destory callback to remove notifications
# where this record is a param
#
# class User < ApplicationRecord
# has_noticed_notifications
# has_noticed_notifications param_name: :owner, destroy: false, model: "Notification"
# end
#
# @user.notifications_as_user
# @user.notifications_as_owner

extend ActiveSupport::Concern

class_methods do
def has_noticed_notifications(param_name: model_name.singular, **options)
model = options.fetch(:model_name, "Notification").constantize

define_method "notifications_as_#{param_name}" do
model.where(params: {param_name.to_sym => self})
end

if options.fetch(:destroy, true)
before_destroy do
send("notifications_as_#{param_name}").destroy_all
end
end
end
end
end
end
3 changes: 3 additions & 0 deletions test/dummy/app/models/user.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
class User < ApplicationRecord
has_many :notifications, as: :recipient

has_noticed_notifications
has_noticed_notifications param_name: :owner, destroy: false

def phone_number
"8675309"
end
Expand Down
Binary file added test/dummy/noticed_test
Binary file not shown.
35 changes: 35 additions & 0 deletions test/noticed/has_notifications_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
require "test_helper"

class HasNotificationsTest < ActiveSupport::TestCase
class DatabaseDelivery < Noticed::Base
deliver_by :database
end

test "has_noticed_notifications" do
assert User.respond_to?(:has_noticed_notifications)
end

test "noticed notifications association" do
assert user.respond_to?(:notifications_as_user)
end

test "noticed notifications with custom name" do
assert user.respond_to?(:notifications_as_owner)
end

test "deletes notifications with matching param" do
DatabaseDelivery.with(user: user).deliver(users(:two))

assert_difference "Notification.count", -1 do
user.destroy
end
end

test "doesn't delete notifications when disabled" do
DatabaseDelivery.with(owner: user).deliver(users(:two))

assert_no_difference "Notification.count" do
user.destroy
end
end
end

0 comments on commit 31d4fb7

Please sign in to comment.