valkyrie-sequel 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 147de175c6560bcb1836553dd0c5c3e94908c5f8
4
+ data.tar.gz: e48b68150de3a5ed8efb8ac4064866db769227ee
5
+ SHA512:
6
+ metadata.gz: 546160cf2ab0d62a4e2196bc8f390510999cc8e2e9e74e6aa3f483eec6c2f236b45103361d37c31f98d84cf4a45fbf4dd98cbe6cf389ad20aefe594eb7326a6f
7
+ data.tar.gz: 396c775f741c4d972e372c5400cd9f23236dac76c983e5d3183553726c745e3bf30be4dbf4dbb17e456d1ee29fbc8eaa551d0e3dc8f7d929e5bddd0629ab688c
@@ -0,0 +1,33 @@
1
+ ---
2
+ version: 2
3
+ jobs:
4
+ build:
5
+ docker:
6
+ - image: circleci/ruby:2.3.7-node-browsers
7
+ environment:
8
+ RAILS_ENV: test
9
+ DB_HOST: localhost
10
+ DB_USERNAME: valkyrie_sequel
11
+ DB_PASSWORD: ""
12
+ DB_DATABASE: "valkyrie_sequel_test"
13
+ - image: postgres:10.3-alpine
14
+ environment:
15
+ POSTGRES_USER: valkyrie_sequel
16
+ POSTGRES_DB: valkyrie_sequel_test
17
+ POSTGRES_PASSWORD: ""
18
+ steps:
19
+ - checkout
20
+ # Restore Cached Dependencies
21
+ - type: cache-restore
22
+ name: Restore bundle cache
23
+ key: valkyrie-sequel-{{ checksum "Gemfile" }}
24
+ # Bundle install dependencies
25
+ - run: bundle install --path vendor/bundle
26
+ # Cache Dependencies
27
+ - type: cache-save
28
+ name: Store bundle cache
29
+ key: valkyrie-sequel-{{ checksum "Gemfile" }}
30
+ paths:
31
+ - vendor/bundle
32
+ - run: bundle exec rubocop
33
+ - run: bundle exec rspec spec
data/.gitignore ADDED
@@ -0,0 +1,12 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ Gemfile.lock
10
+
11
+ # rspec failure tracking
12
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,22 @@
1
+ inherit_gem:
2
+ bixby: bixby_default.yml
3
+ AllCops:
4
+ DisplayCopNames: true
5
+ TargetRubyVersion: 2.3
6
+ Exclude:
7
+ - 'bin/*'
8
+ - 'db/schema.rb'
9
+ - 'vendor/**/*'
10
+ Naming/FileName:
11
+ Exclude:
12
+ - 'valkyrie-sequel.gemspec'
13
+ - 'Gemfile'
14
+ Metrics/BlockLength:
15
+ Exclude:
16
+ - 'spec/**/*.rb'
17
+ Metrics/ClassLength:
18
+ Exclude:
19
+ - 'lib/valkyrie/sequel/query_service.rb'
20
+ Metrics/ParameterLists:
21
+ Exclude:
22
+ - 'lib/valkyrie/sequel/metadata_adapter.rb'
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.4.3
5
+ before_install: gem install bundler -v 1.16.1
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+ source "https://rubygems.org"
3
+
4
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
5
+
6
+ # Specify your gem's dependencies in valkyrie-sequel.gemspec
7
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,14 @@
1
+ Copyright 2018 Princeton University Library
2
+ Additional copyright may be held by others, as reflected in the commit history.
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
data/README.md ADDED
@@ -0,0 +1,28 @@
1
+ # Valkyrie::Sequel
2
+
3
+ Valkyrie adapter for postgres using [Sequel](https://github.com/jeremyevans/sequel)
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'valkyrie-sequel'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install valkyrie-sequel
20
+
21
+ ## Running Specs
22
+
23
+ 1. Ensure Postgres is installed (`brew install postgresql` on Mac)
24
+ 2. If necessary, provide the environment variables DB_USERNAME, DB_PASSWORD,
25
+ DB_HOST, DB_PORT, and DB_DATABASE to the following commands. This should not be
26
+ necessary on a local development setup, though.
27
+ 3. `rake db:create`
28
+ 4. `rspec spec`
data/Rakefile ADDED
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+ require "bundler/gem_tasks"
3
+ require "rspec/core/rake_task"
4
+
5
+ RSpec::Core::RakeTask.new(:spec)
6
+
7
+ task default: :spec
8
+
9
+ namespace :db do
10
+ task :environment do
11
+ require 'valkyrie/sequel'
12
+ require_relative 'spec/support/db_connection_info'
13
+ end
14
+ desc "Create Test Database"
15
+ task create: :environment do
16
+ connection = Sequel.connect(DB_CONNECTION_INFO.merge(adapter: :postgres, database: :postgres))
17
+ begin
18
+ connection.execute "CREATE DATABASE #{DB_CONNECTION_INFO[:database]}"
19
+ puts "Database #{DB_CONNECTION_INFO[:database]} created."
20
+ rescue Sequel::DatabaseError
21
+ puts "Database already exists"
22
+ end
23
+ end
24
+ desc "Drop Test Database"
25
+ task drop: :environment do
26
+ new_connection = Sequel.connect(DB_CONNECTION_INFO.merge(adapter: :postgres, database: :postgres))
27
+ new_connection.execute "DROP DATABASE IF EXISTS #{DB_CONNECTION_INFO[:database]}"
28
+ puts "#{DB_CONNECTION_INFO[:database]} dropped"
29
+ end
30
+ end
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "valkyrie/sequel"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
File without changes
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+ Sequel.migration do
3
+ up do
4
+ run 'CREATE EXTENSION IF NOT EXISTS "uuid-ossp"'
5
+ create_table :orm_resources do
6
+ column :id, :uuid, default: Sequel.function(:uuid_generate_v4), primary_key: true
7
+ column :metadata, :jsonb, default: '{}', index: { type: :gin }
8
+ String :internal_resource, index: true
9
+ Integer :lock_version, index: true
10
+ DateTime :created_at, index: true, default: ::Sequel::CURRENT_TIMESTAMP
11
+ DateTime :updated_at, index: true
12
+ end
13
+ run 'CREATE INDEX orm_resources_metadata_index_pathops ON orm_resources USING gin (metadata jsonb_path_ops)'
14
+ end
15
+ down do
16
+ drop_table :orm_resources
17
+ run 'DROP EXTENSION "uuid-ossp"'
18
+ end
19
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+ require 'oj'
3
+ module Sequel
4
+ def self.object_to_json(obj, *_args, &_block)
5
+ ::Oj.dump(obj.as_json)
6
+ end
7
+
8
+ def self.parse_json(json)
9
+ Oj.load(json, create_additions: false, mode: :compat)
10
+ end
11
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+ module Valkyrie::Sequel
3
+ require 'valkyrie/sequel/resource_factory'
4
+ require 'valkyrie/sequel/query_service'
5
+ require 'valkyrie/sequel/persister'
6
+ class MetadataAdapter
7
+ attr_reader :connection
8
+ def initialize(connection:)
9
+ @connection = connection.tap do |conn|
10
+ conn.extension(:pg_json)
11
+ conn.extension(:pg_streaming)
12
+ end
13
+ end
14
+
15
+ def persister
16
+ @persister ||= Persister.new(adapter: self)
17
+ end
18
+
19
+ def query_service
20
+ @query_service ||= QueryService.new(adapter: self)
21
+ end
22
+
23
+ def resource_factory
24
+ @resource_factory ||= ResourceFactory.new(adapter: self)
25
+ end
26
+
27
+ def id
28
+ @id ||= begin
29
+ to_hash = "sequel://#{host}:#{port}:#{database}"
30
+ Valkyrie::ID.new(Digest::MD5.hexdigest(to_hash))
31
+ end
32
+ end
33
+
34
+ def perform_migrations!
35
+ Sequel.extension :migration
36
+ Sequel::Migrator.run(connection, "#{__dir__}/../../../db/migrations")
37
+ end
38
+
39
+ def reset_database!
40
+ Sequel.extension :migration
41
+ Sequel::Migrator.run(connection, "#{__dir__}/../../../db/migrations", target: 0)
42
+ perform_migrations!
43
+ end
44
+
45
+ def resources
46
+ connection.from(:orm_resources)
47
+ end
48
+
49
+ private
50
+
51
+ def host
52
+ connection.opts[:host]
53
+ end
54
+
55
+ def port
56
+ connection.opts[:port]
57
+ end
58
+
59
+ def database
60
+ connection.opts[:database]
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,151 @@
1
+ # frozen_string_literal: true
2
+ module Valkyrie::Sequel
3
+ class Persister
4
+ attr_reader :adapter
5
+ delegate :resource_factory, to: :adapter
6
+ delegate :resources, :connection, to: :adapter
7
+ def initialize(adapter:)
8
+ @adapter = adapter
9
+ end
10
+
11
+ def save(resource:)
12
+ object_attributes = resource_factory.from_resource(resource: resource)
13
+ output = create_or_update(resource: resource, attributes: object_attributes)
14
+ resource_factory.to_resource(object: output)
15
+ end
16
+
17
+ def save_all(resources:)
18
+ connection.transaction do
19
+ output = SaveAllPersister::Factory.new(persister: self).for(resources: resources).persist!
20
+ raise Valkyrie::Persistence::StaleObjectError, "One or more resources have been updated by another process." if output.length != resources.length
21
+ output.map do |object|
22
+ resource_factory.to_resource(object: object)
23
+ end
24
+ end
25
+ end
26
+
27
+ class SaveAllPersister
28
+ class Factory
29
+ delegate :adapter, to: :persister
30
+ delegate :resource_factory, to: :adapter
31
+ delegate :resources, to: :adapter
32
+ attr_reader :persister
33
+ def initialize(persister:)
34
+ @persister = persister
35
+ end
36
+
37
+ # Resources have to be handled differently based on whether or not
38
+ # optimistic locking is enabled. Splitting it into two upserts allows
39
+ # for faster save_all while still handling optimistic locking.
40
+ def for(resources:)
41
+ grouped_resources = resources.group_by(&:optimistic_locking_enabled?)
42
+ locked_resources = grouped_resources[true] || []
43
+ unlocked_resources = grouped_resources[false] || []
44
+ CompositePersister.new(
45
+ [
46
+ SaveAllPersister.new(resources: locked_resources, relation: locking_relation, resource_factory: resource_factory),
47
+ SaveAllPersister.new(resources: unlocked_resources, relation: relation, resource_factory: resource_factory)
48
+ ]
49
+ )
50
+ end
51
+
52
+ def relation
53
+ resources.returning.insert_conflict(
54
+ target: :id,
55
+ update: update_branches
56
+ )
57
+ end
58
+
59
+ # Locking relation has an update_where condition.
60
+ def locking_relation
61
+ resources.returning.insert_conflict(
62
+ target: :id,
63
+ update: update_branches,
64
+ update_where: { Sequel[:orm_resources][:lock_version] => Sequel[:excluded][:lock_version] }
65
+ )
66
+ end
67
+
68
+ def update_branches
69
+ {
70
+ metadata: Sequel[:excluded][:metadata],
71
+ internal_resource: Sequel[:excluded][:internal_resource],
72
+ lock_version: Sequel[:excluded][:lock_version] + 1,
73
+ created_at: Sequel[:excluded][:created_at],
74
+ updated_at: Time.now.utc
75
+ }
76
+ end
77
+ end
78
+ attr_reader :resources, :relation, :resource_factory
79
+ def initialize(resources:, relation:, resource_factory:)
80
+ @resources = resources
81
+ @relation = relation
82
+ @resource_factory = resource_factory
83
+ end
84
+
85
+ def persist!
86
+ return [] if resources.empty?
87
+ Array.wrap(
88
+ relation.multi_insert(converted_resources)
89
+ )
90
+ end
91
+
92
+ def converted_resources
93
+ @converted_resources ||= resources.map do |resource|
94
+ output = resource_factory.from_resource(resource: resource)
95
+ output[:lock_version] ||= 0
96
+ output[:created_at] ||= Time.now.utc
97
+ output[:updated_at] ||= Time.now.utc
98
+ output
99
+ end
100
+ end
101
+ class CompositePersister
102
+ attr_reader :persisters
103
+ def initialize(persisters)
104
+ @persisters = persisters
105
+ end
106
+
107
+ def persist!
108
+ persisters.flat_map(&:persist!)
109
+ end
110
+ end
111
+ end
112
+ def delete(resource:)
113
+ resources.where(id: resource.id.to_s).delete
114
+ resource
115
+ end
116
+
117
+ def wipe!
118
+ resources.delete
119
+ end
120
+
121
+ private
122
+
123
+ def create_or_update(resource:, attributes:)
124
+ attributes[:updated_at] = Time.now.utc
125
+ attributes[:created_at] ||= Time.now.utc
126
+ return create(resource: resource, attributes: attributes) unless resource.persisted? && !exists?(id: attributes[:id])
127
+ update(resource: resource, attributes: attributes)
128
+ end
129
+
130
+ def create(resource:, attributes:)
131
+ attributes[:lock_version] = 0 if resource.optimistic_locking_enabled? && resources.columns.include?(:lock_version)
132
+ Array(resources.returning.insert(attributes)).first
133
+ end
134
+
135
+ def update(resource:, attributes:)
136
+ relation = resources.where(id: attributes[:id])
137
+ if resource.optimistic_locking_enabled?
138
+ relation = relation.where(lock_version: attributes[:lock_version]) if attributes[:lock_version]
139
+ attributes[:lock_version] = (Sequel[:lock_version] + 1)
140
+ end
141
+ attributes.delete(:lock_version) if attributes[:lock_version].nil?
142
+ output = relation.returning.update(attributes)
143
+ raise Valkyrie::Persistence::StaleObjectError, "The object #{resource.id} has been updated by another process." if output.blank? && resource.optimistic_locking_enabled?
144
+ Array(output).first
145
+ end
146
+
147
+ def exists?(id:)
148
+ resources.select(1).first(id: id).nil?
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,205 @@
1
+ # frozen_string_literal: true
2
+ module Valkyrie::Sequel
3
+ class QueryService
4
+ ACCEPTABLE_UUID = %r{\A(\{)?([a-fA-F0-9]{4}-?){8}(?(1)\}|)\z}
5
+ attr_reader :adapter
6
+ delegate :resources, :resource_factory, :connection, to: :adapter
7
+ def initialize(adapter:)
8
+ @adapter = adapter
9
+ end
10
+
11
+ def find_all
12
+ resources.use_cursor.lazy.map do |attributes|
13
+ resource_factory.to_resource(object: attributes)
14
+ end
15
+ end
16
+
17
+ def find_by(id:)
18
+ id = Valkyrie::ID.new(id.to_s) if id.is_a?(String)
19
+ validate_id(id)
20
+ raise Valkyrie::Persistence::ObjectNotFoundError unless ACCEPTABLE_UUID.match?(id.to_s)
21
+ attributes = resources.first(id: id.to_s)
22
+ raise Valkyrie::Persistence::ObjectNotFoundError unless attributes
23
+ resource_factory.to_resource(object: attributes)
24
+ end
25
+
26
+ def find_all_of_model(model:)
27
+ resources.where(internal_resource: model.to_s).map do |attributes|
28
+ resource_factory.to_resource(object: attributes)
29
+ end
30
+ end
31
+
32
+ def find_many_by_ids(ids:)
33
+ ids = ids.map do |id|
34
+ id = Valkyrie::ID.new(id.to_s) if id.is_a?(String)
35
+ validate_id(id)
36
+ id.to_s
37
+ end
38
+ ids = ids.select do |id|
39
+ ACCEPTABLE_UUID.match?(id)
40
+ end
41
+
42
+ resources.where(id: ids).map do |attributes|
43
+ resource_factory.to_resource(object: attributes)
44
+ end
45
+ end
46
+
47
+ def find_references_by(resource:, property:)
48
+ return [] if resource.id.blank? || resource[property].blank?
49
+ # only return ordered if needed to avoid performance penalties
50
+ if ordered_property?(resource: resource, property: property)
51
+ run_query(find_ordered_references_query, property.to_s, resource.id.to_s)
52
+ else
53
+ run_query(find_references_query, property.to_s, resource.id.to_s)
54
+ end
55
+ end
56
+
57
+ def find_inverse_references_by(resource: nil, id: nil, property:)
58
+ raise ArgumentError, "Provide resource or id" unless resource || id
59
+ ensure_persisted(resource) if resource
60
+ id ||= resource.id
61
+ internal_array = { property => [id: id.to_s] }
62
+ run_query(find_inverse_references_query, internal_array.to_json)
63
+ end
64
+
65
+ # Find and a record using a Valkyrie ID for an alternate ID, and construct
66
+ # a Valkyrie Resource
67
+ # @param [Valkyrie::ID] alternate_identifier
68
+ # @return [Valkyrie::Resource]
69
+ def find_by_alternate_identifier(alternate_identifier:)
70
+ alternate_identifier = Valkyrie::ID.new(alternate_identifier.to_s) if alternate_identifier.is_a?(String)
71
+ validate_id(alternate_identifier)
72
+ internal_array = { alternate_ids: [{ id: alternate_identifier.to_s }] }
73
+ run_query(find_inverse_references_query, internal_array.to_json).first || raise(Valkyrie::Persistence::ObjectNotFoundError)
74
+ end
75
+
76
+ def find_members(resource:, model: nil)
77
+ return [] if resource.id.blank?
78
+ if model
79
+ run_query(find_members_with_type_query, resource.id.to_s, model.to_s)
80
+ else
81
+ run_query(find_members_query, resource.id.to_s)
82
+ end
83
+ end
84
+
85
+ def find_parents(resource:)
86
+ find_inverse_references_by(resource: resource, property: :member_ids)
87
+ end
88
+
89
+ # Constructs a Valkyrie::Persistence::CustomQueryContainer using this query service
90
+ # @return [Valkyrie::Persistence::CustomQueryContainer]
91
+ def custom_queries
92
+ @custom_queries ||= ::Valkyrie::Persistence::CustomQueryContainer.new(query_service: self)
93
+ end
94
+
95
+ def run_query(query, *args)
96
+ connection[query, *args].map do |result|
97
+ resource_factory.to_resource(object: result)
98
+ end
99
+ end
100
+
101
+ private
102
+
103
+ # Generate the SQL query for retrieving member resources in PostgreSQL using a
104
+ # resource ID as an argument.
105
+ # @see https://guides.rubyonrails.org/active_record_querying.html#array-conditions
106
+ # @note this uses a CROSS JOIN for all combinations of member IDs with the
107
+ # IDs of their parents
108
+ # @see https://www.postgresql.org/docs/current/static/queries-table-expressions.html#QUERIES-FROM
109
+ # This also uses JSON functions in order to retrieve JSON property values
110
+ # @see https://www.postgresql.org/docs/current/static/functions-json.html
111
+ # @return [String]
112
+ def find_members_query
113
+ <<-SQL
114
+ SELECT member.* FROM orm_resources a,
115
+ jsonb_array_elements(a.metadata->'member_ids') WITH ORDINALITY AS b(member, member_pos)
116
+ JOIN orm_resources member ON (b.member->>'id')::#{id_type} = member.id WHERE a.id = ?
117
+ ORDER BY b.member_pos
118
+ SQL
119
+ end
120
+
121
+ # Generate the SQL query for retrieving member resources in PostgreSQL using a
122
+ # resource ID and resource type as arguments.
123
+ # @see https://guides.rubyonrails.org/active_record_querying.html#array-conditions
124
+ # @note this uses a CROSS JOIN for all combinations of member IDs with the
125
+ # IDs of their parents
126
+ # @see https://www.postgresql.org/docs/current/static/queries-table-expressions.html#QUERIES-FROM
127
+ # This also uses JSON functions in order to retrieve JSON property values
128
+ # @see https://www.postgresql.org/docs/current/static/functions-json.html
129
+ # @return [String]
130
+ def find_members_with_type_query
131
+ <<-SQL
132
+ SELECT member.* FROM orm_resources a,
133
+ jsonb_array_elements(a.metadata->'member_ids') WITH ORDINALITY AS b(member, member_pos)
134
+ JOIN orm_resources member ON (b.member->>'id')::#{id_type} = member.id WHERE a.id = ?
135
+ AND member.internal_resource = ?
136
+ ORDER BY b.member_pos
137
+ SQL
138
+ end
139
+
140
+ # Generate the SQL query for retrieving member resources in PostgreSQL using a
141
+ # JSON object literal as an argument (e. g. { "alternate_ids": [{"id": "d6e88f80-41b3-4dbf-a2a0-cd79e20f6d10"}] }).
142
+ # @see https://guides.rubyonrails.org/active_record_querying.html#array-conditions
143
+ # This uses JSON functions in order to retrieve JSON property values
144
+ # @see https://www.postgresql.org/docs/current/static/functions-json.html
145
+ # @return [String]
146
+ def find_inverse_references_query
147
+ <<-SQL
148
+ SELECT * FROM orm_resources WHERE
149
+ metadata @> ?
150
+ SQL
151
+ end
152
+
153
+ # Generate the SQL query for retrieving member resources in PostgreSQL using a
154
+ # JSON object literal and resource ID as arguments.
155
+ # @see https://guides.rubyonrails.org/active_record_querying.html#array-conditions
156
+ # @note this uses a CROSS JOIN for all combinations of member IDs with the
157
+ # IDs of their parents
158
+ # @see https://www.postgresql.org/docs/current/static/queries-table-expressions.html#QUERIES-FROM
159
+ # This also uses JSON functions in order to retrieve JSON property values
160
+ # @see https://www.postgresql.org/docs/current/static/functions-json.html
161
+ # @return [String]
162
+ def find_references_query
163
+ <<-SQL
164
+ SELECT DISTINCT member.* FROM orm_resources a,
165
+ jsonb_array_elements(a.metadata->?) AS b(member)
166
+ JOIN orm_resources member ON (b.member->>'id')::#{id_type} = member.id WHERE a.id = ?
167
+ SQL
168
+ end
169
+
170
+ def find_ordered_references_query
171
+ <<-SQL
172
+ SELECT member.* FROM orm_resources a,
173
+ jsonb_array_elements(a.metadata->?) WITH ORDINALITY AS b(member, member_pos)
174
+ JOIN orm_resources member ON (b.member->>'id')::#{id_type} = member.id WHERE a.id = ?
175
+ ORDER BY b.member_pos
176
+ SQL
177
+ end
178
+
179
+ # Accesses the data type in PostgreSQL used for the primary key
180
+ # (For example, a UUID)
181
+ # @see https://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/SchemaCache.html#method-i-columns_hash
182
+ # @return [Symbol]
183
+ def id_type
184
+ @id_type ||= :uuid
185
+ end
186
+
187
+ # Determines whether or not an Object is a Valkyrie ID
188
+ # @param [Object] id
189
+ # @raise [ArgumentError]
190
+ def validate_id(id)
191
+ raise ArgumentError, 'id must be a Valkyrie::ID' unless id.is_a? Valkyrie::ID
192
+ end
193
+
194
+ # Determines whether or not a resource has been persisted
195
+ # @param [Object] resource
196
+ # @raise [ArgumentError]
197
+ def ensure_persisted(resource)
198
+ raise ArgumentError, 'resource is not saved' unless resource.persisted?
199
+ end
200
+
201
+ def ordered_property?(resource:, property:)
202
+ resource.class.schema[property].meta.try(:[], :ordered)
203
+ end
204
+ end
205
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+ module Valkyrie::Sequel
3
+ class ResourceFactory::ORMConverter
4
+ attr_reader :object, :resource_factory
5
+ def initialize(object, resource_factory:)
6
+ @object = object
7
+ @resource_factory = resource_factory
8
+ end
9
+
10
+ def convert!
11
+ @resource ||= resource
12
+ end
13
+
14
+ private
15
+
16
+ # Construct a new Valkyrie Resource using the attributes retrieved from the database
17
+ # @return [Valkyrie::Resource]
18
+ def resource
19
+ resource_klass.new(
20
+ attributes.merge(
21
+ new_record: false,
22
+ Valkyrie::Persistence::Attributes::OPTIMISTIC_LOCK => lock_token
23
+ )
24
+ )
25
+ end
26
+
27
+ # Construct the optimistic lock token using the adapter and lock version for the Resource
28
+ # @return [Valkyrie::Persistence::OptimisticLockToken]
29
+ def lock_token
30
+ @lock_token ||=
31
+ Valkyrie::Persistence::OptimisticLockToken.new(
32
+ adapter_id: resource_factory.adapter_id,
33
+ token: object[:lock_version]
34
+ )
35
+ end
36
+
37
+ # Retrieve the Class used to construct the Valkyrie Resource
38
+ # @return [Class]
39
+ def resource_klass
40
+ internal_resource.constantize
41
+ end
42
+
43
+ # Access the String for the Valkyrie Resource type within the attributes
44
+ # @return [String]
45
+ def internal_resource
46
+ attributes[:internal_resource]
47
+ end
48
+
49
+ def attributes
50
+ @attributes ||= object.except(:metadata).merge(rdf_metadata).symbolize_keys
51
+ end
52
+
53
+ # Generate a Hash derived from Valkyrie Resource metadata encoded in the RDF
54
+ # @return [Hash]
55
+ def rdf_metadata
56
+ @rdf_metadata ||= Valkyrie::Persistence::Postgres::ORMConverter::RDFMetadata.new(object[:metadata]).result
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+ module Valkyrie::Sequel
3
+ class ResourceFactory::ResourceConverter
4
+ attr_reader :resource, :resource_factory
5
+ delegate :orm_class, :adapter, to: :resource_factory
6
+ delegate :resources, to: :adapter
7
+ def initialize(resource, resource_factory:)
8
+ @resource = resource
9
+ @resource_factory = resource_factory
10
+ end
11
+
12
+ def convert!
13
+ output = database_hash
14
+ output[:id] = resource.id.to_s if resource.id
15
+ output.delete(:id) unless !output[:id] || QueryService::ACCEPTABLE_UUID.match?(output[:id].to_s)
16
+ process_lock_token(output)
17
+ output
18
+ end
19
+
20
+ private
21
+
22
+ # Retrieves the optimistic lock token from the Valkyrie attribute value and
23
+ # sets it to the lock_version on ORM resource
24
+ # @see https://api.rubyonrails.org/classes/ActiveRecord/Locking/Optimistic.html
25
+ # @param [ORM::Resource] orm_object
26
+ def process_lock_token(orm_object)
27
+ return unless resource.respond_to?(Valkyrie::Persistence::Attributes::OPTIMISTIC_LOCK)
28
+ postgres_token = resource[Valkyrie::Persistence::Attributes::OPTIMISTIC_LOCK].find do |token|
29
+ token.adapter_id == resource_factory.adapter_id
30
+ end
31
+ return unless postgres_token
32
+ orm_object[:lock_version] = postgres_token.token
33
+ end
34
+
35
+ def database_hash
36
+ resource_hash.select do |k, _v|
37
+ primary_terms.include?(k)
38
+ end.compact.merge(
39
+ metadata: ::Sequel.pg_json(metadata_hash)
40
+ )
41
+ end
42
+
43
+ def resource_hash
44
+ @resource_hash ||= resource.to_h
45
+ end
46
+
47
+ # Convert attributes to all be arrays to better enable querying and
48
+ # "changing of minds" later on.
49
+ # @return [Hash]
50
+ def metadata_hash
51
+ Hash[
52
+ selected_resource_attributes.compact.map do |k, v|
53
+ [k, Array.wrap(v)]
54
+ end
55
+ ]
56
+ end
57
+
58
+ def selected_resource_attributes
59
+ resource_hash.select do |k, _v|
60
+ !primary_terms.include?(k) && !blacklist_terms.include?(k)
61
+ end
62
+ end
63
+
64
+ def primary_terms
65
+ [
66
+ :id,
67
+ :created_at,
68
+ :updated_at,
69
+ :internal_resource
70
+ ]
71
+ end
72
+
73
+ def blacklist_terms
74
+ [
75
+ :new_record
76
+ ]
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+ module Valkyrie::Sequel
3
+ class ResourceFactory
4
+ require 'valkyrie/sequel/resource_factory/resource_converter'
5
+ require 'valkyrie/sequel/resource_factory/orm_converter'
6
+ attr_reader :adapter
7
+ delegate :id, to: :adapter, prefix: true
8
+ def initialize(adapter:)
9
+ @adapter = adapter
10
+ end
11
+
12
+ def to_resource(object:)
13
+ ORMConverter.new(object, resource_factory: self).convert!
14
+ end
15
+
16
+ def from_resource(resource:)
17
+ ResourceConverter.new(resource, resource_factory: self).convert!
18
+ end
19
+
20
+ def orm_class
21
+ adapter.resources
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+ module Valkyrie
3
+ module Sequel
4
+ VERSION = "1.0.0"
5
+ end
6
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+ require "valkyrie/sequel/version"
3
+ require 'valkyrie'
4
+ require 'sequel'
5
+ require 'sequel/adapters/postgresql'
6
+ require 'sequel_pg'
7
+
8
+ module Valkyrie
9
+ module Sequel
10
+ ::Sequel.extension(:pg_json)
11
+ ::Sequel.extension(:pg_json_ops)
12
+ ::Sequel.default_timezone = :utc
13
+ ::Sequel.extension(:oj_parser)
14
+ require 'valkyrie/sequel/metadata_adapter'
15
+ end
16
+ end
@@ -0,0 +1,36 @@
1
+
2
+ # frozen_string_literal: true
3
+ lib = File.expand_path("../lib", __FILE__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require "valkyrie/sequel/version"
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = "valkyrie-sequel"
9
+ spec.version = Valkyrie::Sequel::VERSION
10
+ spec.authors = ["Trey Pendragon"]
11
+ spec.email = ["tpendragon@princeton.edu"]
12
+
13
+ spec.summary = 'Valkyrie::MetadataAdapter for Postgres using Sequel.'
14
+ spec.description = 'Valkyrie::MetadataAdapter for Postgres using Sequel.'
15
+ spec.homepage = "https://github.com/samvera-labs/valkyrie-sequel"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
18
+ f.match(%r{^(test|spec|features)/})
19
+ end
20
+ spec.bindir = "exe"
21
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
22
+ spec.require_paths = ["lib"]
23
+
24
+ spec.add_dependency "sequel"
25
+ spec.add_dependency "sequel_pg"
26
+ spec.add_dependency "valkyrie", "~> 1.5.0.RC1"
27
+ spec.add_dependency "oj"
28
+ spec.add_development_dependency "bundler", "~> 1.16"
29
+ spec.add_development_dependency "rake", "~> 10.0"
30
+ spec.add_development_dependency "rspec", "~> 3.0"
31
+ spec.add_development_dependency "bixby"
32
+ spec.add_development_dependency "pry-byebug"
33
+ spec.add_development_dependency "database_cleaner"
34
+ spec.add_development_dependency "coveralls"
35
+ spec.add_development_dependency "simplecov"
36
+ end
metadata ADDED
@@ -0,0 +1,234 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: valkyrie-sequel
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Trey Pendragon
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2019-04-11 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: sequel
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: sequel_pg
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: valkyrie
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 1.5.0.RC1
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 1.5.0.RC1
55
+ - !ruby/object:Gem::Dependency
56
+ name: oj
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: bundler
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '1.16'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '1.16'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rake
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '10.0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '10.0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rspec
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '3.0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '3.0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: bixby
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: pry-byebug
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: database_cleaner
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ - !ruby/object:Gem::Dependency
154
+ name: coveralls
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
167
+ - !ruby/object:Gem::Dependency
168
+ name: simplecov
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - ">="
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - ">="
179
+ - !ruby/object:Gem::Version
180
+ version: '0'
181
+ description: Valkyrie::MetadataAdapter for Postgres using Sequel.
182
+ email:
183
+ - tpendragon@princeton.edu
184
+ executables: []
185
+ extensions: []
186
+ extra_rdoc_files: []
187
+ files:
188
+ - ".circleci/config.yml"
189
+ - ".gitignore"
190
+ - ".rspec"
191
+ - ".rubocop.yml"
192
+ - ".travis.yml"
193
+ - Gemfile
194
+ - LICENSE
195
+ - README.md
196
+ - Rakefile
197
+ - bin/console
198
+ - bin/setup
199
+ - db/migrations/.gitkeep
200
+ - db/migrations/001_create_orm_resources.rb
201
+ - lib/sequel/extensions/oj_parser.rb
202
+ - lib/valkyrie/sequel.rb
203
+ - lib/valkyrie/sequel/metadata_adapter.rb
204
+ - lib/valkyrie/sequel/persister.rb
205
+ - lib/valkyrie/sequel/query_service.rb
206
+ - lib/valkyrie/sequel/resource_factory.rb
207
+ - lib/valkyrie/sequel/resource_factory/orm_converter.rb
208
+ - lib/valkyrie/sequel/resource_factory/resource_converter.rb
209
+ - lib/valkyrie/sequel/version.rb
210
+ - valkyrie-sequel.gemspec
211
+ homepage: https://github.com/samvera-labs/valkyrie-sequel
212
+ licenses: []
213
+ metadata: {}
214
+ post_install_message:
215
+ rdoc_options: []
216
+ require_paths:
217
+ - lib
218
+ required_ruby_version: !ruby/object:Gem::Requirement
219
+ requirements:
220
+ - - ">="
221
+ - !ruby/object:Gem::Version
222
+ version: '0'
223
+ required_rubygems_version: !ruby/object:Gem::Requirement
224
+ requirements:
225
+ - - ">="
226
+ - !ruby/object:Gem::Version
227
+ version: '0'
228
+ requirements: []
229
+ rubyforge_project:
230
+ rubygems_version: 2.6.14
231
+ signing_key:
232
+ specification_version: 4
233
+ summary: Valkyrie::MetadataAdapter for Postgres using Sequel.
234
+ test_files: []