ALL HOW TOs




References

Creating a Valkyrie based app

Create Rails app

new rails app
rails new valkyrie_pg_demo --database=postgresql
cd valkyrie_pg_demo


add valkyrie
vi Gemfile 
   # add gem 'valkyrie' and save

bundle install --path=vendor/bundle


may want to add these to .gitignore if committing to github
# Ignore vendor/cache
/vendor/cache
/vendor/bundle


Setup Postgres

Prerequisite:  Installation of Postgres  (for mac: Install Postgres with Homebrew; for windows: ???)

setup postgres database
psql postgres
  create role _USER_NAME_ with login encrypted password '_PASSWORD_' createdb;
  create database valkyrie_pg_demo_development with owner _USER_NAME_ encoding 'utf8';
  alter user _USER_NAME_ with SUPERUSER;
  \q

vi config/database.yml 
   # uncomment and set the following under development...
       database: valkyrie_pg_demo_development
       username: _USER_NAME_ as created in pg
       password: _PASSWORD_ as set in pg
       host: localhost
       port: 5432
 
bin/rails valkyrie_engine:install:migrations
bin/rails db:migrate


Configure app to use Valkyrie with Postgres

Terminology

  • metadata adapter
    • provides access to persister and query service for a datasource (e.g. postgres, solr, etc.)  See definitions for persister and query service under Test Direct Persistence


set up to use postgres adapter in config/initializers/valkyrie.rb
Valkyrie::MetadataAdapter.register(
  Valkyrie::Persistence::Postgres::MetadataAdapter.new,
  :postgres
} 


set up quick access to persister and query_service in config/initializers/val_pg.rb.rb
module ValkyriePgDemo
  def self.pg_persister
    Valkyrie::MetadataAdapter.find(:postgres).persister
  end

  def self.pg_query_service
    Valkyrie::MetadataAdapter.find(:postgres).query_service
  end
end
 
# prefixed with `pg_` to allow for easily adding methods for another adapter with different prefix (e.g. `solr_`)


Define Resources

Reference: Valkyrie Wiki (example resource | supported types | modifiers of type)

example book resource in app/resources/book.rb
# frozen_string_literal: true
class Book < Valkyrie::Resource
  attribute :title, Valkyrie::Types::Set               # Set - de-duplicate values
              .of(Valkyrie::Types::String)             #   .of - restricts values to type String
              .meta(ordered: true)                     #   .meta(ordered) - maintains order
  attribute :author, Valkyrie::Types::Set
              .of(Valkyrie::Types::String)
              .meta(ordered: true)
  attribute :series, Valkyrie::Types::String           # single value with type String
              .optional                                #   .optional - nil if not set
  attribute :member_ids, Valkyrie::Types::Array        # Array - allows duplicate values 
              .of(Valkyrie::Types::ID).optional        #   .of - restricts values to type
end

NOTE: If member_ids was restricted to type Page, then the entire Page resource is saved with the Book resource.


example page resource in app/resources/book.rb
# frozen_string_literal: true
class Page < Valkyrie::Resource
  attribute :page_num, Valkyrie::Types::String.optional           # single value that must be a String
  attribute :structure, Valkyrie::Types::String.optional
  attribute :image_id, Valkyrie::Types::ID.optional               # ID of stored file for image
end


Test Direct Persistence

Terminology

  • persister
    • provides methods for saving resources
    • resources are passed to the persister
    • see shared specs for list of methods and how they are expected to behave
  • query service
    • provides methods for finding resources 
    • see shared specs for list of methods and how they are expected to behave


In rails console...

save and retrieve a book resource
book = Book.new(title: 'Free Fall', author: 'Robert Crais', series: 'Elvis Cole/Joe Pike')
saved_book = ValkyriePgDemo.pg_persister.save(resource: book)

book.id # nil
book.persisted? # false # book object was NOT updated by the save method
saved_book.id # #<Valkyrie::ID:0x00007faf9bb499a0 @id="ad494304-ca12-42d6-a28b-34e60d63fb92">
saved_book.persisted? true

retrieved_book = ValkyriePgDemo.pg_query_service.find_by(id: saved_book.id)
retrieved_book.persisted? #true
retrieved_book.title # ['Free Fall']
retrieved_book.title = "Lullaby Town"
retrieved_book.title # ["Lullaby Town"]
retrieved_book.persisted? # true - persisted? indicated only that the resource exists in the database


saving book creates a single row in table orm_resources in postgres
                  id                  |                                                   metadata                                                    |         created_at         |         updated_at         | internal_resource | lock_version 
--------------------------------------+---------------------------------------------------------------------------------------------------------------+----------------------------+----------------------------+-------------------+--------------
 c9f0e8e8-6814-4419-8640-066792e4b1ae | {"title": ["Free Fall"], "author": ["Robert Crais"], "series": ["Elvis Cole/Joe Pike"], "new_record": [true]} | 2019-12-02 18:58:39.833058 | 2019-12-02 18:58:39.833058 | Book              |


