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.
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DatabasusRuby
4
+ # Base class for every error raised by this gem.
5
+ class Error < StandardError; end
6
+
7
+ # Raised when the client is missing something it needs before it can talk to
8
+ # the API at all (an agent id, a workspace id, ...).
9
+ class ConfigurationError < Error; end
10
+
11
+ # Raised when Databasus answers with a non-successful status code.
12
+ class APIError < Error
13
+ attr_reader :status, :body, :response
14
+
15
+ def initialize(message = nil, status: nil, body: nil, response: nil)
16
+ @status = status
17
+ @body = body
18
+ @response = response
19
+ super(message || "Databasus API returned #{status}")
20
+ end
21
+
22
+ # Maps a Faraday response onto the most specific error class available.
23
+ def self.from_response(response)
24
+ klass = for_status(response.status)
25
+ klass.new(message_from(response.body), status: response.status, body: response.body, response: response)
26
+ end
27
+
28
+ def self.for_status(status)
29
+ return ServerError if (500..599).cover?(status)
30
+
31
+ STATUS_ERRORS.fetch(status, APIError)
32
+ end
33
+
34
+ # Every handler in the Go backend answers errors as {"error": "..."}, but
35
+ # streaming endpoints can still fail with a plain-text body.
36
+ def self.message_from(body)
37
+ case body
38
+ when Hash then body["error"] || body["message"] || body[:error]
39
+ when String then body.empty? ? nil : body[0, 500]
40
+ end
41
+ end
42
+ private_class_method :message_from
43
+ end
44
+
45
+ class BadRequestError < APIError; end
46
+ class AuthenticationError < APIError; end
47
+ class ForbiddenError < APIError; end
48
+ class NotFoundError < APIError; end
49
+ class ConflictError < APIError; end
50
+ class RateLimitError < APIError; end
51
+ class ServerError < APIError; end
52
+
53
+ class APIError
54
+ STATUS_ERRORS = {
55
+ 400 => BadRequestError,
56
+ 401 => AuthenticationError,
57
+ 403 => ForbiddenError,
58
+ 404 => NotFoundError,
59
+ 409 => ConflictError,
60
+ 429 => RateLimitError
61
+ }.freeze
62
+ end
63
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DatabasusRuby
4
+ # A thin, duck-typed wrapper around a decoded JSON payload.
5
+ #
6
+ # database = client.databases.get(id)
7
+ # database.name # => "primary"
8
+ # database.workspace_id # => same as database.workspaceId
9
+ # database.postgresqlLogical.host
10
+ #
11
+ # Nested hashes (and arrays of hashes) are wrapped on access, so the whole
12
+ # tree reads the same way. The Databasus API omits empty fields, so reading a
13
+ # key that is not in the payload returns nil rather than raising.
14
+ class Object
15
+ def initialize(attributes = {})
16
+ @attributes = normalize(attributes)
17
+ end
18
+
19
+ def to_h
20
+ @attributes
21
+ end
22
+ alias to_hash to_h
23
+
24
+ def [](key)
25
+ fetch_attribute(key.to_s)
26
+ end
27
+
28
+ def key?(key)
29
+ return false if @attributes.nil?
30
+
31
+ @attributes.key?(key.to_s) || @attributes.key?(camelize(key.to_s))
32
+ end
33
+ alias has_key? key?
34
+
35
+ def keys
36
+ @attributes.nil? ? [] : @attributes.keys
37
+ end
38
+
39
+ def ==(other)
40
+ other.is_a?(Object) && to_h == other.to_h
41
+ end
42
+ alias eql? ==
43
+
44
+ def hash
45
+ to_h.hash
46
+ end
47
+
48
+ def inspect
49
+ "#<#{self.class.name} #{keys.join(", ")}>"
50
+ end
51
+
52
+ def method_missing(name, *args, &block)
53
+ # Endpoint classes inherit from Object without carrying a payload; for
54
+ # them an unknown method is a genuine mistake, not a missing field.
55
+ return super if @attributes.nil?
56
+ return super unless args.empty? && block.nil?
57
+
58
+ fetch_attribute(name.to_s)
59
+ end
60
+
61
+ def respond_to_missing?(name, include_private = false)
62
+ @attributes.nil? ? super : true
63
+ end
64
+
65
+ private
66
+
67
+ def normalize(attributes)
68
+ return nil if attributes.nil?
69
+ return attributes.to_h.transform_keys(&:to_s) if attributes.respond_to?(:to_h)
70
+
71
+ raise ArgumentError, "expected a Hash-like payload, got #{attributes.class}"
72
+ end
73
+
74
+ def fetch_attribute(key)
75
+ raw = if @attributes.key?(key)
76
+ @attributes[key]
77
+ else
78
+ @attributes[camelize(key)]
79
+ end
80
+ wrap(raw)
81
+ end
82
+
83
+ def wrap(value)
84
+ case value
85
+ when Hash then Object.new(value)
86
+ when ::Array then value.map { |item| wrap(item) }
87
+ else value
88
+ end
89
+ end
90
+
91
+ # workspace_id -> workspaceId, so Ruby callers can stay in snake_case.
92
+ def camelize(key)
93
+ head, *rest = key.split("_")
94
+ return key if rest.empty?
95
+
96
+ head + rest.map(&:capitalize).join
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DatabasusRuby
4
+ # Obtaining a token. Every other endpoint needs the JWT these methods
5
+ # return, so #signin and #signup install it on the client as `api_key`.
6
+ #
7
+ # client.auth.signin(email: "me@example.com", password: "secret")
8
+ # client.users.me.name
9
+ #
10
+ # Read the token off the result if you want to keep it for a later client.
11
+ class Auth < Object
12
+ include Endpoint
13
+
14
+ # POST /users/signin
15
+ def signin(email:, password:, cloudflare_turnstile_token: nil)
16
+ authorize object(http_post("users/signin", {
17
+ email: email,
18
+ password: password,
19
+ cloudflareTurnstileToken: cloudflare_turnstile_token
20
+ }))
21
+ end
22
+
23
+ # POST /users/signup
24
+ def signup(email:, name:, password:, cloudflare_turnstile_token: nil)
25
+ authorize object(http_post("users/signup", {
26
+ email: email,
27
+ name: name,
28
+ password: password,
29
+ cloudflareTurnstileToken: cloudflare_turnstile_token
30
+ }))
31
+ end
32
+
33
+ # POST /auth/github/callback
34
+ def github_callback(code:, redirect_uri:)
35
+ object(http_post("auth/github/callback", { code: code, redirectUri: redirect_uri }))
36
+ end
37
+
38
+ # POST /auth/google/callback
39
+ def google_callback(code:, redirect_uri:)
40
+ object(http_post("auth/google/callback", { code: code, redirectUri: redirect_uri }))
41
+ end
42
+
43
+ # POST /users/send-reset-password-code
44
+ def send_reset_password_code(email:, cloudflare_turnstile_token: nil)
45
+ object(http_post("users/send-reset-password-code", {
46
+ email: email,
47
+ cloudflareTurnstileToken: cloudflare_turnstile_token
48
+ }))
49
+ end
50
+
51
+ # POST /users/reset-password
52
+ def reset_password(email:, code:, new_password:)
53
+ object(http_post("users/reset-password", { email: email, code: code, newPassword: new_password }))
54
+ end
55
+
56
+ private
57
+
58
+ def authorize(result)
59
+ token = result.token
60
+ raise Error, "Databasus did not return a token" if token.nil? || token.empty?
61
+
62
+ client.api_key = token
63
+ result
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DatabasusRuby
4
+ # Logical backups.
5
+ #
6
+ # backups = client.backups.list(database_id: id, status: %w[COMPLETED FAILED])
7
+ # backups.total
8
+ # backups.first.fileName
9
+ #
10
+ # Downloads are a two-step flow: mint a short-lived token, then stream the
11
+ # file with it. #download does both.
12
+ class Backups < Object
13
+ include Endpoint
14
+
15
+ STATUSES = %w[IN_PROGRESS COMPLETED FAILED CANCELED].freeze
16
+ ENCRYPTIONS = %w[NONE ENCRYPTED].freeze
17
+ VERIFICATION_STATUSES = %w[NOT_VERIFIED VERIFIED_SUCCESSFUL VERIFICATION_FAILED].freeze
18
+
19
+ # GET /backups — `status` accepts a single value or an array.
20
+ def list(database_id:, limit: nil, offset: nil, status: nil, before_date: nil, pg_wal_backup_type: nil)
21
+ response = http_get("backups", {
22
+ database_id: database_id,
23
+ limit: limit,
24
+ offset: offset,
25
+ status: status,
26
+ beforeDate: before_date,
27
+ pgWalBackupType: pg_wal_backup_type
28
+ })
29
+ collection(response, key: "backups")
30
+ end
31
+ alias all list
32
+
33
+ # POST /backups — queues a backup run for the database.
34
+ def create(database_id:)
35
+ object(http_post("backups", { database_id: database_id }))
36
+ end
37
+ alias trigger create
38
+
39
+ # DELETE /backups/{id}
40
+ def delete(id)
41
+ acknowledged(http_delete("backups/#{escape(id)}"))
42
+ end
43
+
44
+ # POST /backups/{id}/cancel
45
+ def cancel(id)
46
+ acknowledged(http_post("backups/#{escape(id)}/cancel"))
47
+ end
48
+
49
+ # POST /backups/{id}/download-token -> { token, filename, backupId }
50
+ def download_token(id)
51
+ object(http_post("backups/#{escape(id)}/download-token"))
52
+ end
53
+
54
+ # GET /backups/{id}/file — returns the raw body; pass a token from
55
+ # #download_token.
56
+ def file(id, token:)
57
+ http_get("backups/#{escape(id)}/file", { token: token }).body
58
+ end
59
+
60
+ # Mints a token and fetches the file in one call. Returns
61
+ # [filename, contents].
62
+ def download(id)
63
+ grant = download_token(id)
64
+ [grant.filename, file(id, token: grant.token)]
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DatabasusRuby
4
+ # Databases under backup.
5
+ #
6
+ # client.databases.list(workspace_id: id)
7
+ # client.databases.create(name: "primary", type: "POSTGRES_LOGICAL",
8
+ # workspaceId: id, postgresqlLogical: { ... })
9
+ #
10
+ # The create/update bodies are the full databases.Database payload, whose
11
+ # engine-specific half lives under one of `postgresqlLogical`,
12
+ # `postgresqlPhysical`, `mysql`, `mariadb` or `mongodb`.
13
+ class Databases < Object
14
+ include Endpoint
15
+
16
+ TYPES = %w[POSTGRES_LOGICAL POSTGRES_PHYSICAL MYSQL MARIADB MONGODB].freeze
17
+ HEALTH_STATUSES = %w[AVAILABLE UNAVAILABLE].freeze
18
+
19
+ # GET /databases
20
+ def list(workspace_id: nil)
21
+ collection(http_get("databases", { workspace_id: workspace_id || client.workspace_id! }), key: "databases")
22
+ end
23
+ alias all list
24
+
25
+ # GET /databases/{id}
26
+ def get(id)
27
+ object(http_get("databases/#{escape(id)}"))
28
+ end
29
+ alias find get
30
+
31
+ # POST /databases/create
32
+ def create(**attributes)
33
+ object(http_post("databases/create", with_default_workspace(attributes)))
34
+ end
35
+
36
+ # POST /databases/update — the id travels in the body, not the path.
37
+ def update(**attributes)
38
+ object(http_post("databases/update", attributes))
39
+ end
40
+
41
+ # DELETE /databases/{id}
42
+ def delete(id)
43
+ acknowledged(http_delete("databases/#{escape(id)}"))
44
+ end
45
+
46
+ # POST /databases/{id}/copy
47
+ def copy(id)
48
+ object(http_post("databases/#{escape(id)}/copy"))
49
+ end
50
+
51
+ # POST /databases/{id}/test-connection — true when the database answers.
52
+ def test_connection(id)
53
+ acknowledged(http_post("databases/#{escape(id)}/test-connection"))
54
+ end
55
+
56
+ # POST /databases/test-connection-direct — tests a payload that has not
57
+ # been saved yet.
58
+ def test_connection_direct(**attributes)
59
+ acknowledged(http_post("databases/test-connection-direct", attributes))
60
+ end
61
+
62
+ # POST /databases/create-readonly-user -> { username, password }
63
+ def create_readonly_user(**attributes)
64
+ object(http_post("databases/create-readonly-user", attributes))
65
+ end
66
+
67
+ # POST /databases/create-replication-only-user (PostgreSQL physical only)
68
+ def create_replication_only_user(**attributes)
69
+ object(http_post("databases/create-replication-only-user", attributes))
70
+ end
71
+
72
+ # POST /databases/is-readonly -> { isReadOnly, privileges }
73
+ def readonly(**attributes)
74
+ object(http_post("databases/is-readonly", attributes))
75
+ end
76
+
77
+ def readonly?(**attributes)
78
+ readonly(**attributes).isReadOnly == true
79
+ end
80
+
81
+ # The backups of one database, so a caller holding a database does not have
82
+ # to reach back for the other endpoint. Both delegate to Backups, which
83
+ # owns the requests.
84
+ #
85
+ # client.databases.backups(database.id, status: "COMPLETED")
86
+ # client.databases.backup(database.id) # queue a run
87
+ #
88
+ # #backups takes the same filters as Backups#list.
89
+ def backups(id, **filters)
90
+ client.backups.list(database_id: require_id(id), **filters)
91
+ end
92
+
93
+ def backup(id)
94
+ client.backups.create(database_id: require_id(id))
95
+ end
96
+
97
+ # GET /databases/notifier/{id}/databases-count
98
+ def notifier_databases_count(notifier_id)
99
+ single_value(http_get("databases/notifier/#{escape(notifier_id)}/databases-count"))
100
+ end
101
+
102
+ # GET /databases/notifier/{id}/is-using
103
+ def notifier_using?(notifier_id)
104
+ single_value(http_get("databases/notifier/#{escape(notifier_id)}/is-using")) == true
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DatabasusRuby
4
+ # Notification channels.
5
+ #
6
+ # client.notifiers.save(name: "ops", notifierType: "SLACK",
7
+ # workspaceId: id, slackNotifier: { ... })
8
+ #
9
+ # POST /notifiers both creates and updates: include `id` in the payload to
10
+ # update an existing notifier.
11
+ class Notifiers < Object
12
+ include Endpoint
13
+
14
+ TYPES = %w[EMAIL TELEGRAM WEBHOOK SLACK DISCORD TEAMS].freeze
15
+ NOTIFICATION_TYPES = %w[
16
+ ALL BACKUP_SUCCESS BACKUP_FAILED HEALTHCHECK_SUCCESS HEALTHCHECK_FAILED
17
+ VERIFICATION_SUCCESS VERIFICATION_FAILED
18
+ ].freeze
19
+
20
+ # GET /notifiers
21
+ def list(workspace_id: nil)
22
+ collection(http_get("notifiers", { workspace_id: workspace_id || client.workspace_id! }), key: "notifiers")
23
+ end
24
+ alias all list
25
+
26
+ # GET /notifiers/{id}
27
+ def get(id)
28
+ object(http_get("notifiers/#{escape(id)}"))
29
+ end
30
+ alias find get
31
+
32
+ # POST /notifiers
33
+ def save(**attributes)
34
+ object(http_post("notifiers", with_default_workspace(attributes)))
35
+ end
36
+ alias create save
37
+ alias update save
38
+
39
+ # DELETE /notifiers/{id}
40
+ def delete(id)
41
+ acknowledged(http_delete("notifiers/#{escape(id)}"))
42
+ end
43
+
44
+ # POST /notifiers/{id}/test — sends a test notification.
45
+ def test(id)
46
+ acknowledged(http_post("notifiers/#{escape(id)}/test"))
47
+ end
48
+
49
+ # POST /notifiers/direct-test — tests a payload that has not been saved yet.
50
+ def direct_test(**attributes)
51
+ acknowledged(http_post("notifiers/direct-test", attributes))
52
+ end
53
+
54
+ # POST /notifiers/{id}/transfer
55
+ def transfer(id, target_workspace_id:)
56
+ acknowledged(http_post("notifiers/#{escape(id)}/transfer", { targetWorkspaceId: target_workspace_id }))
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DatabasusRuby
4
+ # Backup destinations.
5
+ #
6
+ # client.storages.save(name: "offsite", type: "S3",
7
+ # workspaceId: id, s3Storage: { ... })
8
+ #
9
+ # POST /storages both creates and updates: include `id` in the payload to
10
+ # update an existing storage.
11
+ class Storages < Object
12
+ include Endpoint
13
+
14
+ TYPES = %w[LOCAL S3 GOOGLE_DRIVE NAS AZURE_BLOB FTP SFTP RCLONE].freeze
15
+
16
+ # GET /storages
17
+ def list(workspace_id: nil)
18
+ collection(http_get("storages", { workspace_id: workspace_id || client.workspace_id! }), key: "storages")
19
+ end
20
+ alias all list
21
+
22
+ # GET /storages/{id}
23
+ def get(id)
24
+ object(http_get("storages/#{escape(id)}"))
25
+ end
26
+ alias find get
27
+
28
+ # POST /storages
29
+ def save(**attributes)
30
+ object(http_post("storages", with_default_workspace(attributes)))
31
+ end
32
+ alias create save
33
+ alias update save
34
+
35
+ # DELETE /storages/{id}
36
+ def delete(id)
37
+ acknowledged(http_delete("storages/#{escape(id)}"))
38
+ end
39
+
40
+ # POST /storages/{id}/test
41
+ def test(id)
42
+ acknowledged(http_post("storages/#{escape(id)}/test"))
43
+ end
44
+
45
+ # POST /storages/direct-test — tests a payload that has not been saved yet.
46
+ def direct_test(**attributes)
47
+ acknowledged(http_post("storages/direct-test", attributes))
48
+ end
49
+
50
+ # POST /storages/{id}/transfer
51
+ def transfer(id, target_workspace_id:)
52
+ acknowledged(http_post("storages/#{escape(id)}/transfer", { targetWorkspaceId: target_workspace_id }))
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DatabasusRuby
4
+ # The signed-in user, instance-wide user administration and instance
5
+ # settings.
6
+ #
7
+ # client.users.me.email
8
+ # client.users.list(query: "ops", limit: 25).each { |user| ... }
9
+ #
10
+ # The #list / #get / #activate / #deactivate / #change_role and settings
11
+ # methods are ADMIN-only and answer 403 for a MEMBER token.
12
+ class Users < Object
13
+ include Endpoint
14
+
15
+ ROLES = %w[ADMIN MEMBER].freeze
16
+
17
+ # GET /users/me
18
+ def me
19
+ object(http_get("users/me"))
20
+ end
21
+
22
+ # PUT /users/me
23
+ def update_me(name: nil, email: nil)
24
+ object(http_put("users/me", { name: name, email: email }))
25
+ end
26
+
27
+ # PUT /users/change-password
28
+ def change_password(new_password:)
29
+ object(http_put("users/change-password", { newPassword: new_password }))
30
+ end
31
+
32
+ # POST /users/invite
33
+ def invite(email:, intended_workspace_id: nil, intended_workspace_role: nil)
34
+ object(http_post("users/invite", {
35
+ email: email,
36
+ intendedWorkspaceId: intended_workspace_id,
37
+ intendedWorkspaceRole: intended_workspace_role
38
+ }))
39
+ end
40
+
41
+ # GET /users (ADMIN)
42
+ def list(limit: nil, offset: nil, before_date: nil, query: nil)
43
+ response = http_get("users", { limit: limit, offset: offset, beforeDate: before_date, query: query })
44
+ collection(response, key: "users")
45
+ end
46
+ alias all list
47
+
48
+ # GET /users/{id} (ADMIN)
49
+ def get(id)
50
+ object(http_get("users/#{escape(id)}"))
51
+ end
52
+ alias find get
53
+
54
+ # POST /users/{id}/activate (ADMIN)
55
+ def activate(id)
56
+ acknowledged(http_post("users/#{escape(id)}/activate"))
57
+ end
58
+
59
+ # POST /users/{id}/deactivate (ADMIN)
60
+ def deactivate(id)
61
+ acknowledged(http_post("users/#{escape(id)}/deactivate"))
62
+ end
63
+
64
+ # PUT /users/{id}/role (ADMIN) — role is one of ROLES.
65
+ def change_role(id, role:)
66
+ acknowledged(http_put("users/#{escape(id)}/role", { role: role }))
67
+ end
68
+
69
+ # GET /users/settings (ADMIN) — instance-wide registration settings.
70
+ def settings
71
+ object(http_get("users/settings"))
72
+ end
73
+
74
+ # PUT /users/settings (ADMIN)
75
+ def update_settings(**attributes)
76
+ object(http_put("users/settings", attributes))
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DatabasusRuby
4
+ # Workspaces, their members and their audit trail.
5
+ #
6
+ # workspace = client.workspaces.create(name: "production")
7
+ # client.workspaces.add_member(workspace.id, email: "ops@example.com",
8
+ # role: "WORKSPACE_ADMIN")
9
+ #
10
+ # Note that the membership endpoints live under /workspaces/memberships/{id},
11
+ # where {id} is still the workspace id.
12
+ class Workspaces < Object
13
+ include Endpoint
14
+
15
+ MEMBER_ROLES = %w[WORKSPACE_OWNER WORKSPACE_ADMIN WORKSPACE_MEMBER WORKSPACE_VIEWER].freeze
16
+
17
+ # GET /workspaces — the workspaces the current user belongs to.
18
+ def list
19
+ collection(http_get("workspaces"), key: "workspaces")
20
+ end
21
+
22
+ alias all list
23
+
24
+ # POST /workspaces
25
+ def create(name:)
26
+ object(http_post("workspaces", { name: name }))
27
+ end
28
+
29
+ # GET /workspaces/{id}
30
+ def get(id)
31
+ object(http_get("workspaces/#{escape(id)}"))
32
+ end
33
+ alias find get
34
+
35
+ # PUT /workspaces/{id}
36
+ def update(id, **attributes)
37
+ object(http_put("workspaces/#{escape(id)}", attributes))
38
+ end
39
+
40
+ # DELETE /workspaces/{id}
41
+ def delete(id)
42
+ acknowledged(http_delete("workspaces/#{escape(id)}"))
43
+ end
44
+
45
+ # GET /workspaces/{id}/audit-logs
46
+ def audit_logs(id, limit: nil, offset: nil, before_date: nil)
47
+ response = http_get("workspaces/#{escape(id)}/audit-logs",
48
+ { limit: limit, offset: offset, beforeDate: before_date })
49
+ collection(response, key: "auditLogs")
50
+ end
51
+
52
+ # GET /workspaces/memberships/{id}/members
53
+ def members(id)
54
+ collection(http_get("workspaces/memberships/#{escape(id)}/members"), key: "members")
55
+ end
56
+
57
+ # POST /workspaces/memberships/{id}/members — adds an existing user or
58
+ # invites a new one; the response `status` says which happened
59
+ # (ADDED or INVITED). `role` is one of MEMBER_ROLES.
60
+ def add_member(id, email:, role:)
61
+ object(http_post("workspaces/memberships/#{escape(id)}/members", { email: email, role: role }))
62
+ end
63
+
64
+ # DELETE /workspaces/memberships/{id}/members/{userId}
65
+ def remove_member(id, user_id)
66
+ acknowledged(http_delete("workspaces/memberships/#{escape(id)}/members/#{escape(user_id)}"))
67
+ end
68
+
69
+ # PUT /workspaces/memberships/{id}/members/{userId}/role
70
+ def change_member_role(id, user_id, role:)
71
+ acknowledged(http_put("workspaces/memberships/#{escape(id)}/members/#{escape(user_id)}/role",
72
+ { role: role }))
73
+ end
74
+
75
+ # POST /workspaces/memberships/{id}/transfer-ownership
76
+ def transfer_ownership(id, new_owner_email:)
77
+ acknowledged(http_post("workspaces/memberships/#{escape(id)}/transfer-ownership",
78
+ { newOwnerEmail: new_owner_email }))
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DatabasusRuby
4
+ VERSION = "0.0.1"
5
+ end