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.
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
-
@@ -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
- def initialize(project_url, api_key, timeout = 30)
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
- @api_url = base_url.sub('/api', '') + '/api/v2'
39
+ base_url = base_url.split('/api').first
40
+ @api_url = "#{base_url}/api/v2"
21
41
  else
22
- @api_url = base_url + '/api/v2'
42
+ @api_url = "#{base_url}/api/v2"
23
43
  end
24
44
  else
25
- @api_url = "https://#{project_url}.wowsql.com/api/v2"
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
- @conn = Faraday.new(url: @api_url) do |f|
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
- begin
66
- response = @conn.public_send(method.downcase, path) do |req|
67
- req.params = params if params
68
- req.body = json if json
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
- response.body
78
- rescue Faraday::Error => e
79
- raise WOWSQLException.new("Request failed: #{e.message}")
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
-