Skip to content

bitex-la/jsonapi-mapper

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

42 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

JsonapiMapper

Sanitizes a jsonapi Document and maps it to ActiveRecord, creating or updating as needed.

  • Prevents assiginging unexpected attributes on your records.
  • Prevents unscoped queries when creating/updating records.

Installation

Add this line to your application's Gemfile:

gem 'jsonapi_mapper'

And then execute:

$ bundle

Or install it yourself as:

$ gem install jsonapi_mapper

Usage

See the specs directory for more examples.

      class Person < ActiveRecord::Base
        belongs_to :parent, class_name: 'Person'
        has_many :children, class_name: 'Person', foreign_key: 'parent_id'
        belongs_to :pet, class_name: 'PetDog'
      end

      class PetDog < ActiveRecord::Base
        has_one :person, foreign_key: 'pet_id'
      end

      # This document should create a person and several associations.
      # Notice how these not-persisted resources can be referenced using
      # an internal id, which starts with @
      # The local @ ids shall be replaced with proper server assigned ids
      # once the resources are persisted.
      document = {
        data: {
          type: 'people',
          attributes: { name: 'ian', admin: true },
          relationships: {
            pet: { data: { type: 'pet_dogs', id: '@1' }},
            parent: { data: { type: 'people', id: '@1' }},
            children: { data: [
              { type: 'people', id: '@2' },
              { type: 'people', id: '@3' },
            ]},
          }
        },
        included: [
          { type: 'people', id: '@1', attributes: { name: 'ana', admin: true } },
          { type: 'people', id: '@2', attributes: { name: 'bob', admin: true } },
          { type: 'people', id: '@3', attributes: { name: 'zoe', admin: true } },
          { type: 'pet_dogs', id: '@1', attributes: { name: 'ace', age: 11 } }
        ]
      }

      # The mapper whitelists which types should be expected from the
      # jsonapi document. It also whitelists attributes and relationship names.
      # The last item of the attributes list is a Hash to be used as 'scope'
      # when attempting to fetch and/or modify any resource.
      mapper = JsonapiMapper.doc(document,
        people: [:name, :pet, :parent, :children, country: 'argentina'],
        pet_dogs: [:name, country: 'argentina']
      )

      # The document data lives in mapper.data
      # It could be a simple or multiple resource response.
      # If you want to check wether the document had a single resource
      # or a collection as its primary data you can use the following methods.
      mapper.collection? # Was primary document data a collection?
      mapper.single? # Was primary document data a single resource?


      # The rest of the included resources live in mapper.included
      others = mapper.included 

      # Attempts to save both data and included. Returns false if there
      # were any validation errors.
      mapper.save_all 
      
      # Four people have been created
      Person.count.should == 4

      # All of them from 'argentina' according to the provided scope.
      Person.where(country: 'argentina').count.should == Person.count

      # The 'admin' field was not set, because it wasn't in the mapper list.
      Person.where(admin: true).count.should == 0
      
      # This other document tries to update a bob's name and parent.
      # And it also creates a new dow and assigns it as pet for 'bob' and 'ana'
      other_document = {
        data: {
          type: 'people',
          id: '1',
          attributes: { name: 'rob' },
          relationships: {
            pet: { data: { type: 'pet_dogs', id: '@1' }},
            parent: { data: { type: 'people', id: '2' }},
          }
        },
        included: [
          { 
            type: 'people',
            id: ana.id,
            relationships: {
              pet: { data: { type: 'pet_dogs', id: '@1' }},
            }
          },
          { type: 'pet_dogs', id: '@1', attributes: { name: 'ace' } }
        ]
      }

      mapper = JsonapiMapper.doc other_document,
        people: [:name, :pet, :parent, country: 'uruguay'],
        pet_dogs: [:name, country: 'uruguay']

      mapper.save_all

      # Is dangerous to use unscoped queries
      # For those rare occassions where you don't need them they can be disabled.
      # The JsonapiMapper.doc_unsafe! method receives an argument with the names
      # of all the types for which a scope is not required.
      JsonapiMapper.doc_unsafe! document,
        [:pet_dogs],
        people: [:name, :pet, :parent, country: 'uruguay'],
        pet_dogs: [:name]

      # If you're needing to 'translate' between your jsonapi document names
      # and your ActiveRecord class and column names, you can do it like so:
      # Notice how the second hash has translations for type and attribute names.
      mapper = JsonapiMapper.doc(document, {
        persons: [:handle, :dog, :parental_figure, country: 'uruguay'],
        pets: [:nickname, country: 'uruguay']
      },
      { types: { persons: Person, pets: PetDog },
        attributes: {
          persons: {handle: :name, dog: :pet, parental_figure: :parent},
          pets: {nickname: :name},
        }  
      }).save_all


      # If any resource in your document has errors, you can get a collection
      # with pointers to the specific fields and the type and id of the resource
      # that has the error.
      document = {
        data: [
          { type: 'pets', attributes: { age: 3 } },
          { type: 'pets', attributes: { age: 6 } },
        ],
        included: [
          { type: 'pets', id: '@1', attributes: { age: 4 } }
        ]
      }
      mapper = JsonapiMapper.doc(document,
        { pets: [:nickname, country: 'uruguay'] },
        { types: { pets: PetDog }, attributes: { pets: {nickname: :name} } }
      )

      # all_valid? triggers all validations and sets up errors.
      mapper.all_valid?.should be_falsey

      # Then all errors are presented like so, honoring remapped names too.
      mapper.all_errors.should == {
        errors: [
          { status: 422,
            title: "can't be blank",
            detail: "can't be blank",
            code: "can_t_be_blank",
            meta: {type: "pets"},
            source: {pointer: "/data/0/attributes/nickname"}
          },
          { status: 422,
            title: "can't be blank",
            detail: "can't be blank",
            code: "can_t_be_blank",
            meta: {type: "pets"},
            source: {pointer: "/data/1/attributes/nickname"}
          },
          { status: 422,
            title: "can't be blank",
            detail: "can't be blank",
            code: "can_t_be_blank",
            meta: {type: "pets"},
            source: {pointer: "/included/0/attributes/nickname"}
          }
        ]
      }

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and tags, and push the .gem file to rubygems.org.

Contributing

Bug reports and pull requests are welcome here.

Code Status

Build Status

License

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

About

Sanitize and Read a JSONAPI document with included resources into your ActiveRecord objects.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published