wowsql-sdk 1.2.0 → 3.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 +4 -4
- data/README.md +359 -417
- data/lib/wowmysql.rb +3 -2
- data/lib/wowsql/auth.rb +533 -0
- data/lib/wowsql/client.rb +54 -25
- data/lib/wowsql/exceptions.rb +15 -14
- data/lib/wowsql/query_builder.rb +239 -104
- data/lib/wowsql/schema.rb +250 -0
- data/lib/wowsql/storage.rb +380 -0
- data/lib/wowsql/table.rb +118 -9
- data/lib/wowsql/version.rb +1 -1
- data/lib/wowsql.rb +3 -2
- metadata +23 -6
data/lib/wowmysql.rb
CHANGED
|
@@ -3,8 +3,9 @@ require_relative 'wowsql/exceptions'
|
|
|
3
3
|
require_relative 'wowsql/client'
|
|
4
4
|
require_relative 'wowsql/table'
|
|
5
5
|
require_relative 'wowsql/query_builder'
|
|
6
|
+
require_relative 'wowsql/auth'
|
|
7
|
+
require_relative 'wowsql/storage'
|
|
8
|
+
require_relative 'wowsql/schema'
|
|
6
9
|
|
|
7
10
|
module WOWSQL
|
|
8
|
-
# Main module for WOWSQL SDK
|
|
9
11
|
end
|
|
10
|
-
|
data/lib/wowsql/auth.rb
ADDED
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
require 'faraday'
|
|
2
|
+
require 'json'
|
|
3
|
+
require_relative 'exceptions'
|
|
4
|
+
|
|
5
|
+
module WOWSQL
|
|
6
|
+
# Represents an authenticated user.
|
|
7
|
+
AuthUser = Struct.new(
|
|
8
|
+
:id, :email, :full_name, :avatar_url, :email_verified,
|
|
9
|
+
:user_metadata, :app_metadata, :created_at,
|
|
10
|
+
keyword_init: true
|
|
11
|
+
) do
|
|
12
|
+
def initialize(id: '', email: '', full_name: nil, avatar_url: nil,
|
|
13
|
+
email_verified: false, user_metadata: nil,
|
|
14
|
+
app_metadata: nil, created_at: nil)
|
|
15
|
+
super
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Session tokens returned by the auth service.
|
|
20
|
+
AuthSession = Struct.new(
|
|
21
|
+
:access_token, :refresh_token, :token_type, :expires_in,
|
|
22
|
+
keyword_init: true
|
|
23
|
+
) do
|
|
24
|
+
def initialize(access_token: '', refresh_token: '', token_type: 'bearer', expires_in: 0)
|
|
25
|
+
super
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Response for signup/login requests.
|
|
30
|
+
AuthResponse = Struct.new(:session, :user, keyword_init: true) do
|
|
31
|
+
def initialize(session:, user: nil)
|
|
32
|
+
super
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Interface for persisting tokens.
|
|
37
|
+
module TokenStorage
|
|
38
|
+
def get_access_token
|
|
39
|
+
raise NotImplementedError
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def set_access_token(_token)
|
|
43
|
+
raise NotImplementedError
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def get_refresh_token
|
|
47
|
+
raise NotImplementedError
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def set_refresh_token(_token)
|
|
51
|
+
raise NotImplementedError
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Default in-memory token storage.
|
|
56
|
+
class MemoryTokenStorage
|
|
57
|
+
include TokenStorage
|
|
58
|
+
|
|
59
|
+
def initialize
|
|
60
|
+
@access = nil
|
|
61
|
+
@refresh = nil
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def get_access_token
|
|
65
|
+
@access
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def set_access_token(token)
|
|
69
|
+
@access = token
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def get_refresh_token
|
|
73
|
+
@refresh
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def set_refresh_token(token)
|
|
77
|
+
@refresh = token
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Project-level authentication client.
|
|
82
|
+
#
|
|
83
|
+
# UNIFIED AUTHENTICATION: Uses the same API keys (anon/service) as database operations.
|
|
84
|
+
# One project = one set of keys for ALL operations (auth + database).
|
|
85
|
+
#
|
|
86
|
+
# Key Types:
|
|
87
|
+
# - Anonymous Key (wowsql_anon_...): For client-side auth operations
|
|
88
|
+
# - Service Role Key (wowsql_service_...): For server-side auth operations
|
|
89
|
+
class ProjectAuthClient
|
|
90
|
+
attr_reader :base_url
|
|
91
|
+
|
|
92
|
+
# @param project_url [String] Project subdomain or full URL
|
|
93
|
+
# @param api_key [String] API key for authentication
|
|
94
|
+
# @param base_domain [String] Base domain (default: "wowsql.com")
|
|
95
|
+
# @param secure [Boolean] Use HTTPS (default: true)
|
|
96
|
+
# @param timeout [Integer] Request timeout in seconds (default: 30)
|
|
97
|
+
# @param verify_ssl [Boolean] Verify SSL certificates (default: true)
|
|
98
|
+
# @param token_storage [TokenStorage, nil] Optional token storage
|
|
99
|
+
def initialize(project_url, api_key, base_domain: 'wowsql.com', secure: true,
|
|
100
|
+
timeout: 30, verify_ssl: true, token_storage: nil)
|
|
101
|
+
@api_key = api_key
|
|
102
|
+
@timeout = timeout
|
|
103
|
+
@base_url = build_auth_base_url(project_url, base_domain, secure)
|
|
104
|
+
|
|
105
|
+
@storage = token_storage || MemoryTokenStorage.new
|
|
106
|
+
@access_token = @storage.get_access_token
|
|
107
|
+
@refresh_token = @storage.get_refresh_token
|
|
108
|
+
|
|
109
|
+
ssl_options = verify_ssl ? {} : { verify: false }
|
|
110
|
+
|
|
111
|
+
@conn = Faraday.new(url: @base_url, ssl: ssl_options) do |f|
|
|
112
|
+
f.request :json
|
|
113
|
+
f.response :json
|
|
114
|
+
f.adapter Faraday.default_adapter
|
|
115
|
+
f.options.timeout = timeout
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
@conn.headers['Authorization'] = "Bearer #{api_key}"
|
|
119
|
+
@conn.headers['Content-Type'] = 'application/json'
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Sign up a new user.
|
|
123
|
+
#
|
|
124
|
+
# @param email [String] User email
|
|
125
|
+
# @param password [String] User password (minimum 8 characters)
|
|
126
|
+
# @param full_name [String, nil] Optional full name
|
|
127
|
+
# @param user_metadata [Hash, nil] Optional user metadata
|
|
128
|
+
# @return [AuthResponse]
|
|
129
|
+
def sign_up(email:, password:, full_name: nil, user_metadata: nil)
|
|
130
|
+
payload = { email: email, password: password }
|
|
131
|
+
payload[:full_name] = full_name if full_name
|
|
132
|
+
payload[:user_metadata] = user_metadata if user_metadata
|
|
133
|
+
|
|
134
|
+
data = request('POST', '/signup', nil, payload)
|
|
135
|
+
session = persist_session(data)
|
|
136
|
+
user = data['user'] ? AuthUser.new(**normalize_user(data['user'])) : nil
|
|
137
|
+
AuthResponse.new(session: session, user: user)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Sign in an existing user.
|
|
141
|
+
#
|
|
142
|
+
# @param email [String] User email
|
|
143
|
+
# @param password [String] User password
|
|
144
|
+
# @return [AuthResponse]
|
|
145
|
+
def sign_in(email:, password:)
|
|
146
|
+
payload = { email: email, password: password }
|
|
147
|
+
data = request('POST', '/login', nil, payload)
|
|
148
|
+
session = persist_session(data)
|
|
149
|
+
AuthResponse.new(session: session, user: nil)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Get current authenticated user.
|
|
153
|
+
#
|
|
154
|
+
# @param access_token [String, nil] Override access token
|
|
155
|
+
# @return [AuthUser]
|
|
156
|
+
def get_user(access_token: nil)
|
|
157
|
+
token = access_token || @access_token || @storage.get_access_token
|
|
158
|
+
raise WOWSQLError.new('Access token is required. Call sign_in first.') unless token
|
|
159
|
+
|
|
160
|
+
data = request_with_headers('GET', '/me', nil, nil, 'Authorization' => "Bearer #{token}")
|
|
161
|
+
AuthUser.new(**normalize_user(data))
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Get OAuth authorization URL.
|
|
165
|
+
#
|
|
166
|
+
# @param provider [String] OAuth provider name (e.g., 'github', 'google')
|
|
167
|
+
# @param redirect_uri [String, nil] Optional frontend redirect URI
|
|
168
|
+
# @return [Hash]
|
|
169
|
+
def get_oauth_authorization_url(provider:, redirect_uri: nil)
|
|
170
|
+
raise WOWSQLError.new('provider is required and cannot be empty') if provider.nil? || provider.strip.empty?
|
|
171
|
+
|
|
172
|
+
provider = provider.strip
|
|
173
|
+
params = {}
|
|
174
|
+
params['frontend_redirect_uri'] = redirect_uri.strip if redirect_uri
|
|
175
|
+
|
|
176
|
+
begin
|
|
177
|
+
data = request('GET', "/oauth/#{provider}", params, nil)
|
|
178
|
+
{
|
|
179
|
+
'authorization_url' => data['authorization_url'] || '',
|
|
180
|
+
'provider' => data['provider'] || provider,
|
|
181
|
+
'backend_callback_url' => data['backend_callback_url'] || '',
|
|
182
|
+
'frontend_redirect_uri' => data['frontend_redirect_uri'] || redirect_uri || ''
|
|
183
|
+
}
|
|
184
|
+
rescue WOWSQLError => e
|
|
185
|
+
if e.status_code == 502
|
|
186
|
+
raise WOWSQLError.new(
|
|
187
|
+
"Bad Gateway (502): The backend server may be down or unreachable. " \
|
|
188
|
+
"Check if the backend is running and accessible at #{@base_url}",
|
|
189
|
+
502, e.response
|
|
190
|
+
)
|
|
191
|
+
elsif e.status_code == 400
|
|
192
|
+
raise WOWSQLError.new(
|
|
193
|
+
"Bad Request (400): #{e.message}. " \
|
|
194
|
+
"Ensure OAuth provider '#{provider}' is configured and enabled for this project.",
|
|
195
|
+
400, e.response
|
|
196
|
+
)
|
|
197
|
+
end
|
|
198
|
+
raise
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Exchange OAuth callback code for access tokens.
|
|
203
|
+
#
|
|
204
|
+
# @param provider [String] OAuth provider name
|
|
205
|
+
# @param code [String] Authorization code from OAuth provider callback
|
|
206
|
+
# @param redirect_uri [String, nil] Optional redirect URI
|
|
207
|
+
# @return [AuthResponse]
|
|
208
|
+
def exchange_oauth_callback(provider:, code:, redirect_uri: nil)
|
|
209
|
+
payload = { code: code }
|
|
210
|
+
payload[:redirect_uri] = redirect_uri if redirect_uri
|
|
211
|
+
|
|
212
|
+
data = request('POST', "/oauth/#{provider}/callback", nil, payload)
|
|
213
|
+
session = persist_session(data)
|
|
214
|
+
user = data['user'] ? AuthUser.new(**normalize_user(data['user'])) : nil
|
|
215
|
+
AuthResponse.new(session: session, user: user)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Request password reset.
|
|
219
|
+
#
|
|
220
|
+
# @param email [String] User's email address
|
|
221
|
+
# @return [Hash]
|
|
222
|
+
def forgot_password(email:)
|
|
223
|
+
payload = { email: email }
|
|
224
|
+
data = request('POST', '/forgot-password', nil, payload)
|
|
225
|
+
{
|
|
226
|
+
'success' => data['success'] != false,
|
|
227
|
+
'message' => data['message'] || 'If that email exists, a password reset link has been sent'
|
|
228
|
+
}
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Reset password with token.
|
|
232
|
+
#
|
|
233
|
+
# @param token [String] Password reset token from email
|
|
234
|
+
# @param new_password [String] New password (minimum 8 characters)
|
|
235
|
+
# @return [Hash]
|
|
236
|
+
def reset_password(token:, new_password:)
|
|
237
|
+
payload = { token: token, new_password: new_password }
|
|
238
|
+
data = request('POST', '/reset-password', nil, payload)
|
|
239
|
+
{
|
|
240
|
+
'success' => data['success'] != false,
|
|
241
|
+
'message' => data['message'] || 'Password reset successfully! You can now login with your new password'
|
|
242
|
+
}
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# Send OTP code to user's email.
|
|
246
|
+
#
|
|
247
|
+
# @param email [String] User's email address
|
|
248
|
+
# @param purpose [String] 'login', 'signup', or 'password_reset'
|
|
249
|
+
# @return [Hash]
|
|
250
|
+
def send_otp(email:, purpose: 'login')
|
|
251
|
+
unless %w[login signup password_reset].include?(purpose)
|
|
252
|
+
raise WOWSQLError.new("Purpose must be 'login', 'signup', or 'password_reset'")
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
payload = { email: email, purpose: purpose }
|
|
256
|
+
data = request('POST', '/otp/send', nil, payload)
|
|
257
|
+
{
|
|
258
|
+
'success' => data['success'] != false,
|
|
259
|
+
'message' => data['message'] || 'If that email exists, an OTP code has been sent'
|
|
260
|
+
}
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# Verify OTP and complete authentication.
|
|
264
|
+
#
|
|
265
|
+
# @param email [String] User's email address
|
|
266
|
+
# @param otp [String] 6-digit OTP code
|
|
267
|
+
# @param purpose [String] 'login', 'signup', or 'password_reset'
|
|
268
|
+
# @param new_password [String, nil] Required for password_reset purpose
|
|
269
|
+
# @return [AuthResponse, Hash]
|
|
270
|
+
def verify_otp(email:, otp:, purpose: 'login', new_password: nil)
|
|
271
|
+
unless %w[login signup password_reset].include?(purpose)
|
|
272
|
+
raise WOWSQLError.new("Purpose must be 'login', 'signup', or 'password_reset'")
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
if purpose == 'password_reset' && new_password.nil?
|
|
276
|
+
raise WOWSQLError.new('new_password is required for password_reset purpose')
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
payload = { email: email, otp: otp, purpose: purpose }
|
|
280
|
+
payload[:new_password] = new_password if new_password
|
|
281
|
+
|
|
282
|
+
data = request('POST', '/otp/verify', nil, payload)
|
|
283
|
+
|
|
284
|
+
if purpose == 'password_reset'
|
|
285
|
+
return {
|
|
286
|
+
'success' => data['success'] != false,
|
|
287
|
+
'message' => data['message'] || 'Password reset successfully! You can now login with your new password'
|
|
288
|
+
}
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
session = persist_session(data)
|
|
292
|
+
user = data['user'] ? AuthUser.new(**normalize_user(data['user'])) : nil
|
|
293
|
+
AuthResponse.new(session: session, user: user)
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
# Send magic link to user's email.
|
|
297
|
+
#
|
|
298
|
+
# @param email [String] User's email address
|
|
299
|
+
# @param purpose [String] 'login', 'signup', or 'email_verification'
|
|
300
|
+
# @return [Hash]
|
|
301
|
+
def send_magic_link(email:, purpose: 'login')
|
|
302
|
+
unless %w[login signup email_verification].include?(purpose)
|
|
303
|
+
raise WOWSQLError.new("Purpose must be 'login', 'signup', or 'email_verification'")
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
payload = { email: email, purpose: purpose }
|
|
307
|
+
data = request('POST', '/magic-link/send', nil, payload)
|
|
308
|
+
{
|
|
309
|
+
'success' => data['success'] != false,
|
|
310
|
+
'message' => data['message'] || 'If that email exists, a magic link has been sent'
|
|
311
|
+
}
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
# Verify email using token.
|
|
315
|
+
#
|
|
316
|
+
# @param token [String] Verification token from email
|
|
317
|
+
# @return [Hash]
|
|
318
|
+
def verify_email(token:)
|
|
319
|
+
payload = { token: token }
|
|
320
|
+
data = request('POST', '/verify-email', nil, payload)
|
|
321
|
+
{
|
|
322
|
+
'success' => data['success'] != false,
|
|
323
|
+
'message' => data['message'] || 'Email verified successfully!',
|
|
324
|
+
'user' => data['user'] ? AuthUser.new(**normalize_user(data['user'])) : nil
|
|
325
|
+
}
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
# Resend verification email.
|
|
329
|
+
#
|
|
330
|
+
# @param email [String] User's email address
|
|
331
|
+
# @return [Hash]
|
|
332
|
+
def resend_verification(email:)
|
|
333
|
+
payload = { email: email }
|
|
334
|
+
data = request('POST', '/resend-verification', nil, payload)
|
|
335
|
+
{
|
|
336
|
+
'success' => data['success'] != false,
|
|
337
|
+
'message' => data['message'] || 'If that email exists, a verification email has been sent'
|
|
338
|
+
}
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
# Logout the current user by invalidating their session.
|
|
342
|
+
#
|
|
343
|
+
# @param access_token [String, nil] Override access token
|
|
344
|
+
# @return [Hash]
|
|
345
|
+
def logout(access_token: nil)
|
|
346
|
+
token = access_token || @access_token || @storage.get_access_token
|
|
347
|
+
raise WOWSQLError.new('Access token is required. Call sign_in first.') unless token
|
|
348
|
+
|
|
349
|
+
data = request_with_headers('POST', '/logout', nil, nil, 'Authorization' => "Bearer #{token}")
|
|
350
|
+
clear_session
|
|
351
|
+
data
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
# Exchange a refresh token for new access + refresh tokens.
|
|
355
|
+
#
|
|
356
|
+
# @param refresh_token [String, nil] Override refresh token
|
|
357
|
+
# @return [AuthResponse]
|
|
358
|
+
def refresh_token(refresh_token: nil)
|
|
359
|
+
token = refresh_token || @refresh_token || @storage.get_refresh_token
|
|
360
|
+
raise WOWSQLError.new('Refresh token is required. Call sign_in first.') unless token
|
|
361
|
+
|
|
362
|
+
data = request('POST', '/refresh-token', nil, { refresh_token: token })
|
|
363
|
+
session = persist_session(data)
|
|
364
|
+
AuthResponse.new(session: session, user: nil)
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
# Change the authenticated user's password.
|
|
368
|
+
#
|
|
369
|
+
# @param current_password [String] Current password
|
|
370
|
+
# @param new_password [String] New password (minimum 8 characters)
|
|
371
|
+
# @param access_token [String, nil] Override access token
|
|
372
|
+
# @return [Hash]
|
|
373
|
+
def change_password(current_password:, new_password:, access_token: nil)
|
|
374
|
+
token = access_token || @access_token || @storage.get_access_token
|
|
375
|
+
raise WOWSQLError.new('Access token is required. Call sign_in first.') unless token
|
|
376
|
+
|
|
377
|
+
request_with_headers(
|
|
378
|
+
'POST', '/change-password', nil,
|
|
379
|
+
{ current_password: current_password, new_password: new_password },
|
|
380
|
+
'Authorization' => "Bearer #{token}"
|
|
381
|
+
)
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
# Update the authenticated user's profile.
|
|
385
|
+
#
|
|
386
|
+
# @param full_name [String, nil] Updated full name
|
|
387
|
+
# @param avatar_url [String, nil] Updated avatar URL
|
|
388
|
+
# @param username [String, nil] Updated username
|
|
389
|
+
# @param user_metadata [Hash, nil] Updated metadata
|
|
390
|
+
# @param access_token [String, nil] Override access token
|
|
391
|
+
# @return [AuthUser]
|
|
392
|
+
def update_user(full_name: nil, avatar_url: nil, username: nil, user_metadata: nil, access_token: nil)
|
|
393
|
+
token = access_token || @access_token || @storage.get_access_token
|
|
394
|
+
raise WOWSQLError.new('Access token is required. Call sign_in first.') unless token
|
|
395
|
+
|
|
396
|
+
payload = {}
|
|
397
|
+
payload[:full_name] = full_name unless full_name.nil?
|
|
398
|
+
payload[:avatar_url] = avatar_url unless avatar_url.nil?
|
|
399
|
+
payload[:username] = username unless username.nil?
|
|
400
|
+
payload[:user_metadata] = user_metadata unless user_metadata.nil?
|
|
401
|
+
raise WOWSQLError.new('At least one field to update is required') if payload.empty?
|
|
402
|
+
|
|
403
|
+
data = request_with_headers('PATCH', '/me', nil, payload, 'Authorization' => "Bearer #{token}")
|
|
404
|
+
AuthUser.new(**normalize_user(data))
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
# Get current session tokens.
|
|
408
|
+
#
|
|
409
|
+
# @return [Hash]
|
|
410
|
+
def get_session
|
|
411
|
+
{
|
|
412
|
+
'access_token' => @access_token || @storage.get_access_token,
|
|
413
|
+
'refresh_token' => @refresh_token || @storage.get_refresh_token
|
|
414
|
+
}
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
# Set session tokens.
|
|
418
|
+
#
|
|
419
|
+
# @param access_token [String] Access token
|
|
420
|
+
# @param refresh_token [String, nil] Optional refresh token
|
|
421
|
+
def set_session(access_token:, refresh_token: nil)
|
|
422
|
+
@access_token = access_token
|
|
423
|
+
@refresh_token = refresh_token
|
|
424
|
+
@storage.set_access_token(access_token)
|
|
425
|
+
@storage.set_refresh_token(refresh_token)
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
# Clear session tokens.
|
|
429
|
+
def clear_session
|
|
430
|
+
@access_token = nil
|
|
431
|
+
@refresh_token = nil
|
|
432
|
+
@storage.set_access_token(nil)
|
|
433
|
+
@storage.set_refresh_token(nil)
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
# Close the HTTP connection.
|
|
437
|
+
def close
|
|
438
|
+
@conn.close if @conn.respond_to?(:close)
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
private
|
|
442
|
+
|
|
443
|
+
def build_auth_base_url(project_url, base_domain, secure)
|
|
444
|
+
normalized = project_url.strip
|
|
445
|
+
|
|
446
|
+
if normalized.start_with?('http://') || normalized.start_with?('https://')
|
|
447
|
+
normalized = normalized.chomp('/')
|
|
448
|
+
normalized = normalized[0...-4] if normalized.end_with?('/api')
|
|
449
|
+
return "#{normalized}/api/auth"
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
protocol = secure ? 'https' : 'http'
|
|
453
|
+
if normalized.include?(".#{base_domain}") || normalized.end_with?(base_domain)
|
|
454
|
+
normalized = "#{protocol}://#{normalized}"
|
|
455
|
+
else
|
|
456
|
+
normalized = "#{protocol}://#{normalized}.#{base_domain}"
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
normalized = normalized.chomp('/')
|
|
460
|
+
normalized = normalized[0...-4] if normalized.end_with?('/api')
|
|
461
|
+
|
|
462
|
+
"#{normalized}/api/auth"
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
def normalize_user(user)
|
|
466
|
+
return { id: '', email: '' } unless user
|
|
467
|
+
|
|
468
|
+
{
|
|
469
|
+
id: user['id'] || user[:id] || '',
|
|
470
|
+
email: user['email'] || user[:email] || '',
|
|
471
|
+
full_name: user['full_name'] || user['fullName'] || user[:full_name] || user[:fullName],
|
|
472
|
+
avatar_url: user['avatar_url'] || user['avatarUrl'] || user[:avatar_url] || user[:avatarUrl],
|
|
473
|
+
email_verified: !!(user['email_verified'] || user['emailVerified'] || user[:email_verified] || user[:emailVerified]),
|
|
474
|
+
user_metadata: user['user_metadata'] || user['userMetadata'] || user[:user_metadata] || user[:userMetadata] || {},
|
|
475
|
+
app_metadata: user['app_metadata'] || user['appMetadata'] || user[:app_metadata] || user[:appMetadata] || {},
|
|
476
|
+
created_at: user['created_at'] || user['createdAt'] || user[:created_at] || user[:createdAt]
|
|
477
|
+
}
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
def persist_session(data)
|
|
481
|
+
session = AuthSession.new(
|
|
482
|
+
access_token: data['access_token'] || data[:access_token] || '',
|
|
483
|
+
refresh_token: data['refresh_token'] || data[:refresh_token] || '',
|
|
484
|
+
token_type: data['token_type'] || data[:token_type] || 'bearer',
|
|
485
|
+
expires_in: data['expires_in'] || data[:expires_in] || 0
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
@access_token = session.access_token
|
|
489
|
+
@refresh_token = session.refresh_token
|
|
490
|
+
@storage.set_access_token(session.access_token)
|
|
491
|
+
@storage.set_refresh_token(session.refresh_token)
|
|
492
|
+
|
|
493
|
+
session
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
def request(method, path, params = nil, json = nil)
|
|
497
|
+
response = @conn.public_send(method.downcase, path) do |req|
|
|
498
|
+
req.params = params if params
|
|
499
|
+
req.body = json if json
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
if response.status >= 400
|
|
503
|
+
error_data = response.body.is_a?(Hash) ? response.body : {}
|
|
504
|
+
error_msg = error_data['detail'] || error_data['message'] || error_data['error'] ||
|
|
505
|
+
"Request failed with status #{response.status}"
|
|
506
|
+
raise WOWSQLError.new(error_msg, response.status, error_data)
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
response.body || {}
|
|
510
|
+
rescue Faraday::Error => e
|
|
511
|
+
raise WOWSQLError.new("Request failed: #{e.message}")
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
def request_with_headers(method, path, params, json, extra_headers)
|
|
515
|
+
response = @conn.public_send(method.downcase, path) do |req|
|
|
516
|
+
req.params = params if params
|
|
517
|
+
req.body = json if json
|
|
518
|
+
extra_headers.each { |k, v| req.headers[k] = v } if extra_headers
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
if response.status >= 400
|
|
522
|
+
error_data = response.body.is_a?(Hash) ? response.body : {}
|
|
523
|
+
error_msg = error_data['detail'] || error_data['message'] || error_data['error'] ||
|
|
524
|
+
"Request failed with status #{response.status}"
|
|
525
|
+
raise WOWSQLError.new(error_msg, response.status, error_data)
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
response.body || {}
|
|
529
|
+
rescue Faraday::Error => e
|
|
530
|
+
raise WOWSQLError.new("Request failed: #{e.message}")
|
|
531
|
+
end
|
|
532
|
+
end
|
|
533
|
+
end
|
data/lib/wowsql/client.rb
CHANGED
|
@@ -5,27 +5,54 @@ require_relative 'table'
|
|
|
5
5
|
|
|
6
6
|
module WOWSQL
|
|
7
7
|
# WOWSQL client for interacting with your database via REST API.
|
|
8
|
-
#
|
|
8
|
+
#
|
|
9
9
|
# This client is used for DATABASE OPERATIONS (CRUD on tables).
|
|
10
10
|
# Use Service Role Key or Anonymous Key for authentication.
|
|
11
|
+
#
|
|
12
|
+
# Key Types:
|
|
13
|
+
# - Service Role Key: Full access to all database operations (server-side)
|
|
14
|
+
# - Anonymous Key: Public access with limited permissions (client-side)
|
|
15
|
+
#
|
|
16
|
+
# @example
|
|
17
|
+
# client = WOWSQL::WOWSQLClient.new(
|
|
18
|
+
# "myproject",
|
|
19
|
+
# "wowbase_service_..."
|
|
20
|
+
# )
|
|
21
|
+
# users = client.table("users").get
|
|
11
22
|
class WOWSQLClient
|
|
12
|
-
|
|
23
|
+
attr_reader :api_url, :api_key, :timeout, :verify_ssl
|
|
24
|
+
|
|
25
|
+
# @param project_url [String] Project subdomain or full URL
|
|
26
|
+
# @param api_key [String] API key for database operations
|
|
27
|
+
# @param base_domain [String] Base domain (default: "wowsql.com")
|
|
28
|
+
# @param secure [Boolean] Use HTTPS (default: true)
|
|
29
|
+
# @param timeout [Integer] Request timeout in seconds (default: 30)
|
|
30
|
+
# @param verify_ssl [Boolean] Verify SSL certificates (default: true)
|
|
31
|
+
def initialize(project_url, api_key, base_domain: 'wowsql.com', secure: true, timeout: 30, verify_ssl: true)
|
|
13
32
|
@api_key = api_key
|
|
14
33
|
@timeout = timeout
|
|
34
|
+
@verify_ssl = verify_ssl
|
|
15
35
|
|
|
16
|
-
# Build API URL
|
|
17
36
|
if project_url.start_with?('http://') || project_url.start_with?('https://')
|
|
18
37
|
base_url = project_url.chomp('/')
|
|
19
38
|
if base_url.include?('/api')
|
|
20
|
-
|
|
39
|
+
base_url = base_url.split('/api').first
|
|
40
|
+
@api_url = "#{base_url}/api/v2"
|
|
21
41
|
else
|
|
22
|
-
@api_url = base_url
|
|
42
|
+
@api_url = "#{base_url}/api/v2"
|
|
23
43
|
end
|
|
24
44
|
else
|
|
25
|
-
|
|
45
|
+
protocol = secure ? 'https' : 'http'
|
|
46
|
+
if project_url.include?(".#{base_domain}") || project_url.end_with?(base_domain)
|
|
47
|
+
@api_url = "#{protocol}://#{project_url}/api/v2"
|
|
48
|
+
else
|
|
49
|
+
@api_url = "#{protocol}://#{project_url}.#{base_domain}/api/v2"
|
|
50
|
+
end
|
|
26
51
|
end
|
|
27
52
|
|
|
28
|
-
|
|
53
|
+
ssl_options = verify_ssl ? {} : { verify: false }
|
|
54
|
+
|
|
55
|
+
@conn = Faraday.new(url: @api_url, ssl: ssl_options) do |f|
|
|
29
56
|
f.request :json
|
|
30
57
|
f.response :json
|
|
31
58
|
f.adapter Faraday.default_adapter
|
|
@@ -37,7 +64,7 @@ module WOWSQL
|
|
|
37
64
|
end
|
|
38
65
|
|
|
39
66
|
# Get a table interface for fluent API.
|
|
40
|
-
#
|
|
67
|
+
#
|
|
41
68
|
# @param table_name [String] Name of the table
|
|
42
69
|
# @return [Table] Table instance for the specified table
|
|
43
70
|
def table(table_name)
|
|
@@ -45,7 +72,7 @@ module WOWSQL
|
|
|
45
72
|
end
|
|
46
73
|
|
|
47
74
|
# List all tables in the database.
|
|
48
|
-
#
|
|
75
|
+
#
|
|
49
76
|
# @return [Array<String>] List of table names
|
|
50
77
|
def list_tables
|
|
51
78
|
response = request('GET', '/tables', nil, nil)
|
|
@@ -53,32 +80,34 @@ module WOWSQL
|
|
|
53
80
|
end
|
|
54
81
|
|
|
55
82
|
# Get table schema information.
|
|
56
|
-
#
|
|
83
|
+
#
|
|
57
84
|
# @param table_name [String] Name of the table
|
|
58
85
|
# @return [Hash] Table schema with columns and primary key
|
|
59
86
|
def get_table_schema(table_name)
|
|
60
87
|
request('GET', "/tables/#{table_name}/schema", nil, nil)
|
|
61
88
|
end
|
|
62
89
|
|
|
90
|
+
# Close the HTTP connection.
|
|
91
|
+
def close
|
|
92
|
+
@conn.close if @conn.respond_to?(:close)
|
|
93
|
+
end
|
|
94
|
+
|
|
63
95
|
# Make HTTP request to API.
|
|
64
96
|
def request(method, path, params = nil, json = nil)
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
if response.status >= 400
|
|
72
|
-
error_data = response.body.is_a?(Hash) ? response.body : {}
|
|
73
|
-
error_msg = error_data['detail'] || error_data['message'] || "Request failed with status #{response.status}"
|
|
74
|
-
raise WOWSQLException.new(error_msg, response.status, error_data)
|
|
75
|
-
end
|
|
97
|
+
response = @conn.public_send(method.downcase, path) do |req|
|
|
98
|
+
req.params = params if params
|
|
99
|
+
req.body = json if json
|
|
100
|
+
end
|
|
76
101
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
102
|
+
if response.status >= 400
|
|
103
|
+
error_data = response.body.is_a?(Hash) ? response.body : {}
|
|
104
|
+
error_msg = error_data['detail'] || error_data['message'] || "Request failed with status #{response.status}"
|
|
105
|
+
raise WOWSQLError.new(error_msg, response.status, error_data)
|
|
80
106
|
end
|
|
107
|
+
|
|
108
|
+
response.body
|
|
109
|
+
rescue Faraday::Error => e
|
|
110
|
+
raise WOWSQLError.new("Request failed: #{e.message}")
|
|
81
111
|
end
|
|
82
112
|
end
|
|
83
113
|
end
|
|
84
|
-
|