databasus_ruby 0.0.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 +7 -0
- data/CHANGELOG.md +26 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +305 -0
- data/Rakefile +12 -0
- data/lib/databasus_ruby/client.rb +123 -0
- data/lib/databasus_ruby/collection.rb +83 -0
- data/lib/databasus_ruby/endpoint.rb +99 -0
- data/lib/databasus_ruby/error.rb +63 -0
- data/lib/databasus_ruby/object.rb +99 -0
- data/lib/databasus_ruby/objects/auth.rb +66 -0
- data/lib/databasus_ruby/objects/backups.rb +67 -0
- data/lib/databasus_ruby/objects/databases.rb +107 -0
- data/lib/databasus_ruby/objects/notifiers.rb +59 -0
- data/lib/databasus_ruby/objects/storages.rb +55 -0
- data/lib/databasus_ruby/objects/users.rb +79 -0
- data/lib/databasus_ruby/objects/workspaces.rb +81 -0
- data/lib/databasus_ruby/version.rb +5 -0
- data/lib/databasus_ruby.rb +38 -0
- data/sig/databasus_ruby.rbs +285 -0
- metadata +81 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 9223ce08d06f8a4f29e700d9046bab99ee5aaec01131af37a12620cdec27b7ed
|
|
4
|
+
data.tar.gz: 1cd2c4226183d6106dd8a351e61533c2bdf513f818bb0e43f4c3dd077fe548b0
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: f4c7dda68f74e63dc5833e41b2aff40c730f5462920bc8aaee57549a0b435516d187d07bd6e62591d7ef10b03f863d2492d5754da422021089bcaa4226d26fe2
|
|
7
|
+
data.tar.gz: 5c59d0c7e223f8904a54de677f5083c9cd94466e16e60d082b02ef42124784e52f28c09922fbe33542bf5e7b5f5c07c3b2e7a21a5d675e96e46cda29715ba7e7
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
## [Unreleased]
|
|
2
|
+
|
|
3
|
+
### Added
|
|
4
|
+
|
|
5
|
+
- `DatabasusRuby::Client` with bearer authentication, an optional default
|
|
6
|
+
`workspace_id`, an optional `agent_id`, and one memoized endpoint object per
|
|
7
|
+
API domain.
|
|
8
|
+
- Endpoint coverage for `auth`, `users` (profile, administration, instance
|
|
9
|
+
settings), `workspaces` (settings, membership, audit logs), `databases`,
|
|
10
|
+
logical `backups`, `storages` and `notifiers`.
|
|
11
|
+
- `DatabasusRuby::Collection`, a uniform `Enumerable` return value for every
|
|
12
|
+
list endpoint, normalizing the API's bare-array, paginated-envelope and
|
|
13
|
+
unpaginated-envelope shapes and exposing `total` / `limit` / `offset` /
|
|
14
|
+
`next_offset`.
|
|
15
|
+
- `DatabasusRuby::Object` payload wrapper: nested hashes and arrays are wrapped
|
|
16
|
+
on access, snake_case reads camelCase keys, and omitted fields read as `nil`.
|
|
17
|
+
- Error hierarchy under `DatabasusRuby::Error`: `APIError` and its per-status
|
|
18
|
+
subclasses carry the status and decoded body; `ConfigurationError` is raised
|
|
19
|
+
before a request when the client is missing a required value.
|
|
20
|
+
- `Databases#backups` and `Databases#backup`, listing and queueing the backups
|
|
21
|
+
of one database by delegating to the `backups` endpoint.
|
|
22
|
+
- RBS signatures for the full public API.
|
|
23
|
+
|
|
24
|
+
## [0.0.1] - 2026-08-04
|
|
25
|
+
|
|
26
|
+
- Initial release
|
data/CODE_OF_CONDUCT.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Code of Conduct
|
|
2
|
+
|
|
3
|
+
"databasus_ruby" follows [The Ruby Community Conduct Guideline](https://www.ruby-lang.org/en/conduct) in all "collaborative space", which is defined as community communications channels (such as mailing lists, submitted patches, commit comments, etc.):
|
|
4
|
+
|
|
5
|
+
* Participants will be tolerant of opposing views.
|
|
6
|
+
* Participants must ensure that their language and actions are free of personal attacks and disparaging personal remarks.
|
|
7
|
+
* When interpreting the words and actions of others, participants should always assume good intentions.
|
|
8
|
+
* Behaviour which can be reasonably considered harassment will not be tolerated.
|
|
9
|
+
|
|
10
|
+
If you have any concerns about behaviour within this project, please contact us at ["samuelfelipecorrea@gmail.com"](mailto:"samuelfelipecorrea@gmail.com").
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 SamuelFCorrea
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
# DatabasusRuby
|
|
2
|
+
|
|
3
|
+
A Ruby client for the [Databasus](https://github.com/SamuelFCorrea/databasus_ruby) backup-management API.
|
|
4
|
+
|
|
5
|
+
Each API domain is an endpoint object hanging off the client, list endpoints all
|
|
6
|
+
return a `Collection`, and every response is a duck-typed `Object` you can read
|
|
7
|
+
with plain method calls.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
Install the gem and add to the application's Gemfile by executing:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
bundle add databasus_ruby
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
If bundler is not being used to manage dependencies, install the gem by executing:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
gem install databasus_ruby
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
### Authentication
|
|
26
|
+
|
|
27
|
+
Databasus authenticates with a bearer token. For everything except the
|
|
28
|
+
verification-agent endpoints that token is a user JWT, which you get by signing
|
|
29
|
+
in:
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
client = DatabasusRuby::Client.new(url: "https://databasus.example.com/api/v1/")
|
|
33
|
+
|
|
34
|
+
client.auth.signin(email: "you@example.com", password: ENV["DATABASUS_PASSWORD"])
|
|
35
|
+
client.users.me.email # => "you@example.com"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`signin` installs the returned token on the client. If you already have a
|
|
39
|
+
token, pass it as `api_key` instead:
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
client = DatabasusRuby::Client.new(
|
|
43
|
+
api_key: ENV["DATABASUS_API_KEY"],
|
|
44
|
+
url: ENV.fetch("DATABASUS_URL"),
|
|
45
|
+
workspace_id: ENV["DATABASUS_WORKSPACE_ID"] # optional default, see below
|
|
46
|
+
)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`#signup` registers and installs the token the same way; both raise
|
|
50
|
+
`DatabasusRuby::Error` if the response carries no token, and the result still
|
|
51
|
+
exposes it as `result.token` if you want to keep it. OAuth code exchange lives
|
|
52
|
+
at `client.auth.github_callback` and `client.auth.google_callback`.
|
|
53
|
+
|
|
54
|
+
### Workspaces
|
|
55
|
+
|
|
56
|
+
Almost everything in Databasus is scoped to a workspace.
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
client.workspaces.list.each { |workspace| puts workspace.name }
|
|
60
|
+
|
|
61
|
+
workspace = client.workspaces.create(name: "production")
|
|
62
|
+
client.workspaces.add_member(workspace.id, email: "ops@example.com", role: "WORKSPACE_ADMIN")
|
|
63
|
+
client.workspaces.members(workspace.id).map(&:email)
|
|
64
|
+
client.workspaces.audit_logs(workspace.id, limit: 50)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Because the workspace-scoped list endpoints all require a workspace id, you can
|
|
68
|
+
set one default on the client and omit it per call:
|
|
69
|
+
|
|
70
|
+
```ruby
|
|
71
|
+
client.workspace_id = workspace.id
|
|
72
|
+
client.databases.list # no argument needed
|
|
73
|
+
client.storages.list
|
|
74
|
+
client.notifiers.list
|
|
75
|
+
```
|
|
76
|
+
### Databases
|
|
77
|
+
|
|
78
|
+
```ruby
|
|
79
|
+
databases = client.databases.list(workspace_id: workspace.id)
|
|
80
|
+
databases.total
|
|
81
|
+
databases.first.name
|
|
82
|
+
|
|
83
|
+
database = client.databases.create(
|
|
84
|
+
name: "primary",
|
|
85
|
+
type: "POSTGRES_LOGICAL", # DatabasusRuby::Databases::TYPES
|
|
86
|
+
workspaceId: workspace.id,
|
|
87
|
+
postgresqlLogical: {
|
|
88
|
+
host: "10.0.0.1",
|
|
89
|
+
port: 5432,
|
|
90
|
+
username: "backup",
|
|
91
|
+
password: ENV["PG_PASSWORD"],
|
|
92
|
+
database: "app_production",
|
|
93
|
+
version: "17",
|
|
94
|
+
sslMode: "require"
|
|
95
|
+
}
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
database.name
|
|
99
|
+
database.postgresqlLogical.host
|
|
100
|
+
database.workspace_id # snake_case reads camelCase keys too
|
|
101
|
+
|
|
102
|
+
client.databases.test_connection(database.id) # => true, or raises
|
|
103
|
+
client.databases.update(id: database.id, name: "primary-eu")
|
|
104
|
+
client.databases.copy(database.id)
|
|
105
|
+
client.databases.delete(database.id)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
A database can reach its own backups, which delegates to the `backups` endpoint
|
|
109
|
+
with the same filters as `client.backups.list`:
|
|
110
|
+
|
|
111
|
+
```ruby
|
|
112
|
+
client.databases.backups(database.id, status: "COMPLETED", limit: 25)
|
|
113
|
+
client.databases.backup(database.id) # queue a run
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
`update` posts to `/databases/update` with the id in the body, matching the API.
|
|
117
|
+
There is also `test_connection_direct`, `create_readonly_user`,
|
|
118
|
+
`create_replication_only_user` and `readonly?` for a payload you have not saved
|
|
119
|
+
yet.
|
|
120
|
+
|
|
121
|
+
### Backups
|
|
122
|
+
|
|
123
|
+
```ruby
|
|
124
|
+
backups = client.backups.list(
|
|
125
|
+
database_id: database.id,
|
|
126
|
+
status: %w[COMPLETED FAILED], # DatabasusRuby::Backups::STATUSES
|
|
127
|
+
limit: 25
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
backups.total # => 137
|
|
131
|
+
backups.next_offset # => 25
|
|
132
|
+
backups.first.fileName
|
|
133
|
+
|
|
134
|
+
client.backups.create(database_id: database.id) # queue a run
|
|
135
|
+
client.backups.cancel(backup.id)
|
|
136
|
+
client.backups.delete(backup.id)
|
|
137
|
+
|
|
138
|
+
filename, contents = client.backups.download(backup.id)
|
|
139
|
+
File.binwrite(filename, contents)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
`#download` mints a short-lived token and streams the file in one step; use
|
|
143
|
+
`#download_token` and `#file` if you need the two halves separately.
|
|
144
|
+
|
|
145
|
+
### Storages and notifiers
|
|
146
|
+
|
|
147
|
+
Both use a single save endpoint for create and update — include `id` in the
|
|
148
|
+
payload to update.
|
|
149
|
+
|
|
150
|
+
```ruby
|
|
151
|
+
storage = client.storages.save(
|
|
152
|
+
name: "offsite",
|
|
153
|
+
type: "S3", # DatabasusRuby::Storages::TYPES
|
|
154
|
+
workspaceId: workspace.id,
|
|
155
|
+
s3Storage: { bucket: "backups", region: "eu-west-1", accessKeyId: "...", secretAccessKey: "..." }
|
|
156
|
+
)
|
|
157
|
+
client.storages.test(storage.id)
|
|
158
|
+
client.storages.transfer(storage.id, target_workspace_id: other.id)
|
|
159
|
+
|
|
160
|
+
notifier = client.notifiers.save(
|
|
161
|
+
name: "ops",
|
|
162
|
+
notifierType: "SLACK", # DatabasusRuby::Notifiers::TYPES
|
|
163
|
+
workspaceId: workspace.id,
|
|
164
|
+
slackNotifier: { token: "...", channelId: "C123" }
|
|
165
|
+
)
|
|
166
|
+
client.notifiers.test(notifier.id)
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
`direct_test` on either domain tests an unsaved payload.
|
|
170
|
+
|
|
171
|
+
### Users
|
|
172
|
+
|
|
173
|
+
`client.users` covers both the signed-in user and instance-wide administration
|
|
174
|
+
(the latter needs an `ADMIN` token):
|
|
175
|
+
|
|
176
|
+
```ruby
|
|
177
|
+
client.users.me.name
|
|
178
|
+
client.users.update_me(name: "New Name")
|
|
179
|
+
client.users.change_password(new_password: ENV["NEW_PASSWORD"])
|
|
180
|
+
client.users.invite(email: "new@example.com", intended_workspace_id: workspace.id,
|
|
181
|
+
intended_workspace_role: "WORKSPACE_MEMBER")
|
|
182
|
+
|
|
183
|
+
client.users.list(query: "ops", limit: 25) # ADMIN
|
|
184
|
+
client.users.deactivate(user_id) # ADMIN
|
|
185
|
+
client.users.change_role(user_id, role: "ADMIN")
|
|
186
|
+
client.users.settings # instance registration settings
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Collections
|
|
190
|
+
|
|
191
|
+
Every list endpoint returns a `DatabasusRuby::Collection`, regardless of which
|
|
192
|
+
of the API's three list shapes the endpoint uses:
|
|
193
|
+
|
|
194
|
+
```ruby
|
|
195
|
+
backups = client.backups.list(database_id: database.id, limit: 25)
|
|
196
|
+
|
|
197
|
+
backups.each { |backup| ... } # Enumerable
|
|
198
|
+
backups.map(&:status)
|
|
199
|
+
backups.size # items on this page
|
|
200
|
+
backups.total # items overall
|
|
201
|
+
backups.limit # nil when the endpoint does not paginate
|
|
202
|
+
backups.offset
|
|
203
|
+
backups.paginated?
|
|
204
|
+
backups.next_offset # nil on the last page
|
|
205
|
+
backups.last_page?
|
|
206
|
+
backups.data # the underlying Array
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Paging through everything:
|
|
210
|
+
|
|
211
|
+
```ruby
|
|
212
|
+
offset = 0
|
|
213
|
+
loop do
|
|
214
|
+
page = client.backups.list(database_id: database.id, limit: 100, offset: offset)
|
|
215
|
+
page.each { |backup| process(backup) }
|
|
216
|
+
break if page.last_page?
|
|
217
|
+
|
|
218
|
+
offset = page.next_offset
|
|
219
|
+
end
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Response objects
|
|
223
|
+
|
|
224
|
+
Responses are wrapped in `DatabasusRuby::Object`, which reads keys as methods,
|
|
225
|
+
wraps nested hashes and arrays as it goes, and accepts snake_case for the API's
|
|
226
|
+
camelCase keys:
|
|
227
|
+
|
|
228
|
+
```ruby
|
|
229
|
+
database.postgresqlLogical.sslMode
|
|
230
|
+
database.postgresql_logical.ssl_mode # the same thing
|
|
231
|
+
database.notifiers.map(&:name) # arrays of objects are wrapped too
|
|
232
|
+
database.to_h # the raw decoded payload
|
|
233
|
+
database["name"]
|
|
234
|
+
database.key?(:workspace_id)
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Databasus omits empty fields, so reading a key that isn't in the payload
|
|
238
|
+
returns `nil` rather than raising.
|
|
239
|
+
|
|
240
|
+
### Errors
|
|
241
|
+
|
|
242
|
+
Non-2xx responses raise a subclass of `DatabasusRuby::APIError` carrying the
|
|
243
|
+
status and decoded body:
|
|
244
|
+
|
|
245
|
+
| Status | Error |
|
|
246
|
+
| --- | --- |
|
|
247
|
+
| 400 | `DatabasusRuby::BadRequestError` |
|
|
248
|
+
| 401 | `DatabasusRuby::AuthenticationError` |
|
|
249
|
+
| 403 | `DatabasusRuby::ForbiddenError` |
|
|
250
|
+
| 404 | `DatabasusRuby::NotFoundError` |
|
|
251
|
+
| 409 | `DatabasusRuby::ConflictError` |
|
|
252
|
+
| 429 | `DatabasusRuby::RateLimitError` |
|
|
253
|
+
| 5xx | `DatabasusRuby::ServerError` |
|
|
254
|
+
|
|
255
|
+
```ruby
|
|
256
|
+
begin
|
|
257
|
+
client.databases.test_connection(database.id)
|
|
258
|
+
rescue DatabasusRuby::BadRequestError => e
|
|
259
|
+
e.message # => "dial tcp 10.0.0.1:5432: connect: connection refused"
|
|
260
|
+
e.status # => 400
|
|
261
|
+
e.body # => { "error" => "dial tcp ..." }
|
|
262
|
+
end
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
`DatabasusRuby::ConfigurationError` (a sibling of `APIError` under
|
|
266
|
+
`DatabasusRuby::Error`) is raised before any request goes out when the client is
|
|
267
|
+
missing something it needs, such as a workspace id.
|
|
268
|
+
|
|
269
|
+
## API coverage
|
|
270
|
+
|
|
271
|
+
Implemented: `auth`, `users` (including user management and instance settings),
|
|
272
|
+
`workspaces` (including membership and audit logs), `databases`, `backups`
|
|
273
|
+
(logical), `storages`, `notifiers`.
|
|
274
|
+
|
|
275
|
+
Not yet wrapped: physical backups and their configs, logical backup configs,
|
|
276
|
+
restores, verifications and verification agents/configs, healthchecks, disk
|
|
277
|
+
usage, global audit logs and the `system/*` endpoints. The client is ready for
|
|
278
|
+
them — each is a new class in `lib/databasus_ruby/objects/` plus an accessor on
|
|
279
|
+
`Client`.
|
|
280
|
+
|
|
281
|
+
## Development
|
|
282
|
+
|
|
283
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run
|
|
284
|
+
`rake spec` to run the tests, or `rake` to run the tests and RuboCop. You can
|
|
285
|
+
also run `bin/console` for an interactive prompt with a preconfigured `client`.
|
|
286
|
+
|
|
287
|
+
The specs stub HTTP with WebMock, so they need no running Databasus instance.
|
|
288
|
+
|
|
289
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To
|
|
290
|
+
release a new version, update the version number in `version.rb`, and then run
|
|
291
|
+
`bundle exec rake release`, which will create a git tag for the version, push
|
|
292
|
+
git commits and the created tag, and push the `.gem` file to
|
|
293
|
+
[rubygems.org](https://rubygems.org).
|
|
294
|
+
|
|
295
|
+
## Contributing
|
|
296
|
+
|
|
297
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/SamuelFCorrea/databasus_ruby. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/SamuelFCorrea/databasus_ruby/blob/main/CODE_OF_CONDUCT.md).
|
|
298
|
+
|
|
299
|
+
## License
|
|
300
|
+
|
|
301
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
302
|
+
|
|
303
|
+
## Code of Conduct
|
|
304
|
+
|
|
305
|
+
Everyone interacting in the DatabasusRuby project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/SamuelFCorrea/databasus_ruby/blob/main/CODE_OF_CONDUCT.md).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DatabasusRuby
|
|
4
|
+
# Entry point for the gem. Holds the connection and hands out one endpoint
|
|
5
|
+
# object per API domain.
|
|
6
|
+
#
|
|
7
|
+
# client = DatabasusRuby::Client.new(
|
|
8
|
+
# api_key: ENV["DATABASUS_API_KEY"],
|
|
9
|
+
# url: "https://databasus.example.com/api/v1/"
|
|
10
|
+
# )
|
|
11
|
+
# client.databases.list(workspace_id: id)
|
|
12
|
+
#
|
|
13
|
+
# `api_key` is sent as `Authorization: Bearer <api_key>`. Databasus accepts
|
|
14
|
+
# two kinds of bearer token there: a user JWT (from Auth#signin, used by
|
|
15
|
+
# every regular endpoint) and a verification-agent token (used by the
|
|
16
|
+
# /agent/* endpoints, which also need `agent_id`).
|
|
17
|
+
class Client
|
|
18
|
+
attr_reader :adapter, :url
|
|
19
|
+
attr_accessor :api_key, :workspace_id
|
|
20
|
+
|
|
21
|
+
def initialize(api_key: nil, url: nil, workspace_id: nil, adapter: Faraday.default_adapter)
|
|
22
|
+
raise ConfigurationError, "url is required" if url.nil? || url.to_s.empty?
|
|
23
|
+
|
|
24
|
+
@api_key = api_key
|
|
25
|
+
@adapter = adapter
|
|
26
|
+
@url = normalize_url(url)
|
|
27
|
+
@workspace_id = workspace_id
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def connection
|
|
31
|
+
@connection ||= Faraday.new do |conn|
|
|
32
|
+
conn.url_prefix = url
|
|
33
|
+
conn.request :json
|
|
34
|
+
conn.response :json, content_type: "application/json"
|
|
35
|
+
conn.adapter adapter
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def auth
|
|
40
|
+
@auth ||= Auth.new(self)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def backups
|
|
44
|
+
@backups ||= Backups.new(self)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def databases
|
|
48
|
+
@databases ||= Databases.new(self)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def notifiers
|
|
52
|
+
@notifiers ||= Notifiers.new(self)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def storages
|
|
56
|
+
@storages ||= Storages.new(self)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def users
|
|
60
|
+
@users ||= Users.new(self)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def workspaces
|
|
64
|
+
@workspaces ||= Workspaces.new(self)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# The workspace-scoped list endpoints all require a workspace id; callers
|
|
68
|
+
# can either pass one per call or set a default on the client.
|
|
69
|
+
def workspace_id!
|
|
70
|
+
return workspace_id unless workspace_id.nil? || workspace_id.to_s.empty?
|
|
71
|
+
|
|
72
|
+
raise ConfigurationError,
|
|
73
|
+
"workspace_id is required: pass workspace_id: to this call or to Client.new"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Performs the request and raises the matching APIError subclass on a
|
|
77
|
+
# non-successful status. Returns the raw Faraday::Response so callers can
|
|
78
|
+
# decide how to interpret the body.
|
|
79
|
+
def request(method, path, params: nil, body: nil, headers: nil)
|
|
80
|
+
response = connection.public_send(method, path) do |req|
|
|
81
|
+
req.params.update(stringify(params)) if params
|
|
82
|
+
req.body = body unless body.nil?
|
|
83
|
+
req.headers.update(request_headers(headers))
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
raise APIError.from_response(response) unless response.success?
|
|
87
|
+
|
|
88
|
+
response
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def inspect
|
|
92
|
+
"#<#{self.class.name} url=#{url.inspect} workspace_id=#{workspace_id.inspect}\
|
|
93
|
+
api_key=#{api_key.nil? ? "nil" : "[FILTERED]"}>"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
# Databasus takes the token as a bearer credential; both a user JWT and a
|
|
99
|
+
# verification-agent token go in the same header.
|
|
100
|
+
def request_headers(extra)
|
|
101
|
+
headers = {}
|
|
102
|
+
headers["Authorization"] = "Bearer #{api_key}" unless api_key.nil? || api_key.to_s.empty?
|
|
103
|
+
headers.update(extra) if extra
|
|
104
|
+
headers
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Faraday resolves a path without a leading slash against url_prefix, so
|
|
108
|
+
# the prefix has to keep its trailing slash or /api/v1 would be dropped.
|
|
109
|
+
def normalize_url(url)
|
|
110
|
+
url.to_s.end_with?("/") ? url.to_s : "#{url}/"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def stringify(params)
|
|
114
|
+
params.each_with_object({}) do |(key, value), acc|
|
|
115
|
+
next if value.nil?
|
|
116
|
+
|
|
117
|
+
# The only array-valued query parameter in the API (backups status)
|
|
118
|
+
# is declared as collectionFormat: csv.
|
|
119
|
+
acc[key.to_s] = value.is_a?(::Array) ? value.join(",") : value
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DatabasusRuby
|
|
4
|
+
# Uniform return value for every list endpoint.
|
|
5
|
+
#
|
|
6
|
+
# Databasus answers lists in three different shapes: a bare array
|
|
7
|
+
# (GET /databases), a paginated envelope (GET /backups) and an unpaginated
|
|
8
|
+
# envelope (GET /workspaces). Collection flattens all three into an
|
|
9
|
+
# Enumerable that always answers #total, and answers #limit / #offset when
|
|
10
|
+
# the endpoint actually paginates.
|
|
11
|
+
class Collection
|
|
12
|
+
include Enumerable
|
|
13
|
+
|
|
14
|
+
attr_reader :data, :total, :limit, :offset
|
|
15
|
+
|
|
16
|
+
def initialize(data: [], total: nil, limit: nil, offset: nil)
|
|
17
|
+
@data = data.map { |item| item.is_a?(Object) ? item : Object.new(item) }
|
|
18
|
+
@total = total || @data.size
|
|
19
|
+
@limit = limit
|
|
20
|
+
@offset = offset
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# `key` names the array inside an envelope response and is ignored for
|
|
24
|
+
# bare-array responses.
|
|
25
|
+
def self.from(body, key:)
|
|
26
|
+
case body
|
|
27
|
+
when ::Array
|
|
28
|
+
new(data: body)
|
|
29
|
+
when Hash
|
|
30
|
+
new(data: body[key] || [], total: body["total"], limit: body["limit"], offset: body["offset"])
|
|
31
|
+
when nil
|
|
32
|
+
new
|
|
33
|
+
else
|
|
34
|
+
raise Error, "unexpected list payload: #{body.class}"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def each(&block)
|
|
39
|
+
return data.each unless block
|
|
40
|
+
|
|
41
|
+
data.each(&block)
|
|
42
|
+
self
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def size
|
|
46
|
+
data.size
|
|
47
|
+
end
|
|
48
|
+
alias length size
|
|
49
|
+
|
|
50
|
+
def empty?
|
|
51
|
+
data.empty?
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def [](index)
|
|
55
|
+
data[index]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def last
|
|
59
|
+
data.last
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def paginated?
|
|
63
|
+
!limit.nil?
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Offset to pass to the next call, or nil when this is the last page (or
|
|
67
|
+
# the endpoint does not paginate at all).
|
|
68
|
+
def next_offset
|
|
69
|
+
return nil unless paginated?
|
|
70
|
+
|
|
71
|
+
following = (offset || 0) + data.size
|
|
72
|
+
following < total ? following : nil
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def last_page?
|
|
76
|
+
next_offset.nil?
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def inspect
|
|
80
|
+
"#<#{self.class.name} size=#{size} total=#{total} limit=#{limit.inspect} offset=#{offset.inspect}>"
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
module DatabasusRuby
|
|
6
|
+
# Shared plumbing for the per-domain endpoint classes. Each domain is a
|
|
7
|
+
# single class (DatabasusRuby::Databases, DatabasusRuby::Backups, ...) that
|
|
8
|
+
# inherits Object and includes this module, so it carries both the endpoint
|
|
9
|
+
# methods and the payload-reading behaviour.
|
|
10
|
+
#
|
|
11
|
+
# The transport helpers are prefixed `http_` so domain classes are free to
|
|
12
|
+
# expose the natural #get / #create / #update / #delete names.
|
|
13
|
+
module Endpoint
|
|
14
|
+
attr_reader :client
|
|
15
|
+
|
|
16
|
+
def initialize(client)
|
|
17
|
+
@client = client
|
|
18
|
+
# Endpoint objects are callers, not payloads; leaving @attributes nil
|
|
19
|
+
# makes Object#method_missing raise NoMethodError on a typo instead of
|
|
20
|
+
# quietly answering nil.
|
|
21
|
+
@attributes = nil
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def inspect
|
|
25
|
+
"#<#{self.class.name}>"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def http_get(path, params = nil)
|
|
31
|
+
client.request(:get, path, params: params)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def http_post(path, body = nil, params: nil)
|
|
35
|
+
client.request(:post, path, body: compact(body), params: params)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def http_put(path, body = nil, params: nil)
|
|
39
|
+
client.request(:put, path, body: compact(body), params: params)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def http_delete(path, params = nil)
|
|
43
|
+
client.request(:delete, path, params: params)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Wraps a single-object response.
|
|
47
|
+
def object(response)
|
|
48
|
+
Object.new(response.body.is_a?(Hash) ? response.body : {})
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Wraps a list response; `key` names the array inside an envelope payload.
|
|
52
|
+
def collection(response, key:)
|
|
53
|
+
Collection.from(response.body, key: key)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# For endpoints that answer 200/204 with no meaningful body.
|
|
57
|
+
def acknowledged(_response)
|
|
58
|
+
true
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Endpoints answering a single-entry map whose key is not documented in the
|
|
62
|
+
# swagger schema, e.g. GET /databases/notifier/{id}/is-using.
|
|
63
|
+
def single_value(response)
|
|
64
|
+
body = response.body
|
|
65
|
+
body.is_a?(Hash) ? body.values.first : body
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Request bodies are built from keyword arguments, and the API distinguishes
|
|
69
|
+
# an absent key from a null one in a few places, so nils are dropped.
|
|
70
|
+
def compact(body)
|
|
71
|
+
return body unless body.is_a?(Hash)
|
|
72
|
+
|
|
73
|
+
body.each_with_object({}) do |(key, value), acc|
|
|
74
|
+
acc[key.to_s] = value unless value.nil?
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Fills in the client's default workspace for create/save payloads that
|
|
79
|
+
# did not name one explicitly.
|
|
80
|
+
def with_default_workspace(attributes)
|
|
81
|
+
return attributes if attributes.key?(:workspaceId) || attributes.key?("workspaceId")
|
|
82
|
+
return attributes if client.workspace_id.nil?
|
|
83
|
+
|
|
84
|
+
attributes.merge(workspaceId: client.workspace_id)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def escape(segment)
|
|
88
|
+
URI.encode_uri_component(require_id(segment))
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Same presence check as #escape for ids that travel in a query string or
|
|
92
|
+
# body, where Faraday does the encoding.
|
|
93
|
+
def require_id(segment)
|
|
94
|
+
raise ArgumentError, "id is required" if segment.nil? || segment.to_s.empty?
|
|
95
|
+
|
|
96
|
+
segment.to_s
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|