valkyrie 1.0.0 → 1.1.0
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 +46 -0
- data/.docker-stack/valkyrie-development/docker-compose.yml +68 -0
- data/.docker-stack/valkyrie-test/docker-compose.yml +68 -0
- data/CHANGELOG.md +13 -0
- data/README.md +50 -11
- data/Rakefile +4 -2
- data/db/config.yml +18 -8
- data/lib/config/database_connection.rb +1 -1
- data/lib/valkyrie/persistence/fedora/permissive_schema.rb +16 -0
- data/lib/valkyrie/persistence/fedora/persister.rb +47 -5
- data/lib/valkyrie/persistence/fedora/persister/alternate_identifier.rb +10 -0
- data/lib/valkyrie/persistence/fedora/query_service.rb +19 -7
- data/lib/valkyrie/persistence/memory/query_service.rb +11 -0
- data/lib/valkyrie/persistence/postgres/query_service.rb +8 -2
- data/lib/valkyrie/persistence/solr/queries.rb +1 -0
- data/lib/valkyrie/persistence/solr/queries/find_by_alternate_identifier_query.rb +26 -0
- data/lib/valkyrie/persistence/solr/query_service.rb +7 -0
- data/lib/valkyrie/persistence/solr/repository.rb +1 -1
- data/lib/valkyrie/specs/shared_specs/queries.rb +40 -0
- data/lib/valkyrie/version.rb +1 -1
- data/tasks/docker.rake +31 -0
- data/valkyrie.gemspec +2 -1
- metadata +26 -7
- data/circle.yml +0 -17
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fe10f5866474217fe3edb3acce93747447fe6de5c8ad582d0be617f0dd93c040
|
4
|
+
data.tar.gz: e2210174bf001c9115b14266e388cc2999d0a74c560d8872b554e5c04973eb52
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 72d2fedc30bf650bd3794ab34687a47b071ab41fe1c8b29ebaef582b80616eed7f86aa549bc723976f0655fe9d0e302714294ecfb1a1a2461fb2428f00f4cd7c
|
7
|
+
data.tar.gz: 76bc7d38aa82f6e6bcc57e6e1d8bb7f762cc0f62d2f571f8e96d6a0a0fb76efab8f73327c86e239dfd65dc43fbe2d1d0798695b0008a3240bef0f7d08edd6c2d
|
@@ -0,0 +1,46 @@
|
|
1
|
+
---
|
2
|
+
version: 2
|
3
|
+
jobs:
|
4
|
+
build:
|
5
|
+
machine: true
|
6
|
+
steps:
|
7
|
+
- checkout
|
8
|
+
- run:
|
9
|
+
name: Install Docker Compose
|
10
|
+
command: |
|
11
|
+
curl -L https://github.com/docker/compose/releases/download/1.19.0/docker-compose-`uname -s`-`uname -m` > ~/docker-compose
|
12
|
+
chmod +x ~/docker-compose
|
13
|
+
sudo mv ~/docker-compose /usr/local/bin/docker-compose
|
14
|
+
- restore_cache:
|
15
|
+
keys:
|
16
|
+
- bundle-{{ checksum "Gemfile" }}-{{ checksum "valkyrie.gemspec" }}
|
17
|
+
- bundle- # used if checksum fails
|
18
|
+
- run: sudo apt-get update && sudo apt-get install -y libpq-dev
|
19
|
+
- run:
|
20
|
+
name: Install dependencies
|
21
|
+
command: bundle check --path=vendor/bundle || bundle install --path=vendor/bundle --jobs 4 --retry 3
|
22
|
+
- save_cache:
|
23
|
+
key: bundle-{{ checksum "Gemfile" }}-{{ checksum "valkyrie.gemspec" }}
|
24
|
+
paths:
|
25
|
+
- "vendor/bundle"
|
26
|
+
- run:
|
27
|
+
name: Run Rubocop
|
28
|
+
command: bundle exec rake rubocop
|
29
|
+
- run:
|
30
|
+
name: Run Specs
|
31
|
+
command: bundle exec rake docker:spec
|
32
|
+
workflows:
|
33
|
+
version: 2
|
34
|
+
build:
|
35
|
+
jobs:
|
36
|
+
- build
|
37
|
+
nightly:
|
38
|
+
triggers:
|
39
|
+
- schedule:
|
40
|
+
cron: "0 0 * * *"
|
41
|
+
filters:
|
42
|
+
branches:
|
43
|
+
only:
|
44
|
+
- master
|
45
|
+
jobs:
|
46
|
+
- build
|
@@ -0,0 +1,68 @@
|
|
1
|
+
---
|
2
|
+
version: '3.4'
|
3
|
+
volumes:
|
4
|
+
fedora:
|
5
|
+
db:
|
6
|
+
solr_repo:
|
7
|
+
solr_index:
|
8
|
+
services:
|
9
|
+
fedora:
|
10
|
+
image: nulib/fcrepo4
|
11
|
+
volumes:
|
12
|
+
- fedora:/data
|
13
|
+
ports:
|
14
|
+
- 8986:8080
|
15
|
+
db:
|
16
|
+
image: healthcheck/postgres:alpine
|
17
|
+
volumes:
|
18
|
+
- db:/data
|
19
|
+
environment:
|
20
|
+
- PGDATA=/data
|
21
|
+
- POSTGRES_USER=docker
|
22
|
+
- POSTGRES_PASSWORD=d0ck3r
|
23
|
+
ports:
|
24
|
+
- 5433:5432
|
25
|
+
solr_repo:
|
26
|
+
image: solr:7.2-alpine
|
27
|
+
ports:
|
28
|
+
- 8983:8983
|
29
|
+
volumes:
|
30
|
+
- solr_repo:/opt/solr/server/solr/mycores
|
31
|
+
- "../../solr:/solr_config"
|
32
|
+
entrypoint:
|
33
|
+
- docker-entrypoint.sh
|
34
|
+
- solr-precreate
|
35
|
+
- blacklight-core
|
36
|
+
- "/solr_config/config"
|
37
|
+
healthcheck:
|
38
|
+
test:
|
39
|
+
- CMD
|
40
|
+
- wget
|
41
|
+
- "-O"
|
42
|
+
- "/dev/null"
|
43
|
+
- http://localhost:8983/solr/
|
44
|
+
interval: 30s
|
45
|
+
timeout: 5s
|
46
|
+
retries: 3
|
47
|
+
solr_index:
|
48
|
+
image: solr:7.2-alpine
|
49
|
+
ports:
|
50
|
+
- 8987:8983
|
51
|
+
volumes:
|
52
|
+
- solr_index:/opt/solr/server/solr/mycores
|
53
|
+
- "../../solr:/solr_config"
|
54
|
+
entrypoint:
|
55
|
+
- docker-entrypoint.sh
|
56
|
+
- solr-precreate
|
57
|
+
- hydra-dev
|
58
|
+
- "/solr_config/config"
|
59
|
+
healthcheck:
|
60
|
+
test:
|
61
|
+
- CMD
|
62
|
+
- wget
|
63
|
+
- "-O"
|
64
|
+
- "/dev/null"
|
65
|
+
- http://localhost:8983/solr/
|
66
|
+
interval: 30s
|
67
|
+
timeout: 5s
|
68
|
+
retries: 3
|
@@ -0,0 +1,68 @@
|
|
1
|
+
---
|
2
|
+
version: '3.4'
|
3
|
+
volumes:
|
4
|
+
fedora:
|
5
|
+
db:
|
6
|
+
solr_repo:
|
7
|
+
solr_index:
|
8
|
+
services:
|
9
|
+
fedora:
|
10
|
+
image: nulib/fcrepo4
|
11
|
+
volumes:
|
12
|
+
- fedora:/data
|
13
|
+
ports:
|
14
|
+
- 8988:8080
|
15
|
+
db:
|
16
|
+
image: healthcheck/postgres:alpine
|
17
|
+
volumes:
|
18
|
+
- db:/data
|
19
|
+
environment:
|
20
|
+
- PGDATA=/data
|
21
|
+
- POSTGRES_USER=docker
|
22
|
+
- POSTGRES_PASSWORD=d0ck3r
|
23
|
+
ports:
|
24
|
+
- 5434:5432
|
25
|
+
solr_repo:
|
26
|
+
image: solr:7.2-alpine
|
27
|
+
ports:
|
28
|
+
- 8984:8983
|
29
|
+
volumes:
|
30
|
+
- solr_repo:/opt/solr/server/solr/mycores
|
31
|
+
- "../../solr:/solr_config"
|
32
|
+
entrypoint:
|
33
|
+
- docker-entrypoint.sh
|
34
|
+
- solr-precreate
|
35
|
+
- blacklight-core-test
|
36
|
+
- "/solr_config/config"
|
37
|
+
healthcheck:
|
38
|
+
test:
|
39
|
+
- CMD
|
40
|
+
- wget
|
41
|
+
- "-O"
|
42
|
+
- "/dev/null"
|
43
|
+
- http://localhost:8983/solr/
|
44
|
+
interval: 30s
|
45
|
+
timeout: 5s
|
46
|
+
retries: 3
|
47
|
+
solr_index:
|
48
|
+
image: solr:7.2-alpine
|
49
|
+
ports:
|
50
|
+
- 8985:8983
|
51
|
+
volumes:
|
52
|
+
- solr_index:/opt/solr/server/solr/mycores
|
53
|
+
- "../../solr:/solr_config"
|
54
|
+
entrypoint:
|
55
|
+
- docker-entrypoint.sh
|
56
|
+
- solr-precreate
|
57
|
+
- hydra-test
|
58
|
+
- "/solr_config/config"
|
59
|
+
healthcheck:
|
60
|
+
test:
|
61
|
+
- CMD
|
62
|
+
- wget
|
63
|
+
- "-O"
|
64
|
+
- "/dev/null"
|
65
|
+
- http://localhost:8983/solr/
|
66
|
+
interval: 30s
|
67
|
+
timeout: 5s
|
68
|
+
retries: 3
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,16 @@
|
|
1
|
+
# v1.1.0 2018-05-08
|
2
|
+
|
3
|
+
## Changes since last release
|
4
|
+
|
5
|
+
* Added `find_by_alternate_identifier` query.
|
6
|
+
[stkenny](https://github.com/stkenny)
|
7
|
+
* Added Docker environment for development.
|
8
|
+
[mbklein](https://github.com/mbklein)
|
9
|
+
* Fixed README documentation.
|
10
|
+
[revgum](https://github.com/revgum)
|
11
|
+
* Deprecated `Valkyrie::Persistence::Fedora::PermissiveSchema.references`
|
12
|
+
* Deprecated `Valkyrie::Persistence::Fedora::PermissiveSchema.alternate_ids`
|
13
|
+
|
1
14
|
# v1.0.0 2018-03-23
|
2
15
|
|
3
16
|
## Changes since last release
|
data/README.md
CHANGED
@@ -35,7 +35,7 @@ instance with a short name that can be used to refer to it in your application:
|
|
35
35
|
require 'valkyrie'
|
36
36
|
Rails.application.config.to_prepare do
|
37
37
|
Valkyrie::MetadataAdapter.register(
|
38
|
-
Valkyrie::Persistence::Postgres::MetadataAdapter,
|
38
|
+
Valkyrie::Persistence::Postgres::MetadataAdapter.new,
|
39
39
|
:postgres
|
40
40
|
)
|
41
41
|
|
@@ -83,20 +83,20 @@ A sample configuration file that configures your application to use different ad
|
|
83
83
|
|
84
84
|
```
|
85
85
|
development:
|
86
|
-
|
86
|
+
metadata_adapter: postgres
|
87
87
|
storage_adapter: disk
|
88
88
|
|
89
89
|
test:
|
90
|
-
|
90
|
+
metadata_adapter: memory
|
91
91
|
storage_adapter: memory
|
92
92
|
|
93
93
|
production:
|
94
|
-
|
94
|
+
metadata_adapter: postgres
|
95
95
|
storage_adapter: fedora
|
96
96
|
```
|
97
97
|
|
98
98
|
For each environment, you must set two values:
|
99
|
-
* `
|
99
|
+
* `metadata_adapter` is the store where Valkyrie will put the metadata
|
100
100
|
* `storage_adapter` is the store where Valkyrie will put the files
|
101
101
|
|
102
102
|
The values are the short names used in your initializer.
|
@@ -120,7 +120,7 @@ end
|
|
120
120
|
|
121
121
|
#### Work Types Generator
|
122
122
|
|
123
|
-
To create a custom Valkyrie model in your application, you can use the Rails generator. For example, to
|
123
|
+
To create a custom Valkyrie model in your application, you can use the Rails generator. For example, to
|
124
124
|
generate a model named `FooBar` with an unordered `title` field and an ordered `member_ids` field:
|
125
125
|
|
126
126
|
```
|
@@ -136,15 +136,18 @@ rails generate valkyrie:resource Foo/Bar title member_ids:array
|
|
136
136
|
### Read and Write Data
|
137
137
|
|
138
138
|
```
|
139
|
+
# initialize a metadata adapter
|
140
|
+
adapter = Valkyrie::MetadataAdapter.find(:postgres)
|
141
|
+
|
139
142
|
# create an object
|
140
143
|
object1 = MyModel.new title: 'My Cool Object', authors: ['Jones, Alice', 'Smith, Bob']
|
141
|
-
object1 =
|
144
|
+
object1 = adapter.persister.save(resource: object1)
|
142
145
|
|
143
146
|
# load an object from the database
|
144
|
-
object2 =
|
147
|
+
object2 = adapter.query_service.find_by(id: object1.id)
|
145
148
|
|
146
149
|
# load all objects
|
147
|
-
objects =
|
150
|
+
objects = adapter.query_service.find_all
|
148
151
|
|
149
152
|
# load all MyModel objects
|
150
153
|
Valkyrie.config.metadata_adapter.query_service.find_all_of_model(model: MyModel)
|
@@ -153,17 +156,53 @@ Valkyrie.config.metadata_adapter.query_service.find_all_of_model(model: MyModel)
|
|
153
156
|
|
154
157
|
## Installing a Development environment
|
155
158
|
|
156
|
-
###
|
159
|
+
### Without Docker
|
160
|
+
|
161
|
+
#### External Requirements
|
157
162
|
* PostgreSQL with the uuid-ossp extension.
|
158
163
|
* Note: Enabling uuid-ossp requires database superuser privileges.
|
159
164
|
* From `psql`: `alter user [username] with superuser;`
|
160
165
|
|
161
|
-
|
166
|
+
#### To run the test suite
|
162
167
|
1. Start Solr and Fedora servers for testing with `rake server:test`
|
163
168
|
1. Run `rake db:create` (First time only)
|
164
169
|
1. Run `rake db:migrate`
|
170
|
+
|
171
|
+
### With Docker
|
172
|
+
|
173
|
+
#### External Requirements
|
174
|
+
* [Docker](https://store.docker.com/search?offering=community&type=edition) version >= 17.09.0
|
175
|
+
*
|
176
|
+
### Dependency Setup (Mac OSX)
|
177
|
+
|
178
|
+
1. `brew install docker`
|
179
|
+
1. `brew install docker-machine`
|
180
|
+
1. `brew install docker-compose`
|
181
|
+
|
182
|
+
### Starting Docker (Mac OSX)
|
183
|
+
|
184
|
+
1. `docker-machine create default`
|
185
|
+
1. `docker-machine start default`
|
186
|
+
1. `eval "$(docker-machine env)"
|
187
|
+
|
188
|
+
#### Starting the development mode dependencies
|
189
|
+
1. Start Solr, Fedora, and PostgreSQL with `rake docker:dev:daemon` (or `rake docker:dev:up` in a separate shell to run them in the foreground)
|
190
|
+
1. Run `rake db:create db:migrate` to initialize the database
|
191
|
+
1. Develop!
|
192
|
+
1. Run `rake docker:dev:down` to stop the server stack
|
193
|
+
* Development servers maintain data between runs. To clean them out, run `rake docker:dev:clean`
|
194
|
+
|
195
|
+
#### To run the test suite with all dependencies in one go
|
196
|
+
1. `rake docker:spec`
|
197
|
+
|
198
|
+
#### To run the test suite manually
|
199
|
+
1. Start Solr, Fedora, and PostgreSQL with `rake docker:test:daemon` (or `rake docker:test:up` in a separate shell to run them in the foreground)
|
200
|
+
1. Run `rake db:create db:migrate` to initialize the database
|
165
201
|
1. Run the gem's RSpec test suite with `rspec spec` or `rake`
|
202
|
+
1. Run `rake docker:test:down` to stop the server stack
|
203
|
+
* The test stack cleans up after itself on exit.
|
166
204
|
|
205
|
+
The development and test stacks use fully contained virtual volumes and bind all services to different ports, so they can be running at the same time without issue.
|
167
206
|
|
168
207
|
## License
|
169
208
|
|
data/Rakefile
CHANGED
@@ -5,7 +5,9 @@ require 'yaml'
|
|
5
5
|
require 'config/database_connection'
|
6
6
|
require 'active_record'
|
7
7
|
require 'rubocop/rake_task'
|
8
|
-
load
|
8
|
+
load 'tasks/dev.rake'
|
9
|
+
load 'tasks/docker.rake'
|
10
|
+
|
9
11
|
RSpec::Core::RakeTask.new(:spec)
|
10
12
|
|
11
13
|
task default: :spec
|
@@ -25,7 +27,7 @@ namespace :db do
|
|
25
27
|
end
|
26
28
|
|
27
29
|
task configuration: :environment do
|
28
|
-
@config = YAML.safe_load(ERB.new(File.read("db/config.yml")).result)[DATABASE_ENV]
|
30
|
+
@config = YAML.safe_load(ERB.new(File.read("db/config.yml")).result, [], [], true)[DATABASE_ENV]
|
29
31
|
end
|
30
32
|
|
31
33
|
task configure_connection: :configuration do
|
data/db/config.yml
CHANGED
@@ -1,17 +1,27 @@
|
|
1
|
-
|
1
|
+
<% local = File.exist?('/tmp/.s.PGSQL.5432') && File.stat('/tmp/.s.PGSQL.5432').socket? %>
|
2
|
+
default: &default
|
2
3
|
adapter: postgresql
|
3
|
-
database: Valkyrie_gem_development
|
4
4
|
encoding: utf8
|
5
5
|
min_messages: warning
|
6
6
|
pool: <%= Integer(ENV.fetch("DB_POOL", 5)) %>
|
7
7
|
reaping_frequency: <%= Integer(ENV.fetch("DB_REAPING_FREQUENCY", 10)) %>
|
8
8
|
timeout: 5000
|
9
|
+
<% unless local %>
|
10
|
+
host: localhost
|
11
|
+
username: docker
|
12
|
+
password: d0ck3r
|
13
|
+
<% end %>
|
14
|
+
|
15
|
+
development:
|
16
|
+
<<: *default
|
17
|
+
database: Valkyrie_gem_development
|
18
|
+
<% unless local %>
|
19
|
+
port: 5433
|
20
|
+
<% end %>
|
9
21
|
|
10
22
|
test:
|
11
|
-
|
12
|
-
encoding: utf8
|
13
|
-
min_messages: warning
|
14
|
-
pool: <%= Integer(ENV.fetch("DB_POOL", 5)) %>
|
15
|
-
reaping_frequency: <%= Integer(ENV.fetch("DB_REAPING_FREQUENCY", 10)) %>
|
16
|
-
timeout: 5000
|
23
|
+
<<: *default
|
17
24
|
database: Valkyrie_gem_test
|
25
|
+
<% unless local %>
|
26
|
+
port: 5434
|
27
|
+
<% end %>
|
@@ -7,7 +7,7 @@ module DatabaseConnection
|
|
7
7
|
# Ref https://github.com/puma/puma#clustered-mode
|
8
8
|
ActiveSupport.on_load(:active_record) do
|
9
9
|
::ActiveRecord::Base.connection_pool.disconnect! if ::ActiveRecord::Base.connected?
|
10
|
-
::ActiveRecord::Base.configurations = YAML.safe_load(ERB.new(File.read("db/config.yml")).result) || {}
|
10
|
+
::ActiveRecord::Base.configurations = YAML.safe_load(ERB.new(File.read("db/config.yml")).result, [], [], true) || {}
|
11
11
|
config = ::ActiveRecord::Base.configurations[env.to_s]
|
12
12
|
::ActiveRecord::Base.establish_connection(config)
|
13
13
|
end
|
@@ -22,11 +22,27 @@ module Valkyrie::Persistence::Fedora
|
|
22
22
|
uri_for(:id)
|
23
23
|
end
|
24
24
|
|
25
|
+
# @deprecated Please use {.uri_for} instead
|
26
|
+
def self.alternate_ids
|
27
|
+
warn "[DEPRECATION] `alternate_ids` is deprecated and will be removed in the next major release. " \
|
28
|
+
"It was never used internally - please use `uri_for(:alternate_ids)` " \
|
29
|
+
"Called from #{Gem.location_of_caller.join(':')}"
|
30
|
+
uri_for(:alternate_ids)
|
31
|
+
end
|
32
|
+
|
25
33
|
# @return [RDF::URI]
|
26
34
|
def self.member_ids
|
27
35
|
uri_for(:member_ids)
|
28
36
|
end
|
29
37
|
|
38
|
+
# @deprecated Please use {.uri_for} instead
|
39
|
+
def self.references
|
40
|
+
warn "[DEPRECATION] `references` is deprecated and will be removed in the next major release. " \
|
41
|
+
"It was never used internally - please use `uri_for(:references)` " \
|
42
|
+
"Called from #{Gem.location_of_caller.join(':')}"
|
43
|
+
uri_for(:references)
|
44
|
+
end
|
45
|
+
|
30
46
|
# @return [RDF::URI]
|
31
47
|
def self.valkyrie_bool
|
32
48
|
uri_for(:valkyrie_bool)
|
@@ -3,6 +3,7 @@ module Valkyrie::Persistence::Fedora
|
|
3
3
|
# Persister for Fedora MetadataAdapter.
|
4
4
|
class Persister
|
5
5
|
require 'valkyrie/persistence/fedora/persister/resource_factory'
|
6
|
+
require 'valkyrie/persistence/fedora/persister/alternate_identifier'
|
6
7
|
attr_reader :adapter
|
7
8
|
delegate :connection, :base_path, :resource_factory, to: :adapter
|
8
9
|
def initialize(adapter:)
|
@@ -16,14 +17,17 @@ module Valkyrie::Persistence::Fedora
|
|
16
17
|
resource.updated_at ||= Time.current
|
17
18
|
ensure_multiple_values!(resource)
|
18
19
|
orm = resource_factory.from_resource(resource: resource)
|
20
|
+
alternate_resources = find_or_create_alternate_ids(resource)
|
21
|
+
|
19
22
|
if !orm.new? || resource.id
|
20
|
-
|
21
|
-
|
22
|
-
end
|
23
|
+
cleanup_alternate_resources(resource) if alternate_resources
|
24
|
+
orm.update { |req| req.headers["Prefer"] = "handling=lenient; received=\"minimal\"" }
|
23
25
|
else
|
24
26
|
orm.create
|
25
27
|
end
|
26
|
-
resource_factory.to_resource(object: orm)
|
28
|
+
persisted_resource = resource_factory.to_resource(object: orm)
|
29
|
+
|
30
|
+
alternate_resources ? save_reference_to_resource(persisted_resource, alternate_resources) : persisted_resource
|
27
31
|
end
|
28
32
|
|
29
33
|
# (see Valkyrie::Persistence::Memory::Persister#save_all)
|
@@ -35,8 +39,15 @@ module Valkyrie::Persistence::Fedora
|
|
35
39
|
|
36
40
|
# (see Valkyrie::Persistence::Memory::Persister#delete)
|
37
41
|
def delete(resource:)
|
42
|
+
if resource.try(:alternate_ids)
|
43
|
+
resource.alternate_ids.each do |alternate_identifier|
|
44
|
+
adapter.persister.delete(resource: adapter.query_service.find_by(id: alternate_identifier))
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
38
48
|
orm = resource_factory.from_resource(resource: resource)
|
39
49
|
orm.delete
|
50
|
+
|
40
51
|
resource
|
41
52
|
end
|
42
53
|
|
@@ -60,10 +71,41 @@ module Valkyrie::Persistence::Fedora
|
|
60
71
|
private
|
61
72
|
|
62
73
|
def ensure_multiple_values!(resource)
|
63
|
-
bad_keys = resource.attributes.except(:internal_resource, :created_at, :updated_at, :new_record, :id).select do |_k, v|
|
74
|
+
bad_keys = resource.attributes.except(:internal_resource, :created_at, :updated_at, :new_record, :id, :references).select do |_k, v|
|
64
75
|
!v.nil? && !v.is_a?(Array)
|
65
76
|
end
|
66
77
|
raise ::Valkyrie::Persistence::UnsupportedDatatype, "#{resource}: #{bad_keys.keys} have non-array values, which can not be persisted by Valkyrie. Cast to arrays." unless bad_keys.keys.empty?
|
67
78
|
end
|
79
|
+
|
80
|
+
def find_or_create_alternate_ids(resource)
|
81
|
+
return nil unless resource.try(:alternate_ids)
|
82
|
+
|
83
|
+
resource.alternate_ids.map do |alternate_identifier|
|
84
|
+
begin
|
85
|
+
adapter.query_service.find_by(id: alternate_identifier)
|
86
|
+
rescue ::Valkyrie::Persistence::ObjectNotFoundError
|
87
|
+
alternate_resource = ::Valkyrie::Persistence::Fedora::AlternateIdentifier.new(id: alternate_identifier)
|
88
|
+
adapter.persister.save(resource: alternate_resource)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def cleanup_alternate_resources(updated_resource)
|
94
|
+
persisted_resource = adapter.query_service.find_by(id: updated_resource.id)
|
95
|
+
removed_identifiers = persisted_resource.alternate_ids - updated_resource.alternate_ids
|
96
|
+
|
97
|
+
removed_identifiers.each do |removed_id|
|
98
|
+
adapter.persister.delete(resource: adapter.query_service.find_by(id: removed_id))
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def save_reference_to_resource(resource, alternate_resources)
|
103
|
+
alternate_resources.each do |alternate_resource|
|
104
|
+
alternate_resource.references = resource.id
|
105
|
+
adapter.persister.save(resource: alternate_resource)
|
106
|
+
end
|
107
|
+
|
108
|
+
resource
|
109
|
+
end
|
68
110
|
end
|
69
111
|
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'valkyrie/resource'
|
3
|
+
require 'valkyrie/types'
|
4
|
+
|
5
|
+
module Valkyrie::Persistence::Fedora
|
6
|
+
class AlternateIdentifier < ::Valkyrie::Resource
|
7
|
+
attribute :id, ::Valkyrie::Types::ID.optional
|
8
|
+
attribute :references, ::Valkyrie::Types::ID.optional
|
9
|
+
end
|
10
|
+
end
|
@@ -10,15 +10,19 @@ module Valkyrie::Persistence::Fedora
|
|
10
10
|
|
11
11
|
# (see Valkyrie::Persistence::Memory::QueryService#find_by)
|
12
12
|
def find_by(id:)
|
13
|
-
id = Valkyrie::ID.new(id.to_s) if id.is_a?(String)
|
14
13
|
validate_id(id)
|
15
14
|
uri = adapter.id_to_uri(id)
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
15
|
+
|
16
|
+
resource_from_uri(uri)
|
17
|
+
end
|
18
|
+
|
19
|
+
# (see Valkyrie::Persistence::Memory::QueryService#find_by_alternate_identifier)
|
20
|
+
def find_by_alternate_identifier(alternate_identifier:)
|
21
|
+
validate_id(alternate_identifier)
|
22
|
+
uri = adapter.id_to_uri(alternate_identifier)
|
23
|
+
alternate_id = resource_from_uri(uri).references
|
24
|
+
|
25
|
+
find_by(id: alternate_id)
|
22
26
|
end
|
23
27
|
|
24
28
|
# (see Valkyrie::Persistence::Memory::QueryService#find_many_by_ids)
|
@@ -113,9 +117,17 @@ module Valkyrie::Persistence::Fedora
|
|
113
117
|
private
|
114
118
|
|
115
119
|
def validate_id(id)
|
120
|
+
id = Valkyrie::ID.new(id.to_s) if id.is_a?(String)
|
116
121
|
raise ArgumentError, 'id must be a Valkyrie::ID' unless id.is_a? Valkyrie::ID
|
117
122
|
end
|
118
123
|
|
124
|
+
def resource_from_uri(uri)
|
125
|
+
resource = Ldp::Resource.for(connection, uri, connection.get(uri))
|
126
|
+
resource_factory.to_resource(object: resource)
|
127
|
+
rescue ::Ldp::Gone, ::Ldp::NotFound
|
128
|
+
raise ::Valkyrie::Persistence::ObjectNotFoundError
|
129
|
+
end
|
130
|
+
|
119
131
|
def ensure_persisted(resource)
|
120
132
|
raise ArgumentError, 'resource is not saved' unless resource.persisted?
|
121
133
|
end
|
@@ -24,6 +24,17 @@ module Valkyrie::Persistence::Memory
|
|
24
24
|
cache[id] || raise(::Valkyrie::Persistence::ObjectNotFoundError)
|
25
25
|
end
|
26
26
|
|
27
|
+
# @param alternate_identifier [Valkyrie::ID] The alternate identifier to query for.
|
28
|
+
# @raise [Valkyrie::Persistence::ObjectNotFoundError] Raised when the alternate identifier
|
29
|
+
# isn't in the persistence backend.
|
30
|
+
# @raise [ArgumentError] Raised when alternate identifier is not a String or a Valkyrie::ID
|
31
|
+
# @return [Valkyrie::Resource] The object being searched for.
|
32
|
+
def find_by_alternate_identifier(alternate_identifier:)
|
33
|
+
alternate_identifier = Valkyrie::ID.new(alternate_identifier.to_s) if alternate_identifier.is_a?(String)
|
34
|
+
validate_id(alternate_identifier)
|
35
|
+
cache.select { |_key, resource| resource['alternate_ids'].include?(alternate_identifier) }.values.first || raise(::Valkyrie::Persistence::ObjectNotFoundError)
|
36
|
+
end
|
37
|
+
|
27
38
|
# @param ids [Array<Valkyrie::ID, String>] The IDs to query for.
|
28
39
|
# @raise [ArgumentError] Raised when any ID is not a String or a Valkyrie::ID
|
29
40
|
# @return [Array<Valkyrie::Resource>] All requested objects that were found
|
@@ -37,6 +37,14 @@ module Valkyrie::Persistence::Postgres
|
|
37
37
|
raise Valkyrie::Persistence::ObjectNotFoundError
|
38
38
|
end
|
39
39
|
|
40
|
+
# (see Valkyrie::Persistence::Memory::QueryService#find_by_alternate_identifier)
|
41
|
+
def find_by_alternate_identifier(alternate_identifier:)
|
42
|
+
alternate_identifier = Valkyrie::ID.new(alternate_identifier.to_s) if alternate_identifier.is_a?(String)
|
43
|
+
validate_id(alternate_identifier)
|
44
|
+
internal_array = "{\"alternate_ids\": [{\"id\": \"#{alternate_identifier}\"}]}"
|
45
|
+
run_query(find_inverse_references_query, internal_array).first || raise(Valkyrie::Persistence::ObjectNotFoundError)
|
46
|
+
end
|
47
|
+
|
40
48
|
# (see Valkyrie::Persistence::Memory::QueryService#find_many_by_ids)
|
41
49
|
def find_many_by_ids(ids:)
|
42
50
|
ids.map! do |id|
|
@@ -48,8 +56,6 @@ module Valkyrie::Persistence::Postgres
|
|
48
56
|
orm_class.where(id: ids).map do |orm_resource|
|
49
57
|
resource_factory.to_resource(object: orm_resource)
|
50
58
|
end
|
51
|
-
rescue ActiveRecord::RecordNotFound
|
52
|
-
raise Valkyrie::Persistence::ObjectNotFoundError
|
53
59
|
end
|
54
60
|
|
55
61
|
# (see Valkyrie::Persistence::Memory::QueryService#find_members)
|
@@ -6,6 +6,7 @@ module Valkyrie::Persistence::Solr
|
|
6
6
|
require 'valkyrie/persistence/solr/queries/default_paginator'
|
7
7
|
require 'valkyrie/persistence/solr/queries/find_all_query'
|
8
8
|
require 'valkyrie/persistence/solr/queries/find_by_id_query'
|
9
|
+
require 'valkyrie/persistence/solr/queries/find_by_alternate_identifier_query'
|
9
10
|
require 'valkyrie/persistence/solr/queries/find_many_by_ids_query'
|
10
11
|
require 'valkyrie/persistence/solr/queries/find_inverse_references_query'
|
11
12
|
require 'valkyrie/persistence/solr/queries/find_members_query'
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Valkyrie::Persistence::Solr::Queries
|
3
|
+
# Responsible for returning a single resource identified by an ID.
|
4
|
+
class FindByAlternateIdentifierQuery
|
5
|
+
attr_reader :connection, :resource_factory
|
6
|
+
attr_writer :alternate_identifier
|
7
|
+
def initialize(alternate_identifier, connection:, resource_factory:)
|
8
|
+
@alternate_identifier = alternate_identifier
|
9
|
+
@connection = connection
|
10
|
+
@resource_factory = resource_factory
|
11
|
+
end
|
12
|
+
|
13
|
+
def run
|
14
|
+
raise ::Valkyrie::Persistence::ObjectNotFoundError unless resource
|
15
|
+
resource_factory.to_resource(object: resource)
|
16
|
+
end
|
17
|
+
|
18
|
+
def alternate_identifier
|
19
|
+
@alternate_identifier.to_s
|
20
|
+
end
|
21
|
+
|
22
|
+
def resource
|
23
|
+
connection.get("select", params: { q: "alternate_ids_ssim:\"id-#{alternate_identifier}\"", fl: "*", rows: 1 })["response"]["docs"].first
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -18,6 +18,13 @@ module Valkyrie::Persistence::Solr
|
|
18
18
|
Valkyrie::Persistence::Solr::Queries::FindByIdQuery.new(id, connection: connection, resource_factory: resource_factory).run
|
19
19
|
end
|
20
20
|
|
21
|
+
# (see Valkyrie::Persistence::Memory::QueryService#find_by_alternate_identifier)
|
22
|
+
def find_by_alternate_identifier(alternate_identifier:)
|
23
|
+
alternate_identifier = Valkyrie::ID.new(alternate_identifier.to_s) if alternate_identifier.is_a?(String)
|
24
|
+
validate_id(alternate_identifier)
|
25
|
+
Valkyrie::Persistence::Solr::Queries::FindByAlternateIdentifierQuery.new(alternate_identifier, connection: connection, resource_factory: resource_factory).run
|
26
|
+
end
|
27
|
+
|
21
28
|
# (see Valkyrie::Persistence::Memory::QueryService#find_many_by_ids)
|
22
29
|
def find_many_by_ids(ids:)
|
23
30
|
ids.map! do |id|
|
@@ -39,7 +39,7 @@ module Valkyrie::Persistence::Solr
|
|
39
39
|
end
|
40
40
|
|
41
41
|
def ensure_multiple_values!(resource)
|
42
|
-
bad_keys = resource.attributes.except(:internal_resource, :created_at, :updated_at, :new_record, :id).select do |_k, v|
|
42
|
+
bad_keys = resource.attributes.except(:internal_resource, :alternate_ids, :created_at, :updated_at, :new_record, :id).select do |_k, v|
|
43
43
|
!v.nil? && !v.is_a?(Array)
|
44
44
|
end
|
45
45
|
raise ::Valkyrie::Persistence::UnsupportedDatatype, "#{resource}: #{bad_keys.keys} have non-array values, which can not be persisted by Valkyrie. Cast to arrays." unless bad_keys.keys.empty?
|
@@ -5,6 +5,7 @@ RSpec.shared_examples 'a Valkyrie query provider' do
|
|
5
5
|
defined? adapter
|
6
6
|
class CustomResource < Valkyrie::Resource
|
7
7
|
attribute :id, Valkyrie::Types::ID.optional
|
8
|
+
attribute :alternate_ids, Valkyrie::Types::Array
|
8
9
|
attribute :title
|
9
10
|
attribute :member_ids, Valkyrie::Types::Array
|
10
11
|
attribute :a_member_of
|
@@ -25,6 +26,7 @@ RSpec.shared_examples 'a Valkyrie query provider' do
|
|
25
26
|
it { is_expected.to respond_to(:find_all).with(0).arguments }
|
26
27
|
it { is_expected.to respond_to(:find_all_of_model).with_keywords(:model) }
|
27
28
|
it { is_expected.to respond_to(:find_by).with_keywords(:id) }
|
29
|
+
it { is_expected.to respond_to(:find_by_alternate_identifier).with_keywords(:alternate_identifier) }
|
28
30
|
it { is_expected.to respond_to(:find_many_by_ids).with_keywords(:ids) }
|
29
31
|
it { is_expected.to respond_to(:find_members).with_keywords(:resource, :model) }
|
30
32
|
it { is_expected.to respond_to(:find_references_by).with_keywords(:resource, :property) }
|
@@ -74,6 +76,44 @@ RSpec.shared_examples 'a Valkyrie query provider' do
|
|
74
76
|
end
|
75
77
|
end
|
76
78
|
|
79
|
+
describe ".find_by_alternate_identifier" do
|
80
|
+
it "returns a resource by alternate identifier or string representation of an alternate identifier" do
|
81
|
+
resource = resource_class.new
|
82
|
+
resource.alternate_ids = [Valkyrie::ID.new('p9s0xfj')]
|
83
|
+
resource = persister.save(resource: resource)
|
84
|
+
|
85
|
+
found = query_service.find_by_alternate_identifier(alternate_identifier: resource.alternate_ids.first)
|
86
|
+
expect(found.id).to eq resource.id
|
87
|
+
expect(found).to be_persisted
|
88
|
+
|
89
|
+
found = query_service.find_by_alternate_identifier(alternate_identifier: resource.alternate_ids.first.to_s)
|
90
|
+
expect(found.id).to eq resource.id
|
91
|
+
expect(found).to be_persisted
|
92
|
+
end
|
93
|
+
|
94
|
+
it "returns a Valkyrie::Persistence::ObjectNotFoundError for a non-found alternate identifier" do
|
95
|
+
expect { query_service.find_by_alternate_identifier(alternate_identifier: Valkyrie::ID.new("123123123")) }.to raise_error ::Valkyrie::Persistence::ObjectNotFoundError
|
96
|
+
end
|
97
|
+
|
98
|
+
it 'raises an error if the alternate identifier is not a Valkyrie::ID or a string' do
|
99
|
+
expect { query_service.find_by_alternate_identifier(alternate_identifier: 123) }.to raise_error ArgumentError
|
100
|
+
end
|
101
|
+
|
102
|
+
it 'can have multiple alternate identifiers' do
|
103
|
+
resource = resource_class.new
|
104
|
+
resource.alternate_ids = [Valkyrie::ID.new('p9s0xfj'), Valkyrie::ID.new('jks0xfj')]
|
105
|
+
resource = persister.save(resource: resource)
|
106
|
+
|
107
|
+
found = query_service.find_by_alternate_identifier(alternate_identifier: resource.alternate_ids.first)
|
108
|
+
expect(found.id).to eq resource.id
|
109
|
+
expect(found).to be_persisted
|
110
|
+
|
111
|
+
found = query_service.find_by_alternate_identifier(alternate_identifier: resource.alternate_ids.last)
|
112
|
+
expect(found.id).to eq resource.id
|
113
|
+
expect(found).to be_persisted
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
77
117
|
describe ".find_many_by_ids" do
|
78
118
|
let!(:resource) { persister.save(resource: resource_class.new) }
|
79
119
|
let!(:resource2) { persister.save(resource: resource_class.new) }
|
data/lib/valkyrie/version.rb
CHANGED
data/tasks/docker.rake
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'valkyrie'
|
3
|
+
|
4
|
+
if Rails.env.development? || Rails.env.test?
|
5
|
+
begin
|
6
|
+
require 'docker/stack/rake_task'
|
7
|
+
|
8
|
+
def get_named_task(task_name)
|
9
|
+
Rake::Task[task_name]
|
10
|
+
rescue RuntimeError
|
11
|
+
nil
|
12
|
+
end
|
13
|
+
|
14
|
+
namespace :docker do
|
15
|
+
namespace(:dev) { Docker::Stack::RakeTask.load_tasks }
|
16
|
+
namespace(:test) { Docker::Stack::RakeTask.load_tasks(force_env: 'test', cleanup: true) }
|
17
|
+
|
18
|
+
desc 'Spin up test stack and run specs'
|
19
|
+
task :spec do
|
20
|
+
Rails.env = 'test'
|
21
|
+
Docker::Stack::Controller.new(project: 'valkyrie', cleanup: true).with_containers do
|
22
|
+
Rake::Task['db:create'].invoke
|
23
|
+
Rake::Task['db:migrate'].invoke
|
24
|
+
Rake::Task['spec'].invoke
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
rescue LoadError
|
29
|
+
Rails.logger.warn 'Docker rake tasks not loaded.'
|
30
|
+
end
|
31
|
+
end
|
data/valkyrie.gemspec
CHANGED
@@ -22,7 +22,7 @@ Gem::Specification.new do |spec|
|
|
22
22
|
spec.add_dependency 'dry-struct'
|
23
23
|
spec.add_dependency 'draper'
|
24
24
|
spec.add_dependency 'activemodel'
|
25
|
-
spec.add_dependency 'dry-types'
|
25
|
+
spec.add_dependency 'dry-types', '~> 0.12.0'
|
26
26
|
spec.add_dependency 'rdf'
|
27
27
|
spec.add_dependency 'active-fedora'
|
28
28
|
spec.add_dependency 'activesupport'
|
@@ -49,4 +49,5 @@ Gem::Specification.new do |spec|
|
|
49
49
|
spec.add_development_dependency 'yard'
|
50
50
|
spec.add_development_dependency 'solr_wrapper'
|
51
51
|
spec.add_development_dependency 'fcrepo_wrapper'
|
52
|
+
spec.add_development_dependency 'docker-stack', '~> 0.2.6'
|
52
53
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: valkyrie
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Trey Pendragon
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-
|
11
|
+
date: 2018-05-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: dry-struct
|
@@ -56,16 +56,16 @@ dependencies:
|
|
56
56
|
name: dry-types
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
58
58
|
requirements:
|
59
|
-
- - "
|
59
|
+
- - "~>"
|
60
60
|
- !ruby/object:Gem::Version
|
61
|
-
version:
|
61
|
+
version: 0.12.0
|
62
62
|
type: :runtime
|
63
63
|
prerelease: false
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
|
-
- - "
|
66
|
+
- - "~>"
|
67
67
|
- !ruby/object:Gem::Version
|
68
|
-
version:
|
68
|
+
version: 0.12.0
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
70
|
name: rdf
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
@@ -388,6 +388,20 @@ dependencies:
|
|
388
388
|
- - ">="
|
389
389
|
- !ruby/object:Gem::Version
|
390
390
|
version: '0'
|
391
|
+
- !ruby/object:Gem::Dependency
|
392
|
+
name: docker-stack
|
393
|
+
requirement: !ruby/object:Gem::Requirement
|
394
|
+
requirements:
|
395
|
+
- - "~>"
|
396
|
+
- !ruby/object:Gem::Version
|
397
|
+
version: 0.2.6
|
398
|
+
type: :development
|
399
|
+
prerelease: false
|
400
|
+
version_requirements: !ruby/object:Gem::Requirement
|
401
|
+
requirements:
|
402
|
+
- - "~>"
|
403
|
+
- !ruby/object:Gem::Version
|
404
|
+
version: 0.2.6
|
391
405
|
description:
|
392
406
|
email:
|
393
407
|
- tpendragon@princeton.edu
|
@@ -395,7 +409,10 @@ executables: []
|
|
395
409
|
extensions: []
|
396
410
|
extra_rdoc_files: []
|
397
411
|
files:
|
412
|
+
- ".circleci/config.yml"
|
398
413
|
- ".ctags"
|
414
|
+
- ".docker-stack/valkyrie-development/docker-compose.yml"
|
415
|
+
- ".docker-stack/valkyrie-test/docker-compose.yml"
|
399
416
|
- ".gitignore"
|
400
417
|
- ".rspec"
|
401
418
|
- ".rubocop.yml"
|
@@ -410,7 +427,6 @@ files:
|
|
410
427
|
- bin/rspec
|
411
428
|
- bin/setup
|
412
429
|
- browserslist
|
413
|
-
- circle.yml
|
414
430
|
- config/fedora.yml
|
415
431
|
- config/valkyrie.yml
|
416
432
|
- db/config.yml
|
@@ -446,6 +462,7 @@ files:
|
|
446
462
|
- lib/valkyrie/persistence/fedora/ordered_reader.rb
|
447
463
|
- lib/valkyrie/persistence/fedora/permissive_schema.rb
|
448
464
|
- lib/valkyrie/persistence/fedora/persister.rb
|
465
|
+
- lib/valkyrie/persistence/fedora/persister/alternate_identifier.rb
|
449
466
|
- lib/valkyrie/persistence/fedora/persister/model_converter.rb
|
450
467
|
- lib/valkyrie/persistence/fedora/persister/orm_converter.rb
|
451
468
|
- lib/valkyrie/persistence/fedora/persister/resource_factory.rb
|
@@ -472,6 +489,7 @@ files:
|
|
472
489
|
- lib/valkyrie/persistence/solr/queries.rb
|
473
490
|
- lib/valkyrie/persistence/solr/queries/default_paginator.rb
|
474
491
|
- lib/valkyrie/persistence/solr/queries/find_all_query.rb
|
492
|
+
- lib/valkyrie/persistence/solr/queries/find_by_alternate_identifier_query.rb
|
475
493
|
- lib/valkyrie/persistence/solr/queries/find_by_id_query.rb
|
476
494
|
- lib/valkyrie/persistence/solr/queries/find_inverse_references_query.rb
|
477
495
|
- lib/valkyrie/persistence/solr/queries/find_many_by_ids_query.rb
|
@@ -519,6 +537,7 @@ files:
|
|
519
537
|
- solr/config/xslt/luke.xsl
|
520
538
|
- solr/solr.xml
|
521
539
|
- tasks/dev.rake
|
540
|
+
- tasks/docker.rake
|
522
541
|
- valkyrie.gemspec
|
523
542
|
homepage:
|
524
543
|
licenses: []
|
data/circle.yml
DELETED
@@ -1,17 +0,0 @@
|
|
1
|
-
machine:
|
2
|
-
ruby:
|
3
|
-
version: 2.3.1
|
4
|
-
services:
|
5
|
-
- redis
|
6
|
-
general:
|
7
|
-
artifacts:
|
8
|
-
- "tmp/capybara"
|
9
|
-
dependencies:
|
10
|
-
post:
|
11
|
-
- bundle exec rake rubocop
|
12
|
-
- bundle exec rake server:test:
|
13
|
-
background: true
|
14
|
-
- bin/jetty_wait
|
15
|
-
notify:
|
16
|
-
webhooks:
|
17
|
-
- url: https://coveralls.io/webhook?repo_token=c3AnaQOFVYYTitAR1w6ySNScXSIfLQwN4
|