In rails console...

save a page resource, add it to the book resource, and retrieve the page from the book resource
page = Page.new(page_num: '1', structure: 'title page')
page = ValkyriePgDemo.pg_persister.save(resource: page)
book = ValkyriePgDemo.pg_query_service.find_all_of_model(model: Book).first
book.member_ids = [page.id]
book = ValkyriePgDemo.pg_persister.save(resource: book)

child_pages = ValkyriePgDemo.pg_query_service.find_members(resource: book)
child_pages.first.structure # ['title page']


saving page creates a single row in same orm_resources table; saving book updates the book row
                  id                  |                                                   metadata                                                    |         created_at         |         updated_at         | internal_resource | lock_version 
--------------------------------------+---------------------------------------------------------------------------------------------------------------+----------------------------+----------------------------+-------------------+--------------
 6c899645-80ef-402a-81e3-8cbe48712f06 | {"page_num": ["1"], "structure": ["title page"], "new_record": [true]}                                                                                                               | 2019-12-02 22:13:23.521958 | 2019-12-02 22:13:23.521958 | Page              |
 c9f0e8e8-6814-4419-8640-066792e4b1ae | {"title": ["Free Fall"], "author": ["Robert Crais"], "series": ["Elvis Cole/Joe Pike"], "member_ids": [{"id": "6c899645-80ef-402a-81e3-8cbe48712f06"}], "new_record": [false]} | 2019-12-02 22:11:59.430456 | 2019-12-02 22:13:53.898564 | Book              |





Change Sets: Beyond here be dragons...

The discussion on Change Sets is under construction and possible wrong at this point.


Understanding Change Sets

References: Valkyrie WikiReform API Documentation | Reform Code | Disposable Code

Purpose

Provides separation of the persistence model from models used for updating data, primarily within forms. They provide support for validations, dirty tracking, and coercion of data values.

Libraries

Highly recommend reading Disposable and Reform documentation, starting with Disposable.  Change sets extend the Reform API which relies heavily on Disposable. 

  • Disposable creates a twin with properties allowing for changes to the twin that can either be discarded or synced with the resource to apply the changes to the resource. The twin can be used to render a form using Rails, SimpleForm, or other form generation approach.

  • Reform extends Disposable to provide dirty tracking of changes and data validation before syncing. It provides a means for controlling the twinning and syncing process.

Terminology

  • twin - non-persistent domain object associated with persistent model object. -- defined by Disposable

  • twin #sync - method that copies property data from twin to model object attributes; does not save -- defined by Disposable

  • twin #save - method that syncs a twin to a model and then saves the model -- defined by Disposable

    • NOT supported by Valkyrie because resources do not define #save method; instead, call #sync method on the change set and then use persister to save the resource

  • nested twin - a property in a twin that is associated with a model other than the main twin's model -- defined by Disposable

  • automatic coercion - coerce an incoming value to a specific dry-type (e.g. for type Types::Params::Integer, "1" will coerce to 1) -- defined by Disposable

  • manual coercion - can override twin setters to add code that coerces the incoming value (e.g. def title=(v); super(v.trim); end) -- defined by Disposable

  • composition - create a twin from 2 or more model objects which is useful for creating a form gathers data for multiple persistent objects -- defined by Disposable

  • dirty tracking - the ability to check if a property within the twin has changed and to check if the twin as a whole has changed

Properties

  • defining a property with the same name will set the property with the value from the resource when the ChangeSet is created AND set the properties value in the matching attribute in the resource when the #sync method is called

  • defining a property with a name not matching an attribute will raise NoMethodError (see virtual option)

  • OPTION readable - when true, the value of this property will NOT be read into the twin on creation

  • OPTION writable - when true, the value of this property will NOT be written to resource on sync

  • OPTION virtual - when true, the value of this property will NOT be read into the twin on creation nor written to the resource on sync which is useful when constructing forms to gather complex information for use in coercing values of other properties or as part of the validations

  • OPTION required - when true, method required?(property_name) returns true which is useful when constructing forms to set a field to required and when running custom validations

  • OPTION multiple - when true, method multiple?(property_name) returns true which is useful when constructing forms to set a field to allow multiple values and when running custom validations

  • only values that have property definitions are allowed which is used while processing params returned from a form


Define Change Sets with Properties

example book change set in app/change_sets/book_change_set.rb
# frozen_string_literal: true 
class BookChangeSet < Valkyrie::ChangeSet 
  property :title, multiple: true, required: true 
  property :author, multiple: true, required: true 
  property :series, multiple: false, required: false 
  property :member_ids, multiple: true, required: false 
end


example page change set in app/change_sets/page_change_set.rb
# frozen_string_literal: true 
class PageChangeSet < Valkyrie::ChangeSet 
  property :page_num, multiple: false, required: true 
  property :structure, multiple: false, required: false 
  property :image, multiple: false, required: false 
end

Test Persistence with Change Sets


In rails console...

