Giter Site home page Giter Site logo

clowne's Introduction

Gem Version Build Status Docs

Clowne

A flexible gem for cloning your models. Clowne focuses on ease of use and provides the ability to connect various ORM adapters.

๐Ÿ“– Read Evil Martians Chronicles to learn about possible use cases.

๐Ÿ“‘ Documentation

Sponsored by Evil Martians

Installation

To install Clowne with RubyGems:

gem install clowne

Or add this line to your application's Gemfile:

gem "clowne"

Quick Start

Assume that you have the following model:

class User < ActiveRecord::Base
  # create_table :users do |t|
  #  t.string :login
  #  t.string :email
  #  t.timestamps null: false
  # end

  has_one :profile
  has_many :posts
end

class Profile < ActiveRecord::Base
  # create_table :profiles do |t|
  #   t.string :name
  # end
end

class Post < ActiveRecord::Base
  # create_table :posts
end

Let's declare our cloners first:

class UserCloner < Clowne::Cloner
  adapter :active_record

  include_association :profile, clone_with: SpecialProfileCloner
  include_association :posts

  nullify :login

  # params here is an arbitrary Hash passed into cloner
  finalize do |_source, record, **params|
    record.email = params[:email]
  end
end

class SpecialProfileCloner < Clowne::Cloner
  adapter :active_record

  nullify :name
end

Now you can use UserCloner to clone existing records:

user = User.last
# => <#User id: 1, login: 'clown', email: '[email protected]'>

operation = UserCloner.call(user, email: "[email protected]")
# => <#Clowne::Utils::Operation...>

operation.to_record
# => <#User id: nil, login: nil, email: '[email protected]'>

operation.persist!
# => true

cloned = operation.to_record
# => <#User id: 2, login: nil, email: '[email protected]'>

cloned.login
# => nil
cloned.email
# => "[email protected]"

# associations:
cloned.posts.count == user.posts.count
# => true
cloned.profile.name
# => nil

Take a look at our documentation for more info!

Supported ORM adapters

Adapter 1:1 *:1 1:M M:M
Active Record has_one belongs_to has_many has_and_belongs_to
Sequel one_to_one - one_to_many many_to_many

Maintainers

License

The gem is available as open source under the terms of the MIT License.

clowne's People

Contributors

louim avatar madding avatar palkan avatar pomartel avatar sgerrand avatar sponomarev avatar ssnickolay avatar sub-xaero avatar tbetous avatar tuanmai avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

clowne's Issues

Nested params

Currently, we had an effect of params shadowing: nested cloner could rely on the same params as the top-level ones, but they must not know about each other (more precisely, children cloners must not know about parent cloners).

We need a way to control how params are passed to associations cloners, e.g.:

# just pass params as is (current behaviour)
include_association :users, params: true

# use custom block to prepare params
include_association :users, params: ->(params) { params[:data] }

# or use a key
include_association :profile, params: :profile

# usage
UserCloner.call(user, profile: { reset: true })

I also propose to not proxy params by default to the underlying cloners.

Which association is created first if you have multiple associations?

class Member
  has_many :jobs
  has_many :addresses
end
class Job
  ....

  validate :validate_member_address

  def validate_member_address
    VALID_STREET_ADDRESSES.include?(member.current_address.street_name)
  end
end

When cloning a member, the cloner will clone jobs and addresses but the cloned job objects will fail to validate because at the time you clone member.jobs, member.current_address (class Address) has not been cloned yet.

So, is there any way to making sure we will clone member.address first before member.jobs?

class MemberCloner < Clowne::Cloner
  include_association :jobs
  include_association :addresses
end

polymorphic association

Hi guys!

First of all congrats for your awesome gem. I'm opening an issue because I didn't find anywhere documentation for polymorphic tables, and I think the current behaviour is not as intended.

Example to reproduce. The table tag mapping has a polymorphic field owner (owner_id and owner_type)

# app/models/a.rb
class A < ApplicationRecord
  has_many :tag_mappings
  has_many :tags, through: tag_mappings
end

# app/models/b.rb
class B < ApplicationRecord
  has_many :tag_mappings
  has_many :tags, through: tag_mappings
end

# app/models/tag_mapping.rb
class TagMapping < ApplicationRecord
  belongs_to :tag
  belongs_to :owner, polymorphic: true
end

# app/models/tag.rb
class Tag < ApplicationRecord
  has_many :tag_mappings, dependent: :destroy
  has_many :as, through: :tag_mappings, source: :owner
  has_many :bs, through: :tag_mappings, source: :owner
end

Cloner

class ACloner < Common::BaseCloner
  include_association :tag_mappings
end

rails console

a = FactoryBot.create(:a, :with_tags)
a.tag_mappings # => <TagMapping owner_id: 1, owner_type: A, tag_id: 1>
a.tags # => <Tag id: 1>
cloned_a = ACloner.call(a).to_record
cloned_a.tags # => []
cloned_a.tag_mappings #ย => <TagMapping owner_id: nil, owner_type: A, tag_id: 1>

My workaround is:

class ACloner < Common::BaseCloner
  finalize do |original_a, cloned_a, _params| do
    cloned_a.tags = original_a.tags
  end
end

Thank you :-)

Ruby 3.0 compatibility

It looks like Clowne is not compatible with Ruby 3.0. I get a lot of errors like these when running the tests suite:

ArgumentError: wrong number of arguments (given 2, expected 1)

This appears to be related to this breaking change in Ruby 3.0.

[Question] belongs_to IDs with nested models

Hi! I am planning a new feature for my app that will need cloning a structure with nested models. There are plan has_many/belongs_to and has_many :trhough associations.

My question: does clowne automatically set the correct/new parent IDs for child models?

Say I have an instance of A1, that has_many B1s. These B1s of course have A1's ID as the id for the belongs_to association. If I clone A1 to A2, A2 will have many B2s. Will these B2s have A2's ID as the parent ID, or A1's ID? And what if I have multiple nesting levels? And what about parent IDs in has_many :through associations?

I hope the question is clear. I am still thinking about how to implement this feature so I am trying to put the pieces together.

Thanks in advance!

Multiple Databases

First off, very nice looking gem, I actually just started work on something very similar, except my use case involves cloning records from a separate, but (mostly) identical database. I'm currently using a separate ApplicationRecord base class, for handling the alternate models to clone from, that looks like this.

module GuestHouse
  class ApplicationRecord < ::ActiveRecord::Base
    self.abstract_class = true
    establish_connection "guest_house_#{Rails.env}".to_sym

  end
end

So, if I have:

class Widget < ApplicationRecord
end

class GuestHouse::Widget < GuestHouse::ApplicationRecord
end

Then Widgets can be cloned from GuestHouse::Widgets

Could this gem handle that use case out of the box, or would it need some additional development, and if so, could you point me to a starting place?

Association couldn't be found, Rails 4.1.8

I have a very simple two model scenario with a has_many association in the model that I want to clone. So I declared

class PostsCloner < Clowne::Cloner
  adapter :active_record
  include_association :comments
end

The clone attempt results in an exception: Clowne::Adapters::ActiveRecord::Resolvers::UnknownAssociation: Association comments couldn't be found for User

It appears that this code is attempting to resolve the relation using a string index into ...reflections, but the reflections hash uses symbols.

def call(source, record, declaration, adapter:, params:, **_options)
reflection = source.class.reflections[declaration.name.to_s]
if reflection.nil?
raise UnknownAssociation,
"Association #{declaration.name} couldn't be found for #{source.class}"
end

Maybe this is a compatibility issue with Rails 4.1.8, Ruby 2.3

How to access the target cloned parent?

When doing a deep cloning of an ActiveRecord object, I would like a child to access it's cloned parent in the finalize block. Instead when I access the parent association, it is linked to the source parent.

Is this there a way for the child of deep cloned object to retrieve it's parent association which is also a cloned object?

Is it possible to ignore an attribute?

I know about nullify, but I would like to ignore an attribute all together. This is needed when cloning an object that contains a full text search field (TS_VECTOR) like we have when using Postgresql full text search.

Weird bug upgrading from 0.2.0 to 1.1.0

Hello,

Thank you for your had work! This gem is awesome.

I'm having a hard time debugging an issue after upgrading from 0.2.0 to 1.1.0. I am using the ActiveRecord adaptor with my Rails application and Ruby version 2.6.2.

After updating to the newest version, I am able to successfully clone new objects in my database without any issues. However, records that existed in 0.2.0 land throw an ActiveRecord::RecordInvalid error when trying to clone them.

I've noticed that the older records do not create Operation records after being cloned which I think may be the reason why they get an error. Newly created records do not have this problem and work as described in the documentation. Just to be clear, the same cloner is being used in both instances. One record was created before we upgraded to version 1.1.0 and the other after.

Any ideas about what could be causing this issue? It seems weird to me that the records would be aware of what version of Clowne was being used when they were created. Let me know if you need any more information.

Active Storage & Action Text

How can I handle has_one_attachment like for active storage. I'm also using Rails 6 with has_rich_text and it is not being cloned.

Nested include_association duplication

Hello.
In my app I have a models scheme similar to the next:

class Table
  has_many :rows
  has_many :columns
end

class Row
  belongs_to :table
  has_many :cells
end

class Column
  belongs_to :table
  has_many :cells
end

class Cell
  belongs_to :row
  belongs_to :column
end

and cloners:

class TableCloner < Clowne::Cloner
  adapter :active_record

  include_association :rows
  include_association :columns

  finalize do |source, record, **params|
    record.title = "Copy of #{source.title}"
  end
end

class RowCloner < Clowne::Cloner
  adapter :active_record

  include_association :cells
end

class ColumnCloner < Clowne::Cloner
  adapter :active_record

  include_association :cells
end

class CellCloner < Clowne::Cloner
  adapter :active_record
end

Unfortunately, when I try to clone the Table

operation = TableCloner.call(original_table)
operation.persist!

I get correct table, rows and columns, but cells are cloned twice and it seems that both are wrong:
one cells set has wrong row_id from the original_table rows and correct column_id from the cloned_table columns
and the second cells set has correct row_id from the cloned_table rows but wrong column_id from the original_table columns

It seems that it doesn't understand how to sync all these record cloners (two dimensional table) and does clone rows and columns associations independent from each other.
Could you help me to understand how to clone such a scheme properly?

Thank you

Question

Hello guys,

I see that you copied a lot of our logic from https://github.com/amoeba-rb/amoeba but not mentioned it.
I see one of mainteiners was a contributor in amoeba.

Can you add a mention on original library?

If/Unless modifiers

class StageCloner < Clowne::Cloner
  # Symbol support (method is called on the source)
  include_association :comments, if: :published?

  # Block support
  include_association :votes, if: ->(source, record, params) { params[:need_votes] }
end

Is there possible to use traits in trait?

Hello there! ๐Ÿ‘‹

Is there a way to use traits in trait like in factory_bot?

Here is an example :

class ListCloner < Clowne::Cloner
  trait :with_items do
    include_association :items
  end
  
  trait :special_list do
    with_items
    finalize do |_source, record, **_params|
      # Do some stuff...
    end
  end
end

Thanks for your work! โค๏ธ

Is it possible to clone a record to a different table?

Hey there,

I've got a requirement that requires me to keep a point-in-time snapshot of some data whose associations and attributes change over time.

In order to go back to old records and understand the makeup of it at that point in time, we need to make sure that once that record is at a certain stage, it has a snapshot taken of it, and all its associations, and values of all their attributes.

I really don't want to have to do this manually, and would love to use something like Clowne. In order for me to do that though, I'll need to clone my records to their equivalent *Snapshot models instead of the standard models. Here's a non-real-world example in case I'm not making too much sense:

  • A Course has many Users
  • A Course has many CourseModules
  • A User has a Certificate per Course
  • A Certificate has many CourseModule scores

As we either release new modules for a course, or remove old modules, or update existing modules, I don't want a certificate's scores to be affected.

My goal is that once each module is completed, all the data is snapshot from its CourseModule to a CourseModuleSnapshot record. I'd like to probably do a little bit of postprocessing too, but my main goal is to clone the model to a different but similar model.

Is this at all possible?

BTW - thank you evil martians for all the amazing open source you put out โค๏ธ

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.