booqable 1.0.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/.rspec +2 -0
- data/.rubocop.yml +11 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/README.md +403 -0
- data/Rakefile +12 -0
- data/lib/booqable/auth.rb +114 -0
- data/lib/booqable/client.rb +81 -0
- data/lib/booqable/configurable.rb +143 -0
- data/lib/booqable/default.rb +215 -0
- data/lib/booqable/error.rb +428 -0
- data/lib/booqable/http.rb +383 -0
- data/lib/booqable/json_api_serializer.rb +266 -0
- data/lib/booqable/middleware/auth/api_key.rb +46 -0
- data/lib/booqable/middleware/auth/oauth.rb +88 -0
- data/lib/booqable/middleware/auth/single_use.rb +157 -0
- data/lib/booqable/middleware/base.rb +7 -0
- data/lib/booqable/middleware/raise_error.rb +29 -0
- data/lib/booqable/oauth_client.rb +72 -0
- data/lib/booqable/rate_limit.rb +51 -0
- data/lib/booqable/resource_proxy.rb +149 -0
- data/lib/booqable/resources.json +74 -0
- data/lib/booqable/resources.rb +68 -0
- data/lib/booqable/version.rb +5 -0
- data/lib/booqable.rb +85 -0
- data/sig/booqable.rbs +324 -0
- metadata +174 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
module Booqable
|
|
2
|
+
module Middleware
|
|
3
|
+
module Auth
|
|
4
|
+
# Faraday middleware for OAuth2 authentication
|
|
5
|
+
#
|
|
6
|
+
# This middleware handles OAuth2 token-based authentication for HTTP requests.
|
|
7
|
+
# It automatically manages access tokens, refreshing them when expired, and
|
|
8
|
+
# adds the Bearer token to the Authorization header.
|
|
9
|
+
#
|
|
10
|
+
# @example Adding to Faraday middleware stack
|
|
11
|
+
# builder.use Booqable::Middleware::Auth::OAuth,
|
|
12
|
+
# client_id: "your_client_id",
|
|
13
|
+
# client_secret: "your_client_secret",
|
|
14
|
+
# api_endpoint: "https://company.booqable.com/api/v4/oauth/token",
|
|
15
|
+
# read_token: -> { stored_token },
|
|
16
|
+
# write_token: ->(token) { store_token(token) }
|
|
17
|
+
class OAuth < Base
|
|
18
|
+
# Initialize the OAuth authentication middleware
|
|
19
|
+
#
|
|
20
|
+
# @param app [#call] The next middleware in the Faraday stack
|
|
21
|
+
# @param options [Hash] Configuration options
|
|
22
|
+
# @option options [String] :client_id OAuth client ID
|
|
23
|
+
# @option options [String] :client_secret OAuth client secret
|
|
24
|
+
# @option options [String] :api_endpoint API endpoint URL for the OAuth provider
|
|
25
|
+
# @option options [Proc] :read_token Proc to read stored token
|
|
26
|
+
# @option options [Proc] :write_token Proc to store new token
|
|
27
|
+
# @raise [KeyError] If required options are not provided
|
|
28
|
+
def initialize(app, options = {})
|
|
29
|
+
super(app)
|
|
30
|
+
|
|
31
|
+
@client_id = options.fetch(:client_id)
|
|
32
|
+
@client_secret = options.fetch(:client_secret)
|
|
33
|
+
@api_endpoint = options.fetch(:api_endpoint)
|
|
34
|
+
@read_token = options.fetch(:read_token)
|
|
35
|
+
@write_token = options.fetch(:write_token)
|
|
36
|
+
|
|
37
|
+
@client = OAuthClient.new(
|
|
38
|
+
client_id: @client_id,
|
|
39
|
+
client_secret: @client_secret,
|
|
40
|
+
api_endpoint: @api_endpoint,
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Process the HTTP request and add OAuth authentication
|
|
45
|
+
#
|
|
46
|
+
# Retrieves the stored access token, refreshes it if expired, and adds
|
|
47
|
+
# it to the Authorization header. Then passes the request to the next
|
|
48
|
+
# middleware in the stack.
|
|
49
|
+
#
|
|
50
|
+
# @param env [Faraday::Env] The request environment
|
|
51
|
+
# @return [Faraday::Response] The response from the next middleware
|
|
52
|
+
def call(env)
|
|
53
|
+
@token = @client.get_access_token_from_hash(@read_token.call)
|
|
54
|
+
|
|
55
|
+
if @token.expired? || @token.expires_at.nil?
|
|
56
|
+
@token = refresh_token!
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
env.request_headers["Authorization"] ||= "Bearer #{@token.token}"
|
|
60
|
+
|
|
61
|
+
@app.call(env)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
# Refresh the expired OAuth token
|
|
67
|
+
#
|
|
68
|
+
# Uses the refresh token to obtain a new access token and stores it
|
|
69
|
+
# using the configured write_token proc. Converts OAuth2 errors to
|
|
70
|
+
# Booqable errors for consistent error handling.
|
|
71
|
+
#
|
|
72
|
+
# @return [OAuth2::AccessToken] The new access token
|
|
73
|
+
# @raise [Booqable::Error] For OAuth-related errors
|
|
74
|
+
def refresh_token!
|
|
75
|
+
new_token = @token.refresh!
|
|
76
|
+
|
|
77
|
+
@write_token.call(new_token.to_hash)
|
|
78
|
+
|
|
79
|
+
new_token
|
|
80
|
+
rescue OAuth2::Error => e
|
|
81
|
+
response = e.response.response.env.to_h
|
|
82
|
+
|
|
83
|
+
Booqable::Error.from_response(response)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
module Booqable
|
|
2
|
+
module Middleware
|
|
3
|
+
module Auth
|
|
4
|
+
# Faraday middleware for single-use JWT token authentication
|
|
5
|
+
#
|
|
6
|
+
# This middleware generates and adds single-use JWT tokens for authentication.
|
|
7
|
+
# Each token is unique per request and includes request-specific data like
|
|
8
|
+
# method, path, and body hash to prevent replay attacks.
|
|
9
|
+
#
|
|
10
|
+
# Supports multiple JWT algorithms: HS256 (HMAC), RS256 (RSA), and ES256 (ECDSA).
|
|
11
|
+
#
|
|
12
|
+
# For more info see: https://developers.booqable.com/#authentication-request-signing
|
|
13
|
+
#
|
|
14
|
+
# @example Adding to Faraday middleware stack
|
|
15
|
+
# builder.use Booqable::Middleware::Auth::SingleUse,
|
|
16
|
+
# single_use_token: "token_id",
|
|
17
|
+
# single_use_token_algorithm: "HS256",
|
|
18
|
+
# single_use_token_private_key: "secret_key",
|
|
19
|
+
# single_use_token_company_id: "company_uuid",
|
|
20
|
+
# single_use_token_user_id: "user_uuid",
|
|
21
|
+
# api_endpoint: "https://company.booqable.com/api/4"
|
|
22
|
+
class SingleUse < Base
|
|
23
|
+
# Token kind identifier for JWT header
|
|
24
|
+
KIND = "single_use"
|
|
25
|
+
|
|
26
|
+
# Default domain for issuer URL construction
|
|
27
|
+
BOOQABLE_DOMAIN = "booqable.com"
|
|
28
|
+
|
|
29
|
+
# Initialize the single-use token authentication middleware
|
|
30
|
+
#
|
|
31
|
+
# @param app [#call] The next middleware in the Faraday stack
|
|
32
|
+
# @param options [Hash] Configuration options
|
|
33
|
+
# @option options [String] :single_use_token Token identifier (kid)
|
|
34
|
+
# @option options [String] :single_use_token_algorithm JWT algorithm (HS256, RS256, ES256)
|
|
35
|
+
# @option options [Integer] :single_use_token_expiration_period Token expiration in seconds (default: 600)
|
|
36
|
+
# @option options [String] :single_use_token_company_id Company UUID for audience claim
|
|
37
|
+
# @option options [String] :single_use_token_user_id User UUID for subject claim
|
|
38
|
+
# @option options [String] :single_use_token_private_key Private key or secret for signing
|
|
39
|
+
# @option options [String] :api_endpoint API endpoint URL for issuer determination
|
|
40
|
+
# @raise [SingleUseTokenAlgorithmRequired] If algorithm is not provided
|
|
41
|
+
# @raise [SingleUseTokenCompanyIdRequired] If company ID is not provided
|
|
42
|
+
# @raise [SingleUseTokenUserIdRequired] If user ID is not provided
|
|
43
|
+
# @raise [PrivateKeyOrSecretRequired] If private key/secret is not provided
|
|
44
|
+
def initialize(app, options = {})
|
|
45
|
+
super(app)
|
|
46
|
+
|
|
47
|
+
@kid = options.fetch(:single_use_token)
|
|
48
|
+
@alg = options.fetch(:single_use_token_algorithm) || raise(SingleUseTokenAlgorithmRequired)
|
|
49
|
+
@exp = options.fetch(:single_use_token_expiration_period, Time.now.to_i + 10 * 60)
|
|
50
|
+
@aud = options.fetch(:single_use_token_company_id) || raise(SingleUseTokenCompanyIdRequired)
|
|
51
|
+
@sub = options.fetch(:single_use_token_user_id) || raise(SingleUseTokenUserIdRequired)
|
|
52
|
+
@raw_private_key = options.fetch(:single_use_token_private_key) || raise(PrivateKeyOrSecretRequired)
|
|
53
|
+
@api_endpoint = options.fetch(:api_endpoint, nil)
|
|
54
|
+
|
|
55
|
+
@private_key = private_key
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Process the HTTP request and add single-use token authentication
|
|
59
|
+
#
|
|
60
|
+
# Generates a unique JWT token for this specific request and adds it
|
|
61
|
+
# to the Authorization header. Then passes the request to the next
|
|
62
|
+
# middleware in the stack.
|
|
63
|
+
#
|
|
64
|
+
# @param env [Faraday::Env] The request environment
|
|
65
|
+
# @return [Faraday::Response] The response from the next middleware
|
|
66
|
+
def call(env)
|
|
67
|
+
env.request_headers["Authorization"] ||= "Bearer #{generate_token(env)}"
|
|
68
|
+
|
|
69
|
+
@app.call(env)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
# Generate a JWT token for the current request
|
|
75
|
+
#
|
|
76
|
+
# @param env [Faraday::Env] The request environment
|
|
77
|
+
# @return [String] Encoded JWT token
|
|
78
|
+
def generate_token(env)
|
|
79
|
+
JWT.encode generate_payload(env), @private_key, @alg, headers
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Generate the JWT payload with request-specific claims
|
|
83
|
+
#
|
|
84
|
+
# @param env [Faraday::Env] The request environment
|
|
85
|
+
# @return [Hash] JWT payload with standard and custom claims
|
|
86
|
+
def generate_payload(env)
|
|
87
|
+
data = generate_data(env)
|
|
88
|
+
|
|
89
|
+
request_id = SecureRandom.respond_to?(:uuid_v4) ? SecureRandom.uuid_v4 : SecureRandom.uuid
|
|
90
|
+
|
|
91
|
+
{
|
|
92
|
+
alg: @alg,
|
|
93
|
+
iat: Time.now.to_i,
|
|
94
|
+
exp: Time.now.to_i + @exp,
|
|
95
|
+
aud: @aud,
|
|
96
|
+
sub: @sub,
|
|
97
|
+
iss: iss,
|
|
98
|
+
jti: "#{request_id}.#{data}"
|
|
99
|
+
}
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Generate JWT headers
|
|
103
|
+
#
|
|
104
|
+
# @return [Hash] JWT headers with key ID and kind
|
|
105
|
+
def headers
|
|
106
|
+
{ kid: @kid, kind: KIND }
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Generate request-specific data hash for the JWT
|
|
110
|
+
#
|
|
111
|
+
# Creates a hash from the HTTP method, full path, and body content
|
|
112
|
+
# to ensure each token is unique to the specific request being made.
|
|
113
|
+
#
|
|
114
|
+
# @param env [Faraday::Env] The request environment
|
|
115
|
+
# @return [String] Base64-encoded SHA256 hash of request data
|
|
116
|
+
def generate_data(env)
|
|
117
|
+
request_method = env.method.to_s.upcase
|
|
118
|
+
fullpath = env.url.request_uri
|
|
119
|
+
body = env.body
|
|
120
|
+
encoded_body = body ? Base64.strict_encode64(::OpenSSL::Digest::SHA256.new(body).digest) : nil
|
|
121
|
+
|
|
122
|
+
Base64.strict_encode64(
|
|
123
|
+
::OpenSSL::Digest::SHA256.new([ request_method, fullpath, encoded_body ].join(".")).digest
|
|
124
|
+
)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Generate the issuer URL for the JWT
|
|
128
|
+
#
|
|
129
|
+
# @return [String] Issuer URL based on the API endpoint
|
|
130
|
+
def iss
|
|
131
|
+
"https://#{slug}.#{BOOQABLE_DOMAIN}"
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Extract the company slug from the API endpoint
|
|
135
|
+
#
|
|
136
|
+
# @return [String] Company identifier from the hostname
|
|
137
|
+
def slug
|
|
138
|
+
Addressable::URI.parse(@api_endpoint).host.split(".").first
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Parse the private key based on the configured algorithm
|
|
142
|
+
#
|
|
143
|
+
# @return [OpenSSL::PKey::EC, OpenSSL::PKey::RSA, String] Parsed private key
|
|
144
|
+
def private_key
|
|
145
|
+
case @alg
|
|
146
|
+
when "ES256"
|
|
147
|
+
OpenSSL::PKey::EC.new(@raw_private_key)
|
|
148
|
+
when "RS256"
|
|
149
|
+
OpenSSL::PKey::RSA.new(@raw_private_key)
|
|
150
|
+
when "HS256"
|
|
151
|
+
@raw_private_key
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module Booqable
|
|
2
|
+
# Faraday response middleware for the Booqable client
|
|
3
|
+
module Middleware
|
|
4
|
+
# Faraday middleware that raises Booqable exceptions based on HTTP status codes
|
|
5
|
+
#
|
|
6
|
+
# This middleware automatically converts HTTP error responses into appropriate
|
|
7
|
+
# Booqable exception classes. It inspects the response status code and body to
|
|
8
|
+
# determine the specific error type and raises the corresponding exception.
|
|
9
|
+
#
|
|
10
|
+
# @example Adding to Faraday middleware stack
|
|
11
|
+
# builder.use Booqable::Middleware::RaiseError
|
|
12
|
+
#
|
|
13
|
+
# @see Booqable::Error.from_response
|
|
14
|
+
class RaiseError < Base
|
|
15
|
+
# Handle completed HTTP responses and raise exceptions for errors
|
|
16
|
+
#
|
|
17
|
+
# Called by Faraday after a response is received. Inspects the response
|
|
18
|
+
# and raises an appropriate Booqable exception if the status indicates an error.
|
|
19
|
+
# Successful responses are allowed to pass through unchanged.
|
|
20
|
+
#
|
|
21
|
+
# @param response [Faraday::Response] The HTTP response object
|
|
22
|
+
# @return [void]
|
|
23
|
+
# @raise [Booqable::Error] Various error subclasses based on status code
|
|
24
|
+
def on_complete(response)
|
|
25
|
+
Booqable::Error.from_response(response)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "oauth2"
|
|
4
|
+
|
|
5
|
+
module Booqable
|
|
6
|
+
# OAuth2 client for Booqable API authentication
|
|
7
|
+
#
|
|
8
|
+
# Provides OAuth2 authentication flow support for the Booqable API.
|
|
9
|
+
# Handles authorization code exchange for access tokens using the
|
|
10
|
+
# standard OAuth2 authorization code flow.
|
|
11
|
+
#
|
|
12
|
+
# @example Basic OAuth flow
|
|
13
|
+
# oauth_client = Booqable::OAuthClient.new(
|
|
14
|
+
# base_url: "https://demo.booqable.com",
|
|
15
|
+
# client_id: "your_client_id",
|
|
16
|
+
# client_secret: "your_client_secret",
|
|
17
|
+
# redirect_uri: "https://yourapp.com/callback"
|
|
18
|
+
# )
|
|
19
|
+
#
|
|
20
|
+
# # After user authorizes and returns with code
|
|
21
|
+
# token = oauth_client.get_token_from_code(params[:code])
|
|
22
|
+
# access_token = token.token
|
|
23
|
+
class OAuthClient
|
|
24
|
+
# OAuth2 token endpoint path
|
|
25
|
+
TOKEN_ENDPOINT = "/oauth/token"
|
|
26
|
+
|
|
27
|
+
# Initialize a new OAuth client
|
|
28
|
+
#
|
|
29
|
+
# @param base_url [String] Base URL of the Booqable instance (e.g., "https://demo.booqable.com")
|
|
30
|
+
# @param client_id [String] OAuth2 client identifier
|
|
31
|
+
# @param client_secret [String] OAuth2 client secret
|
|
32
|
+
# @param redirect_uri [String] OAuth2 redirect URI for authorization callback
|
|
33
|
+
def initialize(api_endpoint:, client_id:, client_secret:, redirect_uri: nil)
|
|
34
|
+
@client = OAuth2::Client.new(
|
|
35
|
+
client_id,
|
|
36
|
+
client_secret,
|
|
37
|
+
site: api_endpoint,
|
|
38
|
+
token_url: api_endpoint + TOKEN_ENDPOINT,
|
|
39
|
+
)
|
|
40
|
+
@redirect_uri = redirect_uri
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Exchange an authorization code for an access token
|
|
44
|
+
#
|
|
45
|
+
# Exchanges the authorization code received from the OAuth callback
|
|
46
|
+
# for an access token that can be used to make API requests.
|
|
47
|
+
#
|
|
48
|
+
# @param code [String] Authorization code from OAuth callback
|
|
49
|
+
# @param scope [String] OAuth scope to request (default: "full_access")
|
|
50
|
+
# @return [OAuth2::AccessToken] Access token object with token and refresh token
|
|
51
|
+
# @raise [OAuth2::Error] If the authorization code is invalid or expired
|
|
52
|
+
#
|
|
53
|
+
# @example
|
|
54
|
+
# token = oauth_client.get_token_from_code("auth_code_123")
|
|
55
|
+
# access_token = token.token
|
|
56
|
+
# refresh_token = token.refresh_token
|
|
57
|
+
def get_token_from_code(code, scope: "full_access")
|
|
58
|
+
@client.auth_code.get_token(code,
|
|
59
|
+
redirect_uri: @redirect_uri,
|
|
60
|
+
scope: scope,
|
|
61
|
+
grant_type: "authorization_code")
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Create an access token from a hash.
|
|
65
|
+
#
|
|
66
|
+
# @param hash [Hash] Hash containing access token data.
|
|
67
|
+
# @return [OAuth2::AccessToken] Access token object created from the hash.
|
|
68
|
+
def get_access_token_from_hash(hash)
|
|
69
|
+
OAuth2::AccessToken.from_hash(@client, hash)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Booqable
|
|
4
|
+
# Rate limit information from API responses
|
|
5
|
+
#
|
|
6
|
+
# Contains rate limiting information extracted from HTTP response headers.
|
|
7
|
+
# This information is available when rate limit errors occur or can be
|
|
8
|
+
# accessed from successful responses to monitor API usage.
|
|
9
|
+
#
|
|
10
|
+
# @example Accessing rate limit info from an error
|
|
11
|
+
# begin
|
|
12
|
+
# Booqable.orders.list
|
|
13
|
+
# rescue Booqable::TooManyRequests => e
|
|
14
|
+
# rate_limit = e.context
|
|
15
|
+
# puts "Rate limit: #{rate_limit.remaining}/#{rate_limit.limit}"
|
|
16
|
+
# puts "Resets in: #{rate_limit.resets_in} seconds"
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# @!attribute [w] limit
|
|
20
|
+
# @return [Integer] Max tries per rate limit period
|
|
21
|
+
# @!attribute [w] remaining
|
|
22
|
+
# @return [Integer] Remaining tries per rate limit period
|
|
23
|
+
# @!attribute [w] resets_in
|
|
24
|
+
# @return [Integer] Number of seconds when rate limit resets
|
|
25
|
+
class RateLimit < Struct.new(:limit, :remaining, :resets_in)
|
|
26
|
+
# Extract rate limit information from HTTP response
|
|
27
|
+
#
|
|
28
|
+
# Parses standard rate limit headers from the HTTP response and creates
|
|
29
|
+
# a RateLimit object with the extracted information. Falls back to default
|
|
30
|
+
# values if headers are missing.
|
|
31
|
+
#
|
|
32
|
+
# @param response [#headers, #response_headers] HTTP response object
|
|
33
|
+
# @return [RateLimit] Rate limit information object
|
|
34
|
+
#
|
|
35
|
+
# @example
|
|
36
|
+
# rate_limit = Booqable::RateLimit.from_response(response)
|
|
37
|
+
# puts "#{rate_limit.remaining} requests remaining"
|
|
38
|
+
def self.from_response(response)
|
|
39
|
+
info = new
|
|
40
|
+
headers = response.headers if response.respond_to?(:headers) && !response.headers.nil?
|
|
41
|
+
headers ||= response.response_headers if response.respond_to?(:response_headers) && !response.response_headers.nil?
|
|
42
|
+
if headers
|
|
43
|
+
info.limit = (headers["X-RateLimit-Limit"] || 1).to_i
|
|
44
|
+
info.remaining = (headers["X-RateLimit-Remaining"] || 1).to_i
|
|
45
|
+
info.resets_in = (headers["X-RateLimit-Period"] || 1).to_i
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
info
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Booqable
|
|
4
|
+
# Generic resource proxy for API collections
|
|
5
|
+
#
|
|
6
|
+
# Provides a uniform interface for interacting with API resources using
|
|
7
|
+
# standard CRUD operations. Each resource proxy handles the JSON:API
|
|
8
|
+
# formatting and delegates HTTP requests to the underlying client.
|
|
9
|
+
#
|
|
10
|
+
# @example Working with orders
|
|
11
|
+
# orders = Booqable::ResourceProxy.new(client, "orders")
|
|
12
|
+
#
|
|
13
|
+
# # Of course, you can also just use Booqable.orders directly.
|
|
14
|
+
#
|
|
15
|
+
# # List all orders
|
|
16
|
+
# all_orders = orders.list
|
|
17
|
+
#
|
|
18
|
+
# # Find a specific order
|
|
19
|
+
# order = orders.find("123")
|
|
20
|
+
#
|
|
21
|
+
# # Create a new order
|
|
22
|
+
# new_order = orders.create(starts_at: "2024-01-01T00:00:00Z", stops_at: "2024-01-02T00:00:00Z", status: "draft")
|
|
23
|
+
#
|
|
24
|
+
# # Update an existing order
|
|
25
|
+
# updated_order = orders.update("123", status: "reserved")
|
|
26
|
+
#
|
|
27
|
+
# # Delete an existing order
|
|
28
|
+
# orders.delete("123")
|
|
29
|
+
class ResourceProxy
|
|
30
|
+
# Initialize a new resource proxy
|
|
31
|
+
#
|
|
32
|
+
# @param client [Booqable::Client] The client instance to use for requests
|
|
33
|
+
# @param resource_name [String, Symbol] The name of the resource (e.g., "orders", "customers")
|
|
34
|
+
def initialize(client, resource_name)
|
|
35
|
+
@client = client
|
|
36
|
+
@resource = resource_name.to_s
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# List all resources
|
|
40
|
+
#
|
|
41
|
+
# Retrieves a collection of resources with optional filtering and pagination.
|
|
42
|
+
# Uses the JSON:API specification for query parameters.
|
|
43
|
+
#
|
|
44
|
+
# @param params [Hash] Query parameters for filtering, sorting, and pagination
|
|
45
|
+
# @option params [String] :include Related resources to include (e.g., "customer,items")
|
|
46
|
+
# @option params [Hash] :filter Filter criteria (e.g., { status: "active" })
|
|
47
|
+
# @option params [String] :sort Sort criteria (e.g., "created_at", "-updated_at")
|
|
48
|
+
# @option params [Hash] :page Pagination parameters (e.g., { number: 1, size: 25 })
|
|
49
|
+
# @return [Array, Enumerator] Collection of resources or enumerator for auto-pagination
|
|
50
|
+
#
|
|
51
|
+
# @example List orders with filters
|
|
52
|
+
# orders.list(
|
|
53
|
+
# include: "customer",
|
|
54
|
+
# filter: { status: "active" },
|
|
55
|
+
# sort: "-created_at"
|
|
56
|
+
# )
|
|
57
|
+
def list(params = {})
|
|
58
|
+
paginate @resource, params
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Find a specific resource by ID
|
|
62
|
+
#
|
|
63
|
+
# Retrieves a single resource by its unique identifier.
|
|
64
|
+
#
|
|
65
|
+
# @param id [String, Integer] The unique identifier of the resource
|
|
66
|
+
# @param params [Hash] Additional query parameters
|
|
67
|
+
# @option params [String] :include Related resources to include
|
|
68
|
+
# @return [Hash] The resource data
|
|
69
|
+
# @raise [Booqable::NotFound] If the resource doesn't exist
|
|
70
|
+
#
|
|
71
|
+
# @example Find an order by ID
|
|
72
|
+
# order = orders.find("123", include: "customer,items")
|
|
73
|
+
def find(id, params = {})
|
|
74
|
+
response = request :get, "#{@resource}/#{id}", params
|
|
75
|
+
response.data
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Create a new resource
|
|
79
|
+
#
|
|
80
|
+
# Creates a new resource with the provided attributes using JSON:API format.
|
|
81
|
+
#
|
|
82
|
+
# @param attrs [Hash] Attributes for the new resource
|
|
83
|
+
# @return [Hash] The created resource data
|
|
84
|
+
# @raise [Booqable::UnprocessableEntity] If validation fails
|
|
85
|
+
#
|
|
86
|
+
# @example Create a new order
|
|
87
|
+
# new_order = orders.create(
|
|
88
|
+
# starts_at: "2024-01-01T00:00:00Z",
|
|
89
|
+
# stops_at: "2024-01-02T00:00:00Z",
|
|
90
|
+
# status: "draft"
|
|
91
|
+
# )
|
|
92
|
+
def create(attrs = {})
|
|
93
|
+
response = request :post, @resource, { data: { type: @resource, attributes: attrs } }
|
|
94
|
+
response.data
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Update an existing resource
|
|
98
|
+
#
|
|
99
|
+
# Updates a resource with the provided attributes using JSON:API format.
|
|
100
|
+
#
|
|
101
|
+
# @param id [String, Integer] The unique identifier of the resource to update
|
|
102
|
+
# @param attrs [Hash] Attributes to update
|
|
103
|
+
# @return [Hash] The updated resource data
|
|
104
|
+
# @raise [Booqable::NotFound] If the resource doesn't exist
|
|
105
|
+
# @raise [Booqable::UnprocessableEntity] If validation fails
|
|
106
|
+
#
|
|
107
|
+
# @example Update an order
|
|
108
|
+
# updated_order = orders.update("123", status: "reserved")
|
|
109
|
+
def update(id, attrs = {})
|
|
110
|
+
response = request :put, "#{@resource}/#{id}", { data: { type: @resource, id: id, attributes: attrs } }
|
|
111
|
+
response.data
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Delete an existing resource
|
|
115
|
+
#
|
|
116
|
+
# Deletes a resource by its unique identifier.
|
|
117
|
+
#
|
|
118
|
+
# @param id [String, Integer] The unique identifier of the resource to delete
|
|
119
|
+
# @return [Object] The deleted resource data
|
|
120
|
+
# @raise [Booqable::NotFound] If the resource doesn't exist
|
|
121
|
+
#
|
|
122
|
+
# @example Delete an order
|
|
123
|
+
# deleted_order = orders.delete("123")
|
|
124
|
+
def delete(id)
|
|
125
|
+
response = request :delete, "#{@resource}/#{id}", {}
|
|
126
|
+
response.data
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
private
|
|
130
|
+
|
|
131
|
+
attr_reader :client
|
|
132
|
+
|
|
133
|
+
# Delegate request method to client
|
|
134
|
+
#
|
|
135
|
+
# @param args [Array] Arguments to pass to client.request
|
|
136
|
+
# @return [Object] Response from client.request
|
|
137
|
+
def request(...)
|
|
138
|
+
client.request(...)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Delegate paginate method to client
|
|
142
|
+
#
|
|
143
|
+
# @param args [Array] Arguments to pass to client.paginate
|
|
144
|
+
# @return [Object] Response from client.paginate
|
|
145
|
+
def paginate(...)
|
|
146
|
+
client.paginate(...)
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
[
|
|
2
|
+
{ "app_carriers": "carriers" },
|
|
3
|
+
{ "app_subscriptions": "subscriptions" },
|
|
4
|
+
{ "app_payment_options": "payment_options" },
|
|
5
|
+
"authentication_methods",
|
|
6
|
+
"barcodes",
|
|
7
|
+
"bundles",
|
|
8
|
+
"bundle_items",
|
|
9
|
+
"clusters",
|
|
10
|
+
"collections",
|
|
11
|
+
"collection_items",
|
|
12
|
+
"collection_trees",
|
|
13
|
+
"companies",
|
|
14
|
+
"countries",
|
|
15
|
+
"coupons",
|
|
16
|
+
"customers",
|
|
17
|
+
"default_properties",
|
|
18
|
+
"delivery_distance_calculations",
|
|
19
|
+
"deposit_holds",
|
|
20
|
+
"documents",
|
|
21
|
+
"emails",
|
|
22
|
+
"email_templates",
|
|
23
|
+
"employees",
|
|
24
|
+
"employee_invitations",
|
|
25
|
+
"inventory_breakdowns",
|
|
26
|
+
"inventory_levels",
|
|
27
|
+
"invoice_finalizations",
|
|
28
|
+
"invoice_revisions",
|
|
29
|
+
"item_prices",
|
|
30
|
+
"items",
|
|
31
|
+
"lines",
|
|
32
|
+
"line_charge_suggestions",
|
|
33
|
+
"locations",
|
|
34
|
+
"notes",
|
|
35
|
+
"orders",
|
|
36
|
+
"order_delivery_rate_recalculations",
|
|
37
|
+
"order_delivery_rates",
|
|
38
|
+
"order_fulfillments",
|
|
39
|
+
"order_price_recalculations",
|
|
40
|
+
"order_status_transitions",
|
|
41
|
+
"payments",
|
|
42
|
+
"payment_authorizations",
|
|
43
|
+
"payment_charges",
|
|
44
|
+
"payment_methods",
|
|
45
|
+
"payment_refunds",
|
|
46
|
+
"photos",
|
|
47
|
+
"plannings",
|
|
48
|
+
"price_rules",
|
|
49
|
+
"price_rulesets",
|
|
50
|
+
"price_structures",
|
|
51
|
+
"price_tiles",
|
|
52
|
+
"products",
|
|
53
|
+
"product_groups",
|
|
54
|
+
"properties",
|
|
55
|
+
"provinces",
|
|
56
|
+
"settings",
|
|
57
|
+
"signatures",
|
|
58
|
+
"sortings",
|
|
59
|
+
"stock_adjustments",
|
|
60
|
+
"stock_items",
|
|
61
|
+
"stock_item_archivations",
|
|
62
|
+
"stock_item_plannings",
|
|
63
|
+
"stock_item_suggestions",
|
|
64
|
+
"tags",
|
|
65
|
+
"tax_categories",
|
|
66
|
+
"tax_rates",
|
|
67
|
+
"tax_regions",
|
|
68
|
+
"tax_values",
|
|
69
|
+
"transfers",
|
|
70
|
+
"user_invitations",
|
|
71
|
+
"users",
|
|
72
|
+
"webhook_endpoints",
|
|
73
|
+
"webhooks"
|
|
74
|
+
]
|