valkyrie 2.1.0 → 3.0.0.pre.beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.circleci/config.yml +71 -36
- data/.lando.yml +58 -0
- data/.rubocop.yml +11 -1
- data/.tool-versions +1 -1
- data/CHANGELOG.md +94 -13
- data/CONTRIBUTING.md +30 -8
- data/README.md +24 -48
- data/Rakefile +26 -20
- data/db/config.yml +3 -10
- data/lib/generators/valkyrie/resource_generator.rb +3 -3
- data/lib/valkyrie/change_set.rb +3 -3
- data/lib/valkyrie/id.rb +12 -19
- data/lib/valkyrie/indexers/access_controls_indexer.rb +17 -17
- data/lib/valkyrie/persistence/buffered_persister.rb +2 -2
- data/lib/valkyrie/persistence/composite_persister.rb +3 -3
- data/lib/valkyrie/persistence/custom_query_container.rb +8 -16
- data/lib/valkyrie/persistence/fedora/list_node.rb +43 -43
- data/lib/valkyrie/persistence/fedora/metadata_adapter.rb +5 -1
- data/lib/valkyrie/persistence/fedora/ordered_list.rb +90 -90
- data/lib/valkyrie/persistence/fedora/ordered_reader.rb +5 -5
- data/lib/valkyrie/persistence/fedora/permissive_schema.rb +1 -1
- data/lib/valkyrie/persistence/fedora/persister/model_converter.rb +15 -16
- data/lib/valkyrie/persistence/fedora/persister/orm_converter.rb +14 -19
- data/lib/valkyrie/persistence/fedora/persister.rb +83 -83
- data/lib/valkyrie/persistence/fedora/query_service.rb +39 -41
- data/lib/valkyrie/persistence/memory/persister.rb +51 -35
- data/lib/valkyrie/persistence/memory/query_service.rb +26 -30
- data/lib/valkyrie/persistence/postgres/orm_converter.rb +52 -52
- data/lib/valkyrie/persistence/postgres/persister.rb +4 -1
- data/lib/valkyrie/persistence/postgres/query_service.rb +34 -34
- data/lib/valkyrie/persistence/shared/json_value_mapper.rb +1 -1
- data/lib/valkyrie/persistence/solr/metadata_adapter.rb +15 -3
- data/lib/valkyrie/persistence/solr/model_converter.rb +323 -340
- data/lib/valkyrie/persistence/solr/orm_converter.rb +4 -4
- data/lib/valkyrie/persistence/solr/persister.rb +16 -4
- data/lib/valkyrie/persistence/solr/queries/find_by_alternate_identifier_query.rb +1 -1
- data/lib/valkyrie/persistence/solr/queries/find_by_id_query.rb +1 -1
- data/lib/valkyrie/persistence/solr/queries/find_members_query.rb +1 -1
- data/lib/valkyrie/persistence/solr/query_service.rb +12 -12
- data/lib/valkyrie/persistence/solr/repository.rb +17 -7
- data/lib/valkyrie/resource/access_controls.rb +1 -1
- data/lib/valkyrie/resource.rb +0 -1
- data/lib/valkyrie/specs/shared_specs/change_set.rb +1 -1
- data/lib/valkyrie/specs/shared_specs/file.rb +1 -0
- data/lib/valkyrie/specs/shared_specs/persister.rb +22 -4
- data/lib/valkyrie/specs/shared_specs/queries.rb +7 -0
- data/lib/valkyrie/specs/shared_specs/resource.rb +1 -1
- data/lib/valkyrie/specs/shared_specs/storage_adapter.rb +19 -0
- data/lib/valkyrie/specs/shared_specs/write_only/metadata_adapter.rb +62 -0
- data/lib/valkyrie/specs/shared_specs.rb +2 -0
- data/lib/valkyrie/storage/disk.rb +24 -1
- data/lib/valkyrie/storage/fedora.rb +17 -17
- data/lib/valkyrie/storage_adapter.rb +12 -12
- data/lib/valkyrie/types.rb +1 -1
- data/lib/valkyrie/version.rb +1 -1
- data/lib/valkyrie/vocab/pcdm_use.rb +12 -0
- data/lib/valkyrie.rb +13 -27
- data/tasks/dev.rake +14 -51
- data/valkyrie.gemspec +3 -6
- metadata +25 -63
- data/.docker-stack/valkyrie-development/docker-compose.yml +0 -53
- data/.docker-stack/valkyrie-test/docker-compose.yml +0 -53
- data/tasks/docker.rake +0 -31
@@ -48,7 +48,7 @@ module Valkyrie::Persistence::Solr
|
|
48
48
|
# Construct a Time object from the datestamp for the resource creation date indexed in Solr
|
49
49
|
# @return [Time]
|
50
50
|
def created_at
|
51
|
-
DateTime.parse(solr_document.fetch("created_at_dtsi").to_s).
|
51
|
+
DateTime.parse(solr_document.fetch("created_at_dtsi").to_s).new_offset(0)
|
52
52
|
end
|
53
53
|
|
54
54
|
# Construct a Time object from the datestamp for the date of the last resource update indexed in Solr
|
@@ -79,7 +79,7 @@ module Valkyrie::Persistence::Solr
|
|
79
79
|
|
80
80
|
# Construct the Hash containing the Valkyrie Resource attributes using the Solr Document
|
81
81
|
# @note this filters for attributes which have been indexed as stored multivalued texts (tsim)
|
82
|
-
# @see https://github.com/samvera-labs/valkyrie/blob/
|
82
|
+
# @see https://github.com/samvera-labs/valkyrie/blob/main/solr/config/schema.xml
|
83
83
|
# @see https://lucene.apache.org/solr/guide/defining-fields.html#defining-fields
|
84
84
|
# @return [Hash]
|
85
85
|
def attribute_hash
|
@@ -445,7 +445,7 @@ module Valkyrie::Persistence::Solr
|
|
445
445
|
# @return [Boolean]
|
446
446
|
def self.handles?(value)
|
447
447
|
return false unless value.to_s.start_with?("datetime-")
|
448
|
-
DateTime.iso8601(value.sub(/^datetime-/, '')).
|
448
|
+
DateTime.iso8601(value.sub(/^datetime-/, '')).new_offset(0)
|
449
449
|
rescue
|
450
450
|
false
|
451
451
|
end
|
@@ -453,7 +453,7 @@ module Valkyrie::Persistence::Solr
|
|
453
453
|
# Parses and casts the Solr field value into a UTC DateTime value
|
454
454
|
# @return [Time]
|
455
455
|
def result
|
456
|
-
DateTime.parse(value.sub(/^datetime-/, '')).
|
456
|
+
DateTime.parse(value.sub(/^datetime-/, '')).new_offset(0)
|
457
457
|
end
|
458
458
|
end
|
459
459
|
end
|
@@ -6,7 +6,7 @@ module Valkyrie::Persistence::Solr
|
|
6
6
|
# Most methods are delegated to {Valkyrie::Persistence::Solr::Repository}
|
7
7
|
class Persister
|
8
8
|
attr_reader :adapter
|
9
|
-
delegate :connection, :resource_factory, to: :adapter
|
9
|
+
delegate :connection, :query_service, :resource_factory, :write_only?, :soft_commit?, to: :adapter
|
10
10
|
|
11
11
|
# @param adapter [Valkyrie::Persistence::Solr::MetadataAdapter] The adapter with the
|
12
12
|
# configured solr connection.
|
@@ -15,11 +15,23 @@ module Valkyrie::Persistence::Solr
|
|
15
15
|
end
|
16
16
|
|
17
17
|
# (see Valkyrie::Persistence::Memory::Persister#save)
|
18
|
-
|
19
|
-
|
18
|
+
# @return [Boolean] If write_only, whether saving succeeded.
|
19
|
+
def save(resource:, external_resource: false)
|
20
|
+
if write_only?
|
21
|
+
repository([resource]).persist
|
22
|
+
else
|
23
|
+
raise Valkyrie::Persistence::ObjectNotFoundError, "The object #{resource.id} is previously persisted but not found at save time." unless external_resource || valid_for_save?(resource)
|
24
|
+
repository([resource]).persist.first
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def valid_for_save?(resource)
|
29
|
+
return true unless resource.persisted? # a new resource
|
30
|
+
query_service.find_by(id: resource.id).present? # a persisted resource must be found
|
20
31
|
end
|
21
32
|
|
22
33
|
# (see Valkyrie::Persistence::Memory::Persister#save_all)
|
34
|
+
# @return [Boolean] If write_only, whether saving succeeded.
|
23
35
|
def save_all(resources:)
|
24
36
|
repository(resources).persist
|
25
37
|
end
|
@@ -39,7 +51,7 @@ module Valkyrie::Persistence::Solr
|
|
39
51
|
# @param [Array<Valkyrie::Resource>] resources
|
40
52
|
# @return [Valkyrie::Persistence::Solr::Repository]
|
41
53
|
def repository(resources)
|
42
|
-
Valkyrie::Persistence::Solr::Repository.new(resources: resources,
|
54
|
+
Valkyrie::Persistence::Solr::Repository.new(resources: resources, persister: self)
|
43
55
|
end
|
44
56
|
end
|
45
57
|
end
|
@@ -32,7 +32,7 @@ module Valkyrie::Persistence::Solr::Queries
|
|
32
32
|
# @note the field used here is alternate_ids_ssim and the value is prefixed by "id-"
|
33
33
|
# @return [Hash]
|
34
34
|
def resource
|
35
|
-
connection.get("select", params: { q: "alternate_ids_ssim:\"id-#{alternate_identifier}\"", fl: "*", rows: 1 })["response"]["docs"].first
|
35
|
+
@resource ||= connection.get("select", params: { q: "alternate_ids_ssim:\"id-#{alternate_identifier}\"", fl: "*", rows: 1 })["response"]["docs"].first
|
36
36
|
end
|
37
37
|
end
|
38
38
|
end
|
@@ -31,7 +31,7 @@ module Valkyrie::Persistence::Solr::Queries
|
|
31
31
|
# Query Solr for for the first document with the ID in a field
|
32
32
|
# @return [Hash]
|
33
33
|
def resource
|
34
|
-
connection.get("select", params: { q: "id:\"#{id}\"", fl: "*", rows: 1 })["response"]["docs"].first
|
34
|
+
@resource ||= connection.get("select", params: { q: "id:\"#{id}\"", fl: "*", rows: 1 })["response"]["docs"].first
|
35
35
|
end
|
36
36
|
end
|
37
37
|
end
|
@@ -27,7 +27,7 @@ module Valkyrie::Persistence::Solr::Queries
|
|
27
27
|
# Results are ordered by the member IDs specified in the Valkyrie Resource attribute
|
28
28
|
# @yield [Valkyrie::Resource]
|
29
29
|
def each
|
30
|
-
return []
|
30
|
+
return [] if resource.id.blank?
|
31
31
|
member_ids.map { |id| unordered_members.find { |member| member.id == id } }.reject(&:nil?).each do |member|
|
32
32
|
yield member
|
33
33
|
end
|
@@ -97,19 +97,19 @@ module Valkyrie::Persistence::Solr
|
|
97
97
|
|
98
98
|
private
|
99
99
|
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
100
|
+
# (see Valkyrie::Persistence::Memory::QueryService#validate_id)
|
101
|
+
def validate_id(id)
|
102
|
+
raise ArgumentError, 'id must be a Valkyrie::ID' unless id.is_a? Valkyrie::ID
|
103
|
+
end
|
104
104
|
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
105
|
+
# (see Valkyrie::Persistence::Memory::QueryService#ensure_persisted)
|
106
|
+
def ensure_persisted(resource)
|
107
|
+
raise ArgumentError, 'resource is not saved' unless resource.persisted?
|
108
|
+
end
|
109
109
|
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
110
|
+
# (see Valkyrie::Persistence::Memory::QueryService#ordered_property?)
|
111
|
+
def ordered_property?(resource:, property:)
|
112
|
+
resource.ordered_attribute?(property)
|
113
|
+
end
|
114
114
|
end
|
115
115
|
end
|
@@ -3,17 +3,18 @@ module Valkyrie::Persistence::Solr
|
|
3
3
|
# Responsible for handling the logic for persisting or deleting multiple
|
4
4
|
# objects into or out of solr.
|
5
5
|
class Repository
|
6
|
-
|
6
|
+
SOFT_COMMIT_PARAMS = { softCommit: true, versions: true }.freeze
|
7
|
+
NO_COMMIT_PARAMS = { versions: true }.freeze
|
7
8
|
|
8
|
-
attr_reader :resources, :
|
9
|
+
attr_reader :resources, :persister
|
10
|
+
delegate :connection, :resource_factory, :write_only?, :soft_commit?, to: :persister
|
9
11
|
|
10
12
|
# @param [Array<Valkyrie::Resource>] resources
|
11
13
|
# @param [RSolr::Client] connection
|
12
14
|
# @param [ResourceFactory] resource_factory
|
13
|
-
def initialize(resources:,
|
15
|
+
def initialize(resources:, persister:)
|
14
16
|
@resources = resources
|
15
|
-
@
|
16
|
-
@resource_factory = resource_factory
|
17
|
+
@persister = persister
|
17
18
|
end
|
18
19
|
|
19
20
|
# Persist the resources into Solr
|
@@ -24,6 +25,7 @@ module Valkyrie::Persistence::Solr
|
|
24
25
|
solr_document(resource)
|
25
26
|
end
|
26
27
|
results = add_documents(documents)
|
28
|
+
return true if write_only?
|
27
29
|
versions = results["adds"]&.each_slice(2)&.to_h
|
28
30
|
documents.map do |document|
|
29
31
|
document["_version_"] = versions.fetch(document[:id])
|
@@ -35,7 +37,7 @@ module Valkyrie::Persistence::Solr
|
|
35
37
|
# @return [RSolr::HashWithResponse]
|
36
38
|
# rubocop:disable Style/IfUnlessModifier
|
37
39
|
def add_documents(documents)
|
38
|
-
connection.add documents, params:
|
40
|
+
connection.add documents, params: commit_params
|
39
41
|
rescue RSolr::Error::Http => exception
|
40
42
|
# Error 409 conflict is returned when versions do not match
|
41
43
|
if exception.response&.fetch(:status) == 409
|
@@ -48,7 +50,7 @@ module Valkyrie::Persistence::Solr
|
|
48
50
|
# Deletes a Solr Document using the ID
|
49
51
|
# @return [Array<Valkyrie::Resource>] resources which have been deleted from Solr
|
50
52
|
def delete
|
51
|
-
connection.delete_by_id resources.map { |resource| resource.id.to_s }, params:
|
53
|
+
connection.delete_by_id resources.map { |resource| resource.id.to_s }, params: commit_params
|
52
54
|
resources
|
53
55
|
end
|
54
56
|
|
@@ -75,5 +77,13 @@ module Valkyrie::Persistence::Solr
|
|
75
77
|
raise Valkyrie::Persistence::StaleObjectError, "One or more resources have been updated by another process." if resources.count > 1
|
76
78
|
raise Valkyrie::Persistence::StaleObjectError, "The object #{resources.first.id} has been updated by another process."
|
77
79
|
end
|
80
|
+
|
81
|
+
def commit_params
|
82
|
+
if persister.soft_commit?
|
83
|
+
SOFT_COMMIT_PARAMS
|
84
|
+
else
|
85
|
+
NO_COMMIT_PARAMS
|
86
|
+
end
|
87
|
+
end
|
78
88
|
end
|
79
89
|
end
|
@@ -11,7 +11,7 @@ module Valkyrie
|
|
11
11
|
# attribute :nested_resource
|
12
12
|
# end
|
13
13
|
#
|
14
|
-
# @see https://github.com/samvera/hydra-head/tree/
|
14
|
+
# @see https://github.com/samvera/hydra-head/tree/main/hydra-access-controls
|
15
15
|
# @see lib/valkyrie/indexers/access_controls_indexer/rb
|
16
16
|
module AccessControls
|
17
17
|
def self.included(klass)
|
data/lib/valkyrie/resource.rb
CHANGED
@@ -15,7 +15,6 @@ module Valkyrie
|
|
15
15
|
# @see lib/valkyrie/specs/shared_specs/resource.rb
|
16
16
|
# rubocop:disable Metrics/ClassLength
|
17
17
|
class Resource < Dry::Struct
|
18
|
-
include Draper::Decoratable
|
19
18
|
# Allows a Valkyrie::Resource to be instantiated without providing every
|
20
19
|
# available key, and makes sure the defaults are set up if no value is
|
21
20
|
# given.
|
@@ -75,7 +75,7 @@ RSpec.shared_examples 'a Valkyrie::ChangeSet' do |*_flags|
|
|
75
75
|
|
76
76
|
describe "#optimistic_locking_enabled?" do
|
77
77
|
it "delegates down to the resource" do
|
78
|
-
expect(change_set.optimistic_locking_enabled?).to eq
|
78
|
+
expect(change_set.optimistic_locking_enabled?).to eq change_set.resource.optimistic_locking_enabled?
|
79
79
|
end
|
80
80
|
end
|
81
81
|
end
|
@@ -9,6 +9,7 @@ RSpec.shared_examples 'a Valkyrie::StorageAdapter::File' do
|
|
9
9
|
it { is_expected.to respond_to(:read) }
|
10
10
|
it { is_expected.to respond_to(:rewind) }
|
11
11
|
it { is_expected.to respond_to(:id) }
|
12
|
+
it { is_expected.to respond_to(:close) }
|
12
13
|
describe "#disk_path" do
|
13
14
|
it "returns an existing disk path" do
|
14
15
|
expect(File.exist?(file.disk_path)).to eq true
|
@@ -50,6 +50,24 @@ RSpec.shared_examples 'a Valkyrie::Persister' do |*flags|
|
|
50
50
|
expect(reloaded.nested_resource.first.title).to eq ["Nested"]
|
51
51
|
end
|
52
52
|
|
53
|
+
context "when a persisted resource is not in the database" do
|
54
|
+
it "throws an ObjectNotFoundError" do
|
55
|
+
expect(resource).not_to be_persisted
|
56
|
+
saved = persister.save(resource: resource)
|
57
|
+
|
58
|
+
expect(saved).to be_persisted
|
59
|
+
persister.delete(resource: saved)
|
60
|
+
|
61
|
+
expect { persister.save(resource: saved) }.to raise_error(Valkyrie::Persistence::ObjectNotFoundError)
|
62
|
+
end
|
63
|
+
it "is okay if it's from another persister" do
|
64
|
+
memory_adapter = Valkyrie::Persistence::Memory::MetadataAdapter.new
|
65
|
+
saved = memory_adapter.persister.save(resource: resource)
|
66
|
+
|
67
|
+
expect { persister.save(resource: saved, external_resource: true) }.not_to raise_error
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
53
71
|
it "can persist single values" do
|
54
72
|
resource.single_value = "A single value"
|
55
73
|
|
@@ -194,9 +212,9 @@ RSpec.shared_examples 'a Valkyrie::Persister' do |*flags|
|
|
194
212
|
reloaded = query_service.find_by(id: book.id)
|
195
213
|
|
196
214
|
expect(reloaded.title.first.to_i).to eq(time1.to_i)
|
197
|
-
expect(reloaded.title.first.zone).to eq('
|
215
|
+
expect(reloaded.title.first.zone).to eq('+00:00')
|
198
216
|
expect(reloaded.author.first.to_i).to eq(time2.to_i)
|
199
|
-
expect(reloaded.author.first.zone).to eq('
|
217
|
+
expect(reloaded.author.first.zone).to eq('+00:00')
|
200
218
|
expect(reloaded.other_author.first).to eq "2019-01"
|
201
219
|
end
|
202
220
|
|
@@ -351,7 +369,7 @@ RSpec.shared_examples 'a Valkyrie::Persister' do |*flags|
|
|
351
369
|
# update the resource in the datastore to make its token stale
|
352
370
|
persister.save(resource: resource)
|
353
371
|
|
354
|
-
expect { persister.save(resource: resource) }.to raise_error(Valkyrie::Persistence::StaleObjectError
|
372
|
+
expect { persister.save(resource: resource) }.to raise_error(Valkyrie::Persistence::StaleObjectError)
|
355
373
|
end
|
356
374
|
end
|
357
375
|
|
@@ -425,7 +443,7 @@ RSpec.shared_examples 'a Valkyrie::Persister' do |*flags|
|
|
425
443
|
persister.save(resource: resource2)
|
426
444
|
|
427
445
|
expect { persister.save_all(resources: [resource1, resource2, resource3]) }
|
428
|
-
.to raise_error(Valkyrie::Persistence::StaleObjectError
|
446
|
+
.to raise_error(Valkyrie::Persistence::StaleObjectError)
|
429
447
|
end
|
430
448
|
end
|
431
449
|
end
|
@@ -464,6 +464,13 @@ RSpec.shared_examples 'a Valkyrie query provider' do
|
|
464
464
|
end
|
465
465
|
end
|
466
466
|
|
467
|
+
describe ".custom_queries" do
|
468
|
+
it "raises NoMethodError when the custom query does not exist" do
|
469
|
+
expect(query_service.custom_queries).not_to respond_to :very_fake_query
|
470
|
+
expect { query_service.custom_queries.very_fake_query }.to raise_error(NoMethodError)
|
471
|
+
end
|
472
|
+
end
|
473
|
+
|
467
474
|
describe ".register_query_handler" do
|
468
475
|
it "can register a query handler" do
|
469
476
|
class QueryHandler
|
@@ -30,6 +30,25 @@ RSpec.shared_examples 'a Valkyrie::StorageAdapter' do
|
|
30
30
|
expect(uploaded_file.valid?(digests: { sha1: sha1 })).to be true
|
31
31
|
end
|
32
32
|
|
33
|
+
it "doesn't leave a file handle open on upload/find_by" do
|
34
|
+
# No file handle left open from upload.
|
35
|
+
resource = Valkyrie::Specs::CustomResource.new(id: "testdiscovery")
|
36
|
+
pre_open_files = open_files
|
37
|
+
uploaded_file = storage_adapter.upload(file: file, original_filename: 'foo.jpg', resource: resource, fake_upload_argument: true)
|
38
|
+
file.close
|
39
|
+
expect(pre_open_files.size).to eq open_files.size
|
40
|
+
|
41
|
+
# No file handle left open from find_by
|
42
|
+
pre_open_files = open_files
|
43
|
+
the_file = storage_adapter.find_by(id: uploaded_file.id)
|
44
|
+
expect(the_file).to be_kind_of Valkyrie::StorageAdapter::File
|
45
|
+
expect(pre_open_files.size).to eq open_files.size
|
46
|
+
end
|
47
|
+
|
48
|
+
def open_files
|
49
|
+
`lsof +D . | awk '{print $9}'`.split.uniq[1..-1]
|
50
|
+
end
|
51
|
+
|
33
52
|
it "can upload, validate, re-fetch, and delete a file" do
|
34
53
|
resource = Valkyrie::Specs::CustomResource.new(id: "test")
|
35
54
|
sha1 = Digest::SHA1.file(file).to_s
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
RSpec.shared_examples 'a write-only Valkyrie::MetadataAdapter' do |passed_adapter|
|
3
|
+
before do
|
4
|
+
raise 'adapter must be set with `let(:adapter)`' unless
|
5
|
+
defined? adapter
|
6
|
+
end
|
7
|
+
subject { passed_adapter || adapter }
|
8
|
+
let(:persister) { adapter.persister }
|
9
|
+
it { is_expected.to respond_to(:persister).with(0).arguments }
|
10
|
+
it { is_expected.to respond_to(:id).with(0).arguments }
|
11
|
+
|
12
|
+
describe "#id" do
|
13
|
+
it "is a valid string representation of an MD5 hash" do
|
14
|
+
expect(adapter.id).to be_a Valkyrie::ID
|
15
|
+
expect(adapter.id.to_s.length).to eq 32
|
16
|
+
expect(adapter.id.to_s).to match(/^[a-f,0-9]+$/)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
describe "#write_only?" do
|
21
|
+
it "returns true" do
|
22
|
+
expect(adapter).to be_write_only
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe "persister" do
|
27
|
+
before do
|
28
|
+
class WriteOnlyCustomResource < Valkyrie::Resource
|
29
|
+
include Valkyrie::Resource::AccessControls
|
30
|
+
attribute :title
|
31
|
+
attribute :author
|
32
|
+
attribute :other_author
|
33
|
+
attribute :member_ids
|
34
|
+
attribute :nested_resource
|
35
|
+
attribute :single_value, Valkyrie::Types::String.optional
|
36
|
+
attribute :ordered_authors, Valkyrie::Types::Array.of(Valkyrie::Types::Anything).meta(ordered: true)
|
37
|
+
attribute :ordered_nested, Valkyrie::Types::Array.of(WriteOnlyCustomResource).meta(ordered: true)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
after do
|
41
|
+
Object.send(:remove_const, :WriteOnlyCustomResource)
|
42
|
+
end
|
43
|
+
|
44
|
+
subject { persister }
|
45
|
+
let(:resource_class) { WriteOnlyCustomResource }
|
46
|
+
let(:resource) { resource_class.new }
|
47
|
+
|
48
|
+
it { is_expected.to respond_to(:save).with_keywords(:resource) }
|
49
|
+
it { is_expected.to respond_to(:save_all).with_keywords(:resources) }
|
50
|
+
it { is_expected.to respond_to(:delete).with_keywords(:resource) }
|
51
|
+
|
52
|
+
it "can save a resource" do
|
53
|
+
expect(persister.save(resource: resource)).to eq true
|
54
|
+
end
|
55
|
+
|
56
|
+
it "can save multiple resources at once" do
|
57
|
+
resource2 = resource_class.new
|
58
|
+
results = persister.save_all(resources: [resource, resource2])
|
59
|
+
expect(results).to eq true
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -13,3 +13,5 @@ require 'valkyrie/specs/shared_specs/change_set_persister.rb'
|
|
13
13
|
require 'valkyrie/specs/shared_specs/file.rb'
|
14
14
|
require 'valkyrie/specs/shared_specs/change_set.rb'
|
15
15
|
require 'valkyrie/specs/shared_specs/solr_indexer.rb'
|
16
|
+
# Write-only tests.
|
17
|
+
require 'valkyrie/specs/shared_specs/write_only/metadata_adapter.rb'
|
@@ -36,11 +36,34 @@ module Valkyrie::Storage
|
|
36
36
|
# @return [Valkyrie::StorageAdapter::File]
|
37
37
|
# @raise Valkyrie::StorageAdapter::FileNotFound if nothing is found
|
38
38
|
def find_by(id:)
|
39
|
-
Valkyrie::StorageAdapter::File.new(id: Valkyrie::ID.new(id.to_s), io:
|
39
|
+
Valkyrie::StorageAdapter::File.new(id: Valkyrie::ID.new(id.to_s), io: LazyFile.open(file_path(id), 'rb'))
|
40
40
|
rescue Errno::ENOENT
|
41
41
|
raise Valkyrie::StorageAdapter::FileNotFound
|
42
42
|
end
|
43
43
|
|
44
|
+
## LazyFile takes File.open parameters but doesn't leave a file handle open on
|
45
|
+
# instantiation. This way StorageAdapter#find_by doesn't open a handle
|
46
|
+
# silently and never clean up after itself.
|
47
|
+
class LazyFile
|
48
|
+
def self.open(path, mode)
|
49
|
+
# Open the file regularly and close it, so it can error if it doesn't
|
50
|
+
# exist.
|
51
|
+
File.open(path, mode).close
|
52
|
+
new(path, mode)
|
53
|
+
end
|
54
|
+
|
55
|
+
delegate(*(File.instance_methods - Object.instance_methods), to: :_inner_file)
|
56
|
+
|
57
|
+
def initialize(path, mode)
|
58
|
+
@__path = path
|
59
|
+
@__mode = mode
|
60
|
+
end
|
61
|
+
|
62
|
+
def _inner_file
|
63
|
+
@_inner_file ||= File.open(@__path, @__mode)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
44
67
|
# Delete the file on disk associated with the given identifier.
|
45
68
|
# @param id [Valkyrie::ID]
|
46
69
|
def delete(id:)
|
@@ -37,7 +37,7 @@ module Valkyrie::Storage
|
|
37
37
|
def upload(file:, original_filename:, resource:, content_type: "application/octet-stream", # rubocop:disable Metrics/ParameterLists
|
38
38
|
resource_uri_transformer: default_resource_uri_transformer, **_extra_arguments)
|
39
39
|
identifier = resource_uri_transformer.call(resource, base_url) + '/original'
|
40
|
-
sha1 =
|
40
|
+
sha1 = [5, 6].include?(fedora_version) ? "sha" : "sha1"
|
41
41
|
connection.http.put do |request|
|
42
42
|
request.url identifier
|
43
43
|
request.headers['Content-Type'] = content_type
|
@@ -83,24 +83,24 @@ module Valkyrie::Storage
|
|
83
83
|
|
84
84
|
private
|
85
85
|
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
86
|
+
# @return [IOProxy]
|
87
|
+
def response(id:)
|
88
|
+
response = connection.http.get(fedora_identifier(id: id))
|
89
|
+
raise Valkyrie::StorageAdapter::FileNotFound unless response.success?
|
90
|
+
IOProxy.new(response.body)
|
91
|
+
end
|
92
92
|
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
end
|
93
|
+
def default_resource_uri_transformer
|
94
|
+
lambda do |resource, base_url|
|
95
|
+
id = CGI.escape(resource.id.to_s)
|
96
|
+
RDF::URI.new(base_url + id)
|
98
97
|
end
|
98
|
+
end
|
99
99
|
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
100
|
+
def base_url
|
101
|
+
pre_divider = base_path.starts_with?(SLASH) ? '' : SLASH
|
102
|
+
post_divider = base_path.ends_with?(SLASH) ? '' : SLASH
|
103
|
+
"#{connection.http.url_prefix}#{pre_divider}#{base_path}#{post_divider}"
|
104
|
+
end
|
105
105
|
end
|
106
106
|
end
|
@@ -67,7 +67,7 @@ module Valkyrie
|
|
67
67
|
class File < Dry::Struct
|
68
68
|
attribute :id, Valkyrie::Types::Any
|
69
69
|
attribute :io, Valkyrie::Types::Any
|
70
|
-
delegate :size, :read, :rewind, to: :io
|
70
|
+
delegate :size, :read, :rewind, :close, to: :io
|
71
71
|
def stream
|
72
72
|
io
|
73
73
|
end
|
@@ -106,20 +106,20 @@ module Valkyrie
|
|
106
106
|
|
107
107
|
private
|
108
108
|
|
109
|
-
|
110
|
-
|
111
|
-
|
109
|
+
def tmp_file_name
|
110
|
+
id.to_s.tr(':/', '__')
|
111
|
+
end
|
112
112
|
|
113
|
-
|
114
|
-
|
115
|
-
|
113
|
+
def tmp_file_path
|
114
|
+
::File.join(Dir.tmpdir, tmp_file_name)
|
115
|
+
end
|
116
116
|
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
end
|
117
|
+
def tmp_file
|
118
|
+
@tmp_file ||= ::File.open(tmp_file_path, 'w+b') do |f|
|
119
|
+
IO.copy_stream(io, f)
|
120
|
+
f
|
122
121
|
end
|
122
|
+
end
|
123
123
|
end
|
124
124
|
end
|
125
125
|
end
|
data/lib/valkyrie/types.rb
CHANGED
@@ -84,7 +84,7 @@ module Valkyrie
|
|
84
84
|
Set = Array.constructor do |value|
|
85
85
|
value = Array[value]
|
86
86
|
clean_values = value.reject do |val|
|
87
|
-
|
87
|
+
(val.is_a?(Valkyrie::ID) && val.to_s == '') || val == ''
|
88
88
|
end.reject(&:nil?).uniq
|
89
89
|
|
90
90
|
clean_values.map do |val|
|
data/lib/valkyrie/version.rb
CHANGED
@@ -39,10 +39,22 @@ module Valkyrie::Vocab
|
|
39
39
|
"rdf:subClassOf": %(http://pcdm.org/resources#File),
|
40
40
|
"rdfs:isDefinedBy": %(pcdmuse:),
|
41
41
|
type: "rdfs:Class"
|
42
|
+
term :PreservationFile,
|
43
|
+
comment: %(Best quality representation of the Object appropriate for long-term
|
44
|
+
preservation.),
|
45
|
+
label: "preservation file",
|
46
|
+
"dct:replaces": %(http://pcdm.org/use#PreservationMasterFile),
|
47
|
+
"rdf:subClassOf": %(http://pcdm.org/resources#File),
|
48
|
+
"rdfs:isDefinedBy": %(pcdmuse:),
|
49
|
+
type: "rdfs:Class"
|
50
|
+
warn "[DEPRECATION] PCDM is deprecating '#{self.class}#PreservationMasterFile'. Use #{self.class}#PreservationFile instead."
|
51
|
+
# @deprecated
|
42
52
|
term :PreservationMasterFile,
|
43
53
|
comment: %(Best quality representation of the Object appropriate for long-term
|
44
54
|
preservation.),
|
45
55
|
label: "preservation master file",
|
56
|
+
"dct:isReplacedBy": %(http://pcdm.org/use#PreservationFile),
|
57
|
+
"owl:deprecated": true,
|
46
58
|
"rdf:subClassOf": %(http://pcdm.org/resources#File),
|
47
59
|
"rdfs:isDefinedBy": %(pcdmuse:),
|
48
60
|
type: "rdfs:Class"
|
data/lib/valkyrie.rb
CHANGED
@@ -5,7 +5,6 @@ require 'active_support'
|
|
5
5
|
require 'active_support/core_ext'
|
6
6
|
require 'dry-types'
|
7
7
|
require 'dry-struct'
|
8
|
-
require 'draper'
|
9
8
|
require 'reform'
|
10
9
|
require 'rdf'
|
11
10
|
require 'valkyrie/rdf_patches'
|
@@ -87,19 +86,6 @@ module Valkyrie
|
|
87
86
|
Valkyrie::StorageAdapter.find(super.to_sym)
|
88
87
|
end
|
89
88
|
|
90
|
-
# @api public
|
91
|
-
# Configure id_string_equality to be true in order to make Valkyrie::ID
|
92
|
-
# equal to the string value they contain. This will be the default behavior
|
93
|
-
# in v3.0.0.
|
94
|
-
#
|
95
|
-
# @return [Boolean] Whether `Valkyrie::ID` should be equal to their string counterpart.
|
96
|
-
def id_string_equality
|
97
|
-
super
|
98
|
-
end
|
99
|
-
|
100
|
-
# @!attribute [w] id_string_equality=
|
101
|
-
# The setter for #id_string_equality; see it's implementation
|
102
|
-
|
103
89
|
# @api public
|
104
90
|
#
|
105
91
|
# The returned anonymous method (e.g. responds to #call) has a signature of
|
@@ -119,19 +105,19 @@ module Valkyrie
|
|
119
105
|
|
120
106
|
private
|
121
107
|
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
108
|
+
def defaults
|
109
|
+
{
|
110
|
+
resource_class_resolver: method(:default_resource_class_resolver)
|
111
|
+
}
|
112
|
+
end
|
113
|
+
|
114
|
+
# String constantize is a "by convention" factory. This works, but assumes
|
115
|
+
# the ruby class once used to persist is the model used to now reify.
|
116
|
+
#
|
117
|
+
# @param [String] class_name
|
118
|
+
def default_resource_class_resolver(class_name)
|
119
|
+
class_name.constantize
|
120
|
+
end
|
135
121
|
end
|
136
122
|
|
137
123
|
module_function :config, :logger, :logger=, :config_root_path, :environment, :config_file, :config_hash
|