Research
Security News
Malicious npm Package Targets Solana Developers and Hijacks Funds
A malicious npm package targets Solana developers, rerouting funds in 2% of transactions to a hardcoded address.
The typical Rails (and ActiveRecord) way of building applications is great for small to medium sized projects, but when projects grow larger and more complex, your models too become larger and more complex - it is not uncommon to have god classes such as a User model.
Datamappify tries to solve two common problems in web applications:
Datamappify is loosely based on the Repository Pattern and Entity Aggregation, and is built on top of Virtus and existing ORMs (ActiveRecord and Sequel, etc).
There are three main design goals:
UserAccount
ActiveRecord model that is attached to a User
entity managed by Datamappify.UserRepository.save(user)
, certain attributes of the user entity are now persisted on a remote web service. Better yet, dirty tracking and lazy loading are supported out of the box!Datamappify consists of three components:
find
, save
and destroy
, etc.Below is a high level and somewhat simplified overview of Datamappify's architecture.
Note: Datamappify is NOT affiliated with the Datamapper project.
You may implement your own data provider and criteria, but Datamappify comes with build-in support for the following ORMS:
Add this line to your application's Gemfile:
gem 'datamappify'
Entity uses Virtus DSL for defining attributes and ActiveModel::Validations DSL for validations.
The cool thing about Virtus is that all your attributes get coercion for free!
Below is an example of a User entity, with inline comments on how some of the DSLs work.
class User
include Datamappify::Entity
attribute :first_name, String
attribute :last_name, String
attribute :age, Integer
attribute :passport, String
attribute :driver_license, String
attribute :health_care, String
# Nested entity composition - composing the entity with attributes and validations from other entities
#
# class Job
# include Datamappify::Entity
#
# attributes :title, String
# validates :title, :presence => true
# end
#
# class User
# # ...
# attributes_from Job
# end
#
# essentially equals:
#
# class User
# # ...
# attributes :title, String
# validates :title, :presence => true
# end
attributes_from Job
# optionally you may prefix the attributes, so that:
#
# class Hobby
# include Datamappify::Entity
#
# attributes :name, String
# validates :name, :presence => true
# end
#
# class User
# # ...
# attributes_from Hobby, :prefix_with => :hobby
# end
#
# becomes:
#
# class User
# # ...
# attributes :hobby_name, String
# validates :hobby_name, :presence => true
# end
attributes_from Hobby, :prefix_with => :hobby
# Entity reference
#
# `references` is a convenient method for:
#
# attribute :account_id, Integer
# attr_accessor :account
#
# and it assigns `account_id` the correct value:
#
# user.account = account #=> user.account_id = account.id
references :account
validates :first_name, :presence => true,
:length => { :minimum => 2 }
validates :passport, :presence => true,
:length => { :minimum => 8 }
def full_name
"#{first_name} #{last_name}"
end
end
Inheritance is supported for entities, for example:
class AdminUser < User
attribute :level, Integer
end
class GuestUser < User
attribute :expiry, DateTime
end
Datamappify supports attribute lazy loading via the Lazy
module.
class User
include Datamappify::Entity
include Datamappify::Lazy
end
When an entity is lazy loaded, only attributes from the primary source (e.g. User
entity's primary source would be ActiveRecord::User
as specified in the corresponding repository) will be loaded. Other attributes will only be loaded once they are called. This is especially useful if some of your data sources are external web services.
Repository maps entity attributes to DB columns - better yet, you can even map attributes to different ORMs!
Below is an example of a repository for the User entity, you can have more than one repositories for the same entity.
class UserRepository
include Datamappify::Repository
# specify the entity class
for_entity User
# specify the default data provider for unmapped attributes
# optionally you may use `Datamappify.config` to config this globally
default_provider :ActiveRecord
# specify any attributes that need to be mapped
#
# for attributes mapped from a different source class, a foreign key on the source class is required
#
# for example:
# - 'last_name' is mapped to the 'User' ActiveRecord class and its 'surname' attribute
# - 'driver_license' is mapped to the 'UserDriverLicense' ActiveRecord class and its 'number' attribute
# - 'passport' is mapped to the 'UserPassport' Sequel class and its 'number' attribute
# - attributes not specified here are mapped automatically to 'User' with provider 'ActiveRecord'
map_attribute :last_name, :to => 'User#surname'
map_attribute :driver_license, :to => 'UserDriverLicense#number'
map_attribute :passport, :to => 'UserPassport#number', :provider => :Sequel
map_attribute :health_care, :to => 'UserHealthCare#number', :provider => :Sequel
# alternatively, you may group attribute mappings if they share certain options:
group :provider => :Sequel do
map_attribute :passport, :to => 'UserPassport#number'
map_attribute :health_care, :to => 'UserHealthCare#number'
end
# attributes can also be reverse mapped by specifying the `via` option
#
# for example, the below attribute will look for `hobby_id` on the user object,
# and map `hobby_name` from the `name` attribute of `ActiveRecord::Hobby`
#
# this is useful for mapping form fields (similar to ActiveRecord's nested attributes)
map_attribute :hobby_name, :to => 'Hobby#name', :via => :hobby_id
# by default, Datamappify maps attributes using an inferred reference (foreign) key,
# for example, the first mapping below will look for the `user_id` key in `Bio`,
# the second mapping below will look for the `person_id` key in `Bio` instead
map_attribute :bio, :to => 'Bio#body'
map_attribute :bio, :to => 'Bio#body', :reference_key => :person_id
end
Inheritance is supported for repositories when your data structure is based on STI (Single Table Inheritance), for example:
class AdminUserRepository < UserRepository
for_entity AdminUser
end
class GuestUserRepository < UserRepository
for_entity GuestUser
map_attribute :expiry, :to => 'User#expiry_date'
end
In the above example, both repositories deal with the ActiveRecord::User
data model.
Datamappify repository by default creates the underlying data model classes for you. For example:
map_attribute :driver_license, :to => 'UserData::DriverLicense#number'
In the above example, a Datamppify::Data::Record::ActiveRecord::UserDriverLicense
ActiveRecord model will be created. If you would like to customise the data model class, you may do so by creating one either under the default namespace or under the Datamappify::Data::Record::NameOfDataProvider
namespace:
module UserData
class DriverLicense < ActiveRecord::Base
# your customisation...
end
end
module Datamappify::Data::Record::ActiveRecord::UserData
class DriverLicense < ::ActiveRecord::Base
# your customisation...
end
end
More repository APIs are being added, below is a list of the currently implemented APIs.
Accepts an id.
user = UserRepository.find(1)
Accepts an entity.
UserRepository.exists?(user)
Returns an array of entities.
users = UserRepository.all
Returns an array of entities.
users = UserRepository.where(:first_name => 'Fred', :driver_license => 'AABBCCDD')
users = UserRepository.match(:first_name => 'Fre%', :driver_license => '%bbcc%')
You may compose search criteria via the criteria
method.
users = UserRepository.criteria(
:where => {
:first_name => 'Fred'
},
:order => {
:last_name => :asc
},
:limit => [10, 20]
)
Currently implemented criteria options:
Note: it does not currently support searching attributes from different data providers.
Accepts an entity.
There is also save!
that raises Datamappify::Data::EntityNotSaved
.
UserRepository.save(user)
Datamappify supports attribute dirty tracking - only dirty attributes will be saved.
Sometimes it's useful to manually mark the whole entity, or some attributes in the entity to be dirty. In this case, you could:
UserRepository.states.mark_as_dirty(user) # marks the whole entity as dirty
UserRepository.states.find(user).changed? #=> true
UserRepository.states.find(user).first_name_changed? #=> true
UserRepository.states.find(user).last_name_changed? #=> true
UserRepository.states.find(user).age_changed? #=> true
Or:
UserRepository.states.mark_as_dirty(user, :first_name, :last_name) # marks only first_name and last_name as dirty
UserRepository.states.find(user).changed? #=> true
UserRepository.states.find(user).first_name_changed? #=> true
UserRepository.states.find(user).last_name_changed? #=> true
UserRepository.states.find(user).age_changed? #=> false
Accepts an entity.
There is also destroy!
that raises Datamappify::Data::EntityNotDestroyed
.
Note that due to the attributes mapping, any data found in mapped records are not touched. For example the corresponding ActiveRecord::User
record will be destroyed, but ActiveRecord::Hobby
that is associated will not.
UserRepository.destroy(user)
Accepts an entity class and returns a new entity.
This is useful for using before_init
and after_init
callbacks to set up the entity.
UserRepository.init(user_class) #=> user
Datamappify supports the following callbacks via Hooks:
Callbacks are defined in repositories, and they have access to the entity. For example:
class UserRepository
include Datamappify::Repository
before_create :make_me_admin
before_create :make_me_awesome
after_save :make_me_smile
private
def make_me_admin(entity)
# ...
end
def make_me_awesome(entity)
# ...
end
def make_me_smile(entity)
# ...
end
# ...
end
Note: Returning either nil
or false
from the callback will cancel all subsequent callbacks (and the action itself, if it's a before_
callback).
Datamappify also supports entity association. It is experimental and it currently supports the following association types:
Set up your entities and repositories:
# entities
class User
include Datamappify::Entity
has_one :title, :via => Title
has_many :posts, :via => Post
end
class Title
include Datamappify::Entity
belongs_to :user
end
class Post
include Datamappify::Entity
belongs_to :user
end
# repositories
class UserRepository
include Datamappify::Repository
for_entity User
references :title, :via => TitleRepository
references :posts, :via => PostRepository
end
class TitleRepository
include Datamappify::Repository
for_entity Title
end
class PostRepository
include Datamappify::Repository
for_entity Post
end
Usage examples:
new_post = Post.new(post_attributes)
another_new_post = Post.new(post_attributes)
user = UserRepository.find(1)
user.title = Title.new(title_attributes)
user.posts = [new_post, another_new_post]
persisted_user = UserRepository.save!(user)
persisted_user.title #=> associated title
persisted_user.posts #=> an array of associated posts
Like ActiveRecord and ActionView, Datamappify also supports nested attributes via fields_for
or simple_fields_for
.
# slim template
= simple_form_for @post do |f|
= f.input :title
= f.input :body
= f.simple_fields_for :comment do |fp|
= fp.input :author_name
= fp.input :comment_body
You may configure Datamappify's default behaviour. In Rails you would put it in an initializer file.
Datamappify.config do |c|
c.default_provider = :ActiveRecord
end
Datamappify ships with a few extensions to make certain tasks easier.
Use Criteria
with page
and per
.
UserRepository.criteria(
:where => {
:gender => 'male',
:age => 42
},
:page => 1,
:per => 10
)
You may check out this article for more examples.
Refer to CHANGELOG.
Licensed under MIT
FAQs
Unknown package
We found that datamappify demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Research
Security News
A malicious npm package targets Solana developers, rerouting funds in 2% of transactions to a hardcoded address.
Security News
Research
Socket researchers have discovered malicious npm packages targeting crypto developers, stealing credentials and wallet data using spyware delivered through typosquats of popular cryptographic libraries.
Security News
Socket's package search now displays weekly downloads for npm packages, helping developers quickly assess popularity and make more informed decisions.