test validate new resource with change sets
book = Book.new
book_cs = BookChangeSet.new(book)
book_cs.changed? # false
book_cs.title = "Ender's Game"    # update title in the change set
book_cs.changed?                  # true  - tests if anything has changed
book_cs.changed? :title           # true  - tests if the title changed
book_cs.changed? :author          # false - tests if the author changed
book.title # []                   # the book resource remains unchanged
book_cs.sync
book.title # ["Ender's Game"]     # book is updated after sync
book.persisted?                   # false - book is not yet in the database because it was created above
 
book_cs.save                      # raise no NoMethodError
pg_persister.save(resource: book) # use persister to save resource after syncing
book.persisted?                   # true - book was created in the database by the persister


Define Change Sets with Validations

Reference: ActiveRecord validations

Validations

Supports validations through ActiveModel validations. Not all ActiveRecord validations are supported (e.g. uniqueness is not supported). Some common examples are...

  • validates property_name, property_name, ..., presence - passes if the property or list of properties all have values present

  • validates_with ClassName[, property: property_name] - calls #validate method in the identified class to determine if the property's value is valid; if no property_name is identified, it is used to validate the change set as a whole


example book change set in app/change_sets/book_change_set.rb
# frozen_string_literal: true 
class BookChangeSet < Valkyrie::ChangeSet 
  property :title, multiple: true, required: true 
  property :author, multiple: true, required: true 
  property :series, multiple: false, required: false 
  property :member_ids, multiple: true, required: false 
 
  validates :title, :author, presence: true
  validates_with SeriesValidator
end


example custom validator in app/validators/series_validator.rb
# frozen_string_literal: true
class SeriesValidator < ActiveModel::Validator
  # ensure the property exists and is in the controlled vocabulary
  def validate(record)
    return ['Joe Pike', 'Elvis Cole', 'Elvis Cole/Joe Pike'].include? record.series)
    record.errors.add :series, "#{record.series} is not a valid series"
  end
end


Test Validation with Change Sets

In rails console...

test validate new resource with change sets
book_cs = BookChangeSet.new(Book.new)
book_cs.title = "Ender's Game"
book = book_cs.save

saved_book = ValkyriePgDemo.pg_persister.save(resource: book)
book.id # nil
saved_book.id # #<Valkyrie::ID:0x00007faf9bb499a0 @id="ad494304-ca12-42d6-a28b-34e60d63fb92">
 
retrieved_book = ValkyriePgDemo.pg_query_service.find_by(id: saved_book.id)
retrieved_book.title # ['Free Fall']


test edit existing resource with change sets
book_cs = BookChangeSet.new(Book.new)
book_cs.title = "Ender's Game"
book = book_cs.save

saved_book = ValkyriePgDemo.pg_persister.save(resource: book)
book.id # nil
saved_book.id # #<Valkyrie::ID:0x00007faf9bb499a0 @id="ad494304-ca12-42d6-a28b-34e60d63fb92">
 
retrieved_book = ValkyriePgDemo.pg_query_service.find_by(id: saved_book.id)
retrieved_book.title # ['Free Fall']







Valkyrie in Hyrax

Define non-work resource

Defining resources is basically the same as for a non-Hyrax Valkyrie app.  But it inherits from Hyrax::Resource instead of Valkyrie::Resource.   In this case the resource is supporting and not part of the PCDM model.

example book resource in app/resources/book.rb
# frozen_string_literal: true 
class CollectionType < Hyrax::Resource 
  attribute :title, Valkyrie::Types::String 
  attribute :description, Valkyrie::Types::String 
  attribute :machine_id, Valkyrie::Types::ID 
end


Define work resource

example book resource in app/resources/book.rb
# frozen_string_literal: true 
class Book < Hyrax::Work # Hyrax::Work isa Hyrax::Resource isa Valkyrie::Resource 
  include Hyrax::Schema(:core_metadata)
 
  attribute :author, Valkyrie::Types::Set.of(Valkyrie::Types::String).meta(ordered: true) 
  attribute :series, Valkyrie::Types::String.optional 
end

NOTE: This produces basically the same attribute definitions as those created for the Valkyrie app.  The title attribute is defined by the Hyrax::Schema.  The member_ids attribute is defined by Hyrax::Work.

NOTE: You could also use include Hyrax::Schema(:basic_metadata) to use the default metadata defined by Hyrax instead of defining it all in the Book work.

Core and Basic metadata defined in...  https://github.com/samvera/hyrax/tree/master/config/metadata

These are loaded using... https://github.com/samvera/hyrax/blob/master/app/services/hyrax/simple_schema_loader.rb


example page resource in app/resources/book.rb
# frozen_string_literal: true 
class Page < Hyrax::Work 
  include Hyrax::Schema(:core_metadata) 
 
  attribute :page_num, Valkyrie::Types::String.optional 
  attribute :structure, Valkyrie::Types::String.optional 
  attribute :image_id, Valkyrie::Types::ID.optional 
end




  • No labels