nohead 0.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 +7 -0
- data/CHANGELOG.md +21 -0
- data/LICENSE +21 -0
- data/README.md +298 -0
- data/lib/nohead/client.rb +265 -0
- data/lib/nohead/errors.rb +119 -0
- data/lib/nohead/nohead_object.rb +63 -0
- data/lib/nohead/operations.rb +70 -0
- data/lib/nohead/page.rb +52 -0
- data/lib/nohead/resources/assets.rb +71 -0
- data/lib/nohead/resources/other.rb +38 -0
- data/lib/nohead/resources/records.rb +137 -0
- data/lib/nohead/resources/schema.rb +148 -0
- data/lib/nohead/transport.rb +34 -0
- data/lib/nohead/uploads.rb +65 -0
- data/lib/nohead/version.rb +5 -0
- data/lib/nohead/webhooks.rb +120 -0
- data/lib/nohead.rb +25 -0
- metadata +64 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nohead
|
|
4
|
+
# The base of every error the SDK raises.
|
|
5
|
+
class Error < StandardError; end
|
|
6
|
+
|
|
7
|
+
# The API answered with an error status. Subclasses follow the error `type`.
|
|
8
|
+
class APIError < Error
|
|
9
|
+
# The HTTP status.
|
|
10
|
+
attr_reader :status
|
|
11
|
+
# The error `type`, e.g. "validation_error"; nil if the body had none.
|
|
12
|
+
attr_reader :type
|
|
13
|
+
# The request's ID ("req_..."), for support requests and logs.
|
|
14
|
+
attr_reader :request_id
|
|
15
|
+
# Field-level details: objects with `field`, `code` and `message`.
|
|
16
|
+
attr_reader :details
|
|
17
|
+
# The response headers (lowercase names).
|
|
18
|
+
attr_reader :headers
|
|
19
|
+
# The parsed response body, or its text when it was not JSON.
|
|
20
|
+
attr_reader :body
|
|
21
|
+
|
|
22
|
+
def initialize(status, body, headers)
|
|
23
|
+
error = body.is_a?(Hash) && body["error"].is_a?(Hash) ? body["error"] : {}
|
|
24
|
+
@status = status
|
|
25
|
+
@type = error["type"]
|
|
26
|
+
@request_id = error["request_id"] || headers["x-request-id"]
|
|
27
|
+
@details = Array(error["details"]).map { |detail| NoheadObject.wrap(detail) }
|
|
28
|
+
@headers = headers
|
|
29
|
+
@body = body
|
|
30
|
+
super(error["message"] || "Request failed with status #{status}")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def to_s
|
|
34
|
+
suffix = request_id ? " (request #{request_id})" : ""
|
|
35
|
+
"#{status} #{type || 'error'}: #{super}#{suffix}"
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
class InvalidRequestError < APIError; end
|
|
40
|
+
class AuthenticationError < APIError; end
|
|
41
|
+
class PlanLimitExceededError < APIError; end
|
|
42
|
+
class AuthorizationError < APIError; end
|
|
43
|
+
class NotFoundError < APIError; end
|
|
44
|
+
class ConflictError < APIError; end
|
|
45
|
+
|
|
46
|
+
# An `if_match` revision was not the current one: reload and try again.
|
|
47
|
+
class PreconditionFailedError < APIError
|
|
48
|
+
# The resource's current revision, when the API reports it.
|
|
49
|
+
def current_revision
|
|
50
|
+
details.find { |detail| detail["code"] == "revision_mismatch" }&.[]("current_revision")
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
class ValidationError < APIError; end
|
|
55
|
+
|
|
56
|
+
class RateLimitError < APIError
|
|
57
|
+
# Seconds to wait before retrying, from `Retry-After`.
|
|
58
|
+
def retry_after
|
|
59
|
+
Nohead.retry_after_seconds(headers)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
class InternalServerError < APIError; end
|
|
64
|
+
class ServiceUnavailableError < APIError; end
|
|
65
|
+
|
|
66
|
+
# The request never got a response: DNS, TLS, a reset connection.
|
|
67
|
+
class ConnectionError < Error; end
|
|
68
|
+
|
|
69
|
+
# An attempt took longer than the `timeout` option.
|
|
70
|
+
class TimeoutError < ConnectionError; end
|
|
71
|
+
|
|
72
|
+
# A presigned upload to storage failed (`assets.upload`).
|
|
73
|
+
class UploadError < Error
|
|
74
|
+
# Storage's HTTP status, when it answered.
|
|
75
|
+
attr_reader :status
|
|
76
|
+
|
|
77
|
+
def initialize(message, status = nil)
|
|
78
|
+
@status = status
|
|
79
|
+
super(message)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# A webhook request's signature or timestamp did not check out.
|
|
84
|
+
class WebhookVerificationError < Error; end
|
|
85
|
+
|
|
86
|
+
ERROR_CLASSES = {
|
|
87
|
+
"invalid_request" => InvalidRequestError,
|
|
88
|
+
"authentication_error" => AuthenticationError,
|
|
89
|
+
"plan_limit_exceeded" => PlanLimitExceededError,
|
|
90
|
+
"authorization_error" => AuthorizationError,
|
|
91
|
+
"not_found" => NotFoundError,
|
|
92
|
+
"conflict" => ConflictError,
|
|
93
|
+
"precondition_failed" => PreconditionFailedError,
|
|
94
|
+
"validation_error" => ValidationError,
|
|
95
|
+
"rate_limited" => RateLimitError,
|
|
96
|
+
"internal_error" => InternalServerError,
|
|
97
|
+
"service_unavailable" => ServiceUnavailableError
|
|
98
|
+
}.freeze
|
|
99
|
+
|
|
100
|
+
# The error for a response, by its `type`, or by status when it has none.
|
|
101
|
+
def self.api_error(status, body, headers)
|
|
102
|
+
type = body.is_a?(Hash) && body["error"].is_a?(Hash) ? body["error"]["type"] : nil
|
|
103
|
+
klass = ERROR_CLASSES[type] ||
|
|
104
|
+
if status == 503 then ServiceUnavailableError
|
|
105
|
+
elsif status >= 500 then InternalServerError
|
|
106
|
+
else APIError
|
|
107
|
+
end
|
|
108
|
+
klass.new(status, body, headers)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def self.retry_after_seconds(headers)
|
|
112
|
+
value = headers["retry-after"] or return nil
|
|
113
|
+
return [Float(value), 0].max if value.match?(/\A\d+(\.\d+)?\z/)
|
|
114
|
+
|
|
115
|
+
[Time.httpdate(value) - Time.now, 0].max
|
|
116
|
+
rescue ArgumentError
|
|
117
|
+
nil
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module Nohead
|
|
6
|
+
# What the SDK returns for the API's resources: read fields as methods
|
|
7
|
+
# (`record.data`) or with `[]` (`record["data"]`), nested objects included.
|
|
8
|
+
# Fields named like Ruby's own methods (`method`, `hash`...) need `[]`.
|
|
9
|
+
# Timestamps (`*_at`) are Times. Unknown fields are kept: the API adds
|
|
10
|
+
# fields without notice.
|
|
11
|
+
class NoheadObject
|
|
12
|
+
ISO_TIME = /\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})\z/
|
|
13
|
+
|
|
14
|
+
def self.wrap(value, key = nil)
|
|
15
|
+
case value
|
|
16
|
+
when Hash then new(value)
|
|
17
|
+
when Array then value.map { |item| wrap(item) }
|
|
18
|
+
when String
|
|
19
|
+
key.to_s.end_with?("_at") && value.match?(ISO_TIME) ? Time.iso8601(value) : value
|
|
20
|
+
else value
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def initialize(values)
|
|
25
|
+
@values = values.to_h { |key, value| [key.to_s, value] }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def [](key)
|
|
29
|
+
key = key.to_s
|
|
30
|
+
self.class.wrap(@values[key], key)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def key?(key) = @values.key?(key.to_s)
|
|
34
|
+
def keys = @values.keys
|
|
35
|
+
|
|
36
|
+
def dig(key, *rest)
|
|
37
|
+
value = self[key]
|
|
38
|
+
rest.empty? || value.nil? ? value : value.dig(*rest)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# The fields as a plain Hash with string keys (raw values).
|
|
42
|
+
def to_h = @values
|
|
43
|
+
|
|
44
|
+
def ==(other) = other.is_a?(NoheadObject) && other.to_h == to_h
|
|
45
|
+
|
|
46
|
+
def respond_to_missing?(name, include_private = false)
|
|
47
|
+
@values.key?(name.to_s) || super
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def method_missing(name, *args)
|
|
51
|
+
key = name.to_s
|
|
52
|
+
return super unless args.empty? && @values.key?(key)
|
|
53
|
+
|
|
54
|
+
self[key]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def inspect
|
|
58
|
+
label = @values["object"] ? "#{@values['object']} " : ""
|
|
59
|
+
"#<Nohead::NoheadObject #{label}#{@values.inspect}>"
|
|
60
|
+
end
|
|
61
|
+
alias to_s inspect
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Generated by scripts/generate.rb from openapi.json. Do not edit.
|
|
4
|
+
module Nohead
|
|
5
|
+
# Every operation an API key can call: [method, path].
|
|
6
|
+
OPERATIONS = {
|
|
7
|
+
"assets_complete" => ["POST", "/v1/assets/{asset_id}/complete"],
|
|
8
|
+
"assets_delete" => ["DELETE", "/v1/assets/{asset_id}"],
|
|
9
|
+
"assets_download_url" => ["GET", "/v1/assets/{asset_id}/download-url"],
|
|
10
|
+
"assets_get" => ["GET", "/v1/assets/{asset_id}"],
|
|
11
|
+
"assets_image_url" => ["GET", "/v1/assets/{asset_id}/image-url"],
|
|
12
|
+
"assets_list" => ["GET", "/v1/projects/{project_id}/assets"],
|
|
13
|
+
"assets_restore" => ["POST", "/v1/assets/{asset_id}/restore"],
|
|
14
|
+
"assets_upload" => ["POST", "/v1/projects/{project_id}/assets/uploads"],
|
|
15
|
+
"audit_events_list_for_project" => ["GET", "/v1/projects/{project_id}/audit-events"],
|
|
16
|
+
"collections_create" => ["POST", "/v1/projects/{project_id}/collections"],
|
|
17
|
+
"collections_delete" => ["DELETE", "/v1/collections/{collection_id}"],
|
|
18
|
+
"collections_get" => ["GET", "/v1/collections/{collection_id}"],
|
|
19
|
+
"collections_get_schema" => ["GET", "/v1/collections/{collection_id}/schema"],
|
|
20
|
+
"collections_list" => ["GET", "/v1/projects/{project_id}/collections"],
|
|
21
|
+
"collections_restore" => ["POST", "/v1/collections/{collection_id}/restore"],
|
|
22
|
+
"collections_search" => ["GET", "/v1/collections/{collection_id}/search"],
|
|
23
|
+
"collections_update" => ["PATCH", "/v1/collections/{collection_id}"],
|
|
24
|
+
"feature_flags_list" => ["GET", "/v1/feature-flags"],
|
|
25
|
+
"fields_create" => ["POST", "/v1/collections/{collection_id}/fields"],
|
|
26
|
+
"fields_delete" => ["DELETE", "/v1/fields/{field_id}"],
|
|
27
|
+
"fields_list" => ["GET", "/v1/collections/{collection_id}/fields"],
|
|
28
|
+
"fields_migrate" => ["POST", "/v1/fields/{field_id}/migrations"],
|
|
29
|
+
"fields_remove_alias" => ["DELETE", "/v1/fields/{field_id}/aliases/{alias}"],
|
|
30
|
+
"fields_reorder" => ["POST", "/v1/collections/{collection_id}/fields/reorder"],
|
|
31
|
+
"fields_restore" => ["POST", "/v1/fields/{field_id}/restore"],
|
|
32
|
+
"fields_update" => ["PATCH", "/v1/fields/{field_id}"],
|
|
33
|
+
"health_check" => ["GET", "/v1/health"],
|
|
34
|
+
"me_get" => ["GET", "/v1/me"],
|
|
35
|
+
"migrations_cancel" => ["POST", "/v1/migrations/{migration_id}/cancel"],
|
|
36
|
+
"migrations_get" => ["GET", "/v1/migrations/{migration_id}"],
|
|
37
|
+
"migrations_list" => ["GET", "/v1/collections/{collection_id}/migrations"],
|
|
38
|
+
"projects_search" => ["GET", "/v1/projects/{project_id}/search"],
|
|
39
|
+
"record_revisions_get" => ["GET", "/v1/records/{record_id}/revisions/{revision}"],
|
|
40
|
+
"record_revisions_list" => ["GET", "/v1/records/{record_id}/revisions"],
|
|
41
|
+
"record_revisions_revert" => ["POST", "/v1/records/{record_id}/revisions/{revision}/revert"],
|
|
42
|
+
"records_bulk" => ["POST", "/v1/collections/{collection_id}/records/bulk"],
|
|
43
|
+
"records_count" => ["GET", "/v1/collections/{collection_id}/records/count"],
|
|
44
|
+
"records_create" => ["POST", "/v1/collections/{collection_id}/records"],
|
|
45
|
+
"records_delete" => ["DELETE", "/v1/records/{record_id}"],
|
|
46
|
+
"records_diff" => ["GET", "/v1/records/{record_id}/diff"],
|
|
47
|
+
"records_get" => ["GET", "/v1/records/{record_id}"],
|
|
48
|
+
"records_list" => ["GET", "/v1/collections/{collection_id}/records"],
|
|
49
|
+
"records_publish" => ["POST", "/v1/records/{record_id}/publish"],
|
|
50
|
+
"records_restore" => ["POST", "/v1/records/{record_id}/restore"],
|
|
51
|
+
"records_schedule" => ["PUT", "/v1/records/{record_id}/schedule"],
|
|
52
|
+
"records_unpublish" => ["POST", "/v1/records/{record_id}/unpublish"],
|
|
53
|
+
"records_unschedule" => ["DELETE", "/v1/records/{record_id}/schedule"],
|
|
54
|
+
"records_update" => ["PATCH", "/v1/records/{record_id}"],
|
|
55
|
+
"schema_changes_get" => ["GET", "/v1/collections/{collection_id}/schema-changes/{schema_change_id}"],
|
|
56
|
+
"schema_changes_list" => ["GET", "/v1/collections/{collection_id}/schema-changes"],
|
|
57
|
+
"search_index_get" => ["GET", "/v1/collections/{collection_id}/search-index"],
|
|
58
|
+
"search_index_rebuild" => ["POST", "/v1/collections/{collection_id}/search-index/rebuild"],
|
|
59
|
+
"webhook_deliveries_get" => ["GET", "/v1/webhook-deliveries/{delivery_id}"],
|
|
60
|
+
"webhook_deliveries_list" => ["GET", "/v1/webhooks/{webhook_id}/deliveries"],
|
|
61
|
+
"webhook_deliveries_retry" => ["POST", "/v1/webhook-deliveries/{delivery_id}/retry"],
|
|
62
|
+
"webhooks_create" => ["POST", "/v1/projects/{project_id}/webhooks"],
|
|
63
|
+
"webhooks_delete" => ["DELETE", "/v1/webhooks/{webhook_id}"],
|
|
64
|
+
"webhooks_get" => ["GET", "/v1/webhooks/{webhook_id}"],
|
|
65
|
+
"webhooks_list" => ["GET", "/v1/projects/{project_id}/webhooks"],
|
|
66
|
+
"webhooks_rotate_secret" => ["POST", "/v1/webhooks/{webhook_id}/rotate-secret"],
|
|
67
|
+
"webhooks_test" => ["POST", "/v1/webhooks/{webhook_id}/test"],
|
|
68
|
+
"webhooks_update" => ["PATCH", "/v1/webhooks/{webhook_id}"]
|
|
69
|
+
}.freeze
|
|
70
|
+
end
|
data/lib/nohead/page.rb
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nohead
|
|
4
|
+
# One page of a list. `each` (and so `map`, `first`, `to_a`...) walks every
|
|
5
|
+
# item from here on, fetching later pages as needed:
|
|
6
|
+
#
|
|
7
|
+
# nohead.records.list("posts").each { |record| puts record.data["title"] }
|
|
8
|
+
#
|
|
9
|
+
# page = nohead.records.list("posts", limit: 100)
|
|
10
|
+
# page.data # this page's items
|
|
11
|
+
# page = page.next_page while page.next_page?
|
|
12
|
+
class Page
|
|
13
|
+
include Enumerable
|
|
14
|
+
|
|
15
|
+
# The items on this page.
|
|
16
|
+
attr_reader :data
|
|
17
|
+
# `next_cursor` and `has_more` (and `total_estimate` for search).
|
|
18
|
+
attr_reader :meta
|
|
19
|
+
|
|
20
|
+
def initialize(data, meta, &fetch)
|
|
21
|
+
@data = data
|
|
22
|
+
@meta = meta
|
|
23
|
+
@fetch = fetch
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def next_page?
|
|
27
|
+
meta["has_more"] == true && !meta["next_cursor"].nil?
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# The next page; raises Nohead::Error when there is none.
|
|
31
|
+
def next_page
|
|
32
|
+
raise Error, "There is no next page" unless next_page?
|
|
33
|
+
|
|
34
|
+
@fetch.call(meta["next_cursor"])
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def each(&block)
|
|
38
|
+
return enum_for(:each) unless block
|
|
39
|
+
|
|
40
|
+
page = self
|
|
41
|
+
loop do
|
|
42
|
+
page.data.each(&block)
|
|
43
|
+
break unless page.next_page?
|
|
44
|
+
|
|
45
|
+
page = page.next_page
|
|
46
|
+
end
|
|
47
|
+
self
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def inspect = "#<Nohead::Page #{data.size} items, more: #{next_page?}>"
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nohead
|
|
4
|
+
module Resources
|
|
5
|
+
# Files of the key's project. `asset` is an asset ID ("ast_...").
|
|
6
|
+
class Assets < Resource
|
|
7
|
+
# Uploads a file and returns the `ready` asset: creates the upload, sends
|
|
8
|
+
# the bytes straight to storage, then completes it (which checks the
|
|
9
|
+
# file). `file` is a path or an IO; for bytes, pass StringIO.new(bytes).
|
|
10
|
+
# Raises UploadError if storage refuses the bytes, and ValidationError if
|
|
11
|
+
# the file fails the checks.
|
|
12
|
+
def upload(file, filename: nil, content_type: nil, byte_size: nil, idempotency_key: nil)
|
|
13
|
+
source = UploadSource.new(file, filename: filename, content_type: content_type,
|
|
14
|
+
byte_size: byte_size)
|
|
15
|
+
begin
|
|
16
|
+
created = create_upload(filename: source.filename, content_type: source.content_type,
|
|
17
|
+
byte_size: source.byte_size, idempotency_key: idempotency_key)
|
|
18
|
+
upload = created.upload
|
|
19
|
+
response = @client.put_upload(upload.url, upload["method"], upload.headers.to_h, source)
|
|
20
|
+
ensure
|
|
21
|
+
source.close
|
|
22
|
+
end
|
|
23
|
+
unless response.status.between?(200, 299)
|
|
24
|
+
raise UploadError.new("Storage refused the upload of #{source.filename} (#{response.status})",
|
|
25
|
+
response.status)
|
|
26
|
+
end
|
|
27
|
+
complete(created.asset.id)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# The first step of an upload: a `pending` asset and a presigned URL to
|
|
31
|
+
# PUT the bytes to (for example from a browser). Then `complete` it.
|
|
32
|
+
def create_upload(idempotency_key: nil, **params)
|
|
33
|
+
@client.request("assets_upload", body: params, idempotency_key: idempotency_key)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Checks an uploaded file and marks the asset `ready`.
|
|
37
|
+
def complete(asset, idempotency_key: nil)
|
|
38
|
+
@client.request("assets_complete", path: { asset_id: asset }, idempotency_key: idempotency_key)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def list(deleted: nil, limit: nil, cursor: nil)
|
|
42
|
+
@client.paginate("assets_list", query: { deleted: deleted, limit: limit, cursor: cursor })
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def get(asset)
|
|
46
|
+
@client.request("assets_get", path: { asset_id: asset })
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Soft-deletes the asset; the file is purged after 30 days.
|
|
50
|
+
def delete(asset, idempotency_key: nil)
|
|
51
|
+
@client.request("assets_delete", path: { asset_id: asset }, idempotency_key: idempotency_key)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def restore(asset, idempotency_key: nil)
|
|
55
|
+
@client.request("assets_restore", path: { asset_id: asset }, idempotency_key: idempotency_key)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# A signed, cacheable URL of an image rendition (`url`).
|
|
59
|
+
def image_url(asset, width: nil, height: nil, fit: nil, format: nil, quality: nil)
|
|
60
|
+
@client.request("assets_image_url", path: { asset_id: asset }, query: {
|
|
61
|
+
width: width, height: height, fit: fit, format: format, quality: quality
|
|
62
|
+
})
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# A 15-minute link to download the original file (`url`).
|
|
66
|
+
def download_url(asset)
|
|
67
|
+
@client.request("assets_download_url", path: { asset_id: asset })
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nohead
|
|
4
|
+
module Resources
|
|
5
|
+
# The project's activity log, newest first. Needs the audit:read scope.
|
|
6
|
+
class AuditEvents < Resource
|
|
7
|
+
# `filter` takes action (exact, or a "record.*" prefix), resource_type,
|
|
8
|
+
# resource_id, actor_type, actor_id, since and until.
|
|
9
|
+
def list(filter: nil, sort: nil, limit: nil, cursor: nil)
|
|
10
|
+
@client.paginate("audit_events_list_for_project",
|
|
11
|
+
query: { filter: filter, sort: sort, limit: limit, cursor: cursor })
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Feature flags evaluated for the key's project.
|
|
16
|
+
class FeatureFlags < Resource
|
|
17
|
+
# Flags evaluated for the key's project.
|
|
18
|
+
def list
|
|
19
|
+
@client.request("feature_flags_list")
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# The credentials the client uses.
|
|
24
|
+
class Me < Resource
|
|
25
|
+
# The API key: its project, scopes, published_only and expiry.
|
|
26
|
+
def get
|
|
27
|
+
@client.request("me_get")
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Whether the API is up.
|
|
32
|
+
class Health < Resource
|
|
33
|
+
def check
|
|
34
|
+
@client.request("health_check")
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nohead
|
|
4
|
+
module Resources
|
|
5
|
+
# The base of the resources: each holds the client it sends requests with.
|
|
6
|
+
class Resource
|
|
7
|
+
def initialize(client)
|
|
8
|
+
@client = client
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def inspect = "#<#{self.class.name}>"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Records: the content of a collection. `collection` is a collection ID or
|
|
15
|
+
# slug; `record` a record ID ("rec_...").
|
|
16
|
+
class Records < Resource
|
|
17
|
+
attr_reader :revisions
|
|
18
|
+
|
|
19
|
+
def initialize(client)
|
|
20
|
+
super
|
|
21
|
+
@revisions = Revisions.new(client)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# A collection's records, newest first by default. `filter` holds
|
|
25
|
+
# equality filters ({ status: "published" }), `expand` relation or asset
|
|
26
|
+
# fields to embed under `expanded` (at most 5).
|
|
27
|
+
def list(collection, filter: nil, sort: nil, expand: nil, limit: nil, cursor: nil)
|
|
28
|
+
@client.paginate("records_list", path: { collection_id: collection }, query: {
|
|
29
|
+
filter: filter, sort: sort, expand: expand, limit: limit, cursor: cursor
|
|
30
|
+
})
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def get(record, expand: nil, include_deleted: nil)
|
|
34
|
+
@client.request("records_get", path: { record_id: record },
|
|
35
|
+
query: { expand: expand, include_deleted: include_deleted })
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Creates a draft: create("posts", data: { title: "Hello" }).
|
|
39
|
+
def create(collection, idempotency_key: nil, change_note: nil, **params)
|
|
40
|
+
@client.request("records_create", path: { collection_id: collection }, body: params,
|
|
41
|
+
idempotency_key: idempotency_key, change_note: change_note)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Sets the given fields (nil clears one); others keep their values.
|
|
45
|
+
def update(record, if_match: nil, idempotency_key: nil, change_note: nil, **params)
|
|
46
|
+
@client.request("records_update", path: { record_id: record }, body: params,
|
|
47
|
+
if_match: if_match, idempotency_key: idempotency_key,
|
|
48
|
+
change_note: change_note)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Soft-deletes the record: restore brings it back within 30 days.
|
|
52
|
+
def delete(record, if_match: nil, idempotency_key: nil, change_note: nil)
|
|
53
|
+
@client.request("records_delete", path: { record_id: record }, if_match: if_match,
|
|
54
|
+
idempotency_key: idempotency_key, change_note: change_note)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def restore(record, idempotency_key: nil, change_note: nil)
|
|
58
|
+
@client.request("records_restore", path: { record_id: record },
|
|
59
|
+
idempotency_key: idempotency_key, change_note: change_note)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def publish(record, if_match: nil, idempotency_key: nil, change_note: nil)
|
|
63
|
+
@client.request("records_publish", path: { record_id: record }, if_match: if_match,
|
|
64
|
+
idempotency_key: idempotency_key, change_note: change_note)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def unpublish(record, if_match: nil, idempotency_key: nil, change_note: nil)
|
|
68
|
+
@client.request("records_unpublish", path: { record_id: record }, if_match: if_match,
|
|
69
|
+
idempotency_key: idempotency_key, change_note: change_note)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Publishes and/or unpublishes the record later. Times left out keep
|
|
73
|
+
# their value; name them in `clear` to remove them.
|
|
74
|
+
def schedule(record, publish_at: nil, unpublish_at: nil, clear: [], idempotency_key: nil)
|
|
75
|
+
body = clear.to_h { |name| [name.to_s, nil] }
|
|
76
|
+
body["publish_at"] = publish_at unless publish_at.nil?
|
|
77
|
+
body["unpublish_at"] = unpublish_at unless unpublish_at.nil?
|
|
78
|
+
@client.request("records_schedule", path: { record_id: record }, body: body,
|
|
79
|
+
idempotency_key: idempotency_key)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Clears both scheduled times.
|
|
83
|
+
def unschedule(record, idempotency_key: nil)
|
|
84
|
+
@client.request("records_unschedule", path: { record_id: record },
|
|
85
|
+
idempotency_key: idempotency_key)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def count(collection, filter: nil)
|
|
89
|
+
@client.request("records_count", path: { collection_id: collection },
|
|
90
|
+
query: { filter: filter })
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Publishes, unpublishes, deletes, restores or updates up to 100 records.
|
|
94
|
+
# Each record succeeds or fails on its own; see `results`.
|
|
95
|
+
def bulk(collection, idempotency_key: nil, change_note: nil, **params)
|
|
96
|
+
@client.request("records_bulk", path: { collection_id: collection }, body: params,
|
|
97
|
+
idempotency_key: idempotency_key, change_note: change_note)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# The changes between two revisions.
|
|
101
|
+
def diff(record, from_revision, to_revision)
|
|
102
|
+
@client.request("records_diff", path: { record_id: record },
|
|
103
|
+
query: { from: from_revision, to: to_revision })
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Full-text search in one collection (search_enabled collections), most
|
|
107
|
+
# relevant first, through the first 1,000 hits.
|
|
108
|
+
def search(collection, query, filter: nil, sort: nil, expand: nil, limit: nil, cursor: nil)
|
|
109
|
+
@client.paginate("collections_search", path: { collection_id: collection }, query: {
|
|
110
|
+
q: query, filter: filter, sort: sort, expand: expand, limit: limit,
|
|
111
|
+
cursor: cursor
|
|
112
|
+
})
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# A record's version history. `revision` is a revision number.
|
|
117
|
+
class Revisions < Resource
|
|
118
|
+
# Newest first. `filter` takes operation, actor_id, since and until.
|
|
119
|
+
def list(record, filter: nil, limit: nil, cursor: nil)
|
|
120
|
+
@client.paginate("record_revisions_list", path: { record_id: record },
|
|
121
|
+
query: { filter: filter, limit: limit, cursor: cursor })
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def get(record, revision)
|
|
125
|
+
@client.request("record_revisions_get", path: { record_id: record, revision: revision })
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Restores the record's data as of `revision`, as a new revision. With
|
|
129
|
+
# dry_run: true, previews it.
|
|
130
|
+
def revert(record, revision, dry_run: false, if_match: nil, idempotency_key: nil)
|
|
131
|
+
@client.request("record_revisions_revert", path: { record_id: record, revision: revision },
|
|
132
|
+
query: { dry_run: dry_run || nil },
|
|
133
|
+
if_match: if_match, idempotency_key: idempotency_key)
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|