wowsql-sdk 3.0.2 → 3.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.
data/README.md CHANGED
@@ -1,496 +1,728 @@
1
1
  # WowSQL Ruby SDK
2
2
 
3
- Official Ruby client for [WowSQL](https://wowsql.com) PostgreSQL backend-as-a-service with project auth and object storage.
3
+ The official Ruby SDK for [WowSQL](https://wowsqlconnect.com). Provides a clean, chainable interface for all PostgREST database operations, authentication, file storage, and schema management.
4
4
 
5
- **Gem:** `wowsql-sdk` · **Module:** `WOWSQL` · **Ruby:** 2.6+
5
+ ---
6
6
 
7
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+ ## Table of Contents
8
+
9
+ - [Requirements](#requirements)
10
+ - [Installation](#installation)
11
+ - [Quick Start](#quick-start)
12
+ - [Client Configuration](#client-configuration)
13
+ - [Database Operations](#database-operations)
14
+ - [get — Query Records](#get--query-records)
15
+ - [get_by_id — Fetch by Primary Key](#get_by_id--fetch-by-primary-key)
16
+ - [create / insert — Insert a Record](#create--insert--insert-a-record)
17
+ - [bulk_insert — Insert Multiple Records](#bulk_insert--insert-multiple-records)
18
+ - [upsert — Insert or Update](#upsert--insert-or-update)
19
+ - [update — Update by ID](#update--update-by-id)
20
+ - [delete — Delete by ID](#delete--delete-by-id)
21
+ - [Query Builder](#query-builder)
22
+ - [select — Choose Columns](#select--choose-columns)
23
+ - [Filtering](#filtering)
24
+ - [order_by — Sort Results](#order_by--sort-results)
25
+ - [group_by — Aggregate Groups](#group_by--aggregate-groups)
26
+ - [limit / offset — Pagination](#limit--offset--pagination)
27
+ - [paginate — Page-Based Pagination](#paginate--page-based-pagination)
28
+ - [first — Single Record](#first--single-record)
29
+ - [single — Exactly One Record](#single--exactly-one-record)
30
+ - [count — Total Count](#count--total-count)
31
+ - [sum / avg — Aggregates](#sum--avg--aggregates)
32
+ - [Authentication](#authentication)
33
+ - [sign_up](#sign_up)
34
+ - [sign_in](#sign_in)
35
+ - [get_user](#get_user)
36
+ - [OAuth — Google, GitHub, etc.](#oauth--google-github-etc)
37
+ - [forgot_password / reset_password](#forgot_password--reset_password)
38
+ - [send_otp / verify_otp](#send_otp--verify_otp)
39
+ - [send_magic_link](#send_magic_link)
40
+ - [verify_email / resend_verification](#verify_email--resend_verification)
41
+ - [refresh_token](#refresh_token)
42
+ - [change_password / update_user](#change_password--update_user)
43
+ - [logout](#logout)
44
+ - [File Storage](#file-storage)
45
+ - [create_bucket](#create_bucket)
46
+ - [upload / upload_from_path](#upload--upload_from_path)
47
+ - [list_files / download / delete_file](#list_files--download--delete_file)
48
+ - [get_public_url](#get_public_url)
49
+ - [Schema Management](#schema-management)
50
+ - [create_table](#create_table)
51
+ - [add_column / drop_column / rename_column](#add_column--drop_column--rename_column)
52
+ - [drop_table / execute_sql](#drop_table--execute_sql)
53
+ - [Error Handling](#error-handling)
54
+ - [Response Format](#response-format)
8
55
 
9
56
  ---
10
57
 
11
- ## Table of contents
12
-
13
- 1. [Installation](#installation)
14
- 2. [Quick start](#quick-start)
15
- 3. [Concepts & API keys](#concepts--api-keys)
16
- 4. [Database: `WOWSQLClient`](#database-wowsqlclient)
17
- 5. [Table & `QueryBuilder`](#table--querybuilder)
18
- 6. [Authentication: `ProjectAuthClient`](#authentication-projectauthclient)
19
- 7. [Storage: `WOWSQLStorage`](#storage-wowsqlstorage)
20
- 8. [Schema: `WOWSQLSchema`](#schema-wowsqlschema)
21
- 9. [Models & types](#models--types)
22
- 10. [Exceptions](#exceptions)
23
- 11. [Configuration](#configuration)
24
- 12. [Rails integration](#rails-integration)
25
- 13. [Examples](#examples)
26
- 14. [Troubleshooting](#troubleshooting)
27
- 15. [Links](#links)
58
+ ## Requirements
59
+
60
+ - Ruby 2.7 or higher
61
+ - `faraday` >= 1.0
62
+ - `faraday-multipart` >= 1.0
28
63
 
29
64
  ---
30
65
 
31
66
  ## Installation
32
67
 
33
- ### Gemfile
68
+ Add to your `Gemfile`:
34
69
 
35
70
  ```ruby
36
- gem 'wowsql-sdk', '~> 1.3'
37
- ```
38
-
39
- ```bash
40
- bundle install
71
+ gem 'wowsql-sdk'
41
72
  ```
42
73
 
43
- ### Manual
74
+ Or install directly:
44
75
 
45
76
  ```bash
46
77
  gem install wowsql-sdk
47
78
  ```
48
79
 
49
- ### Require
80
+ ---
81
+
82
+ ## Quick Start
50
83
 
51
84
  ```ruby
52
85
  require 'wowsql'
53
- # or
54
- require 'wowmysql' # legacy alias entry (same library)
86
+
87
+ # Initialize client with your project slug and API key
88
+ client = WOWSQL::WOWSQLClient.new("myproject", "wowsql_anon_...")
89
+
90
+ # Insert a record
91
+ user = client.table("users").create({ email: "alice@example.com", name: "Alice" })
92
+
93
+ # Query with filters
94
+ result = client.table("users")
95
+ .select("id", "email", "name")
96
+ .eq("is_active", true)
97
+ .order_by("created_at", "desc")
98
+ .limit(10)
99
+ .get
100
+
101
+ result["data"].each { |u| puts u["email"] }
102
+
103
+ # Close when done
104
+ client.close
55
105
  ```
56
106
 
57
107
  ---
58
108
 
59
- ## Quick start
60
-
61
- ### Database (CRUD + query builder)
109
+ ## Client Configuration
62
110
 
63
111
  ```ruby
64
- require 'wowsql'
65
-
66
112
  client = WOWSQL::WOWSQLClient.new(
67
- 'https://your-project.wowsql.com',
68
- ENV.fetch('WOWSQL_SERVICE_KEY'),
69
- base_domain: 'wowsql.com',
70
- secure: true,
71
- timeout: 30,
72
- verify_ssl: true
113
+ "myproject", # Project slug, full hostname, or full URL
114
+ "wowsql_anon_...", # Anonymous key (or service role key for privileged ops)
115
+ base_domain: "wowsqlconnect.com", # Default; only needed if self-hosting
116
+ secure: true, # Use HTTPS (default: true)
117
+ timeout: 30, # Request timeout in seconds (default: 30)
118
+ verify_ssl: true # Verify SSL certificates (default: true)
73
119
  )
120
+ ```
74
121
 
75
- rows = client.table('posts')
76
- .select('id', 'title', 'created_at')
77
- .eq('published', true)
78
- .order_by('created_at', 'desc')
79
- .limit(10)
80
- .get
122
+ **project_url formats accepted:**
81
123
 
82
- puts rows['data'].inspect
124
+ | Format | Description |
125
+ |--------|-------------|
126
+ | `"myproject"` | Appends `.wowsqlconnect.com` automatically |
127
+ | `"myproject.wowsqlconnect.com"` | Full hostname |
128
+ | `"https://myproject.wowsqlconnect.com"` | Full URL |
129
+ | `"https://your-self-hosted-domain.com"` | Self-hosted instance |
83
130
 
84
- created = client.table('posts').create(
85
- 'title' => 'Hello',
86
- 'body' => 'World',
87
- 'published' => true
88
- )
89
- puts created['id']
131
+ **API Key types:**
90
132
 
91
- client.close
92
- ```
133
+ | Key Prefix | Purpose |
134
+ |------------|---------|
135
+ | `wowsql_anon_...` | Public / client-side operations |
136
+ | `wowsql_service_...` | Server-side, privileged operations (schema management, admin) |
93
137
 
94
- ### Project authentication
138
+ ---
95
139
 
96
- ```ruby
97
- auth = WOWSQL::ProjectAuthClient.new(
98
- ENV.fetch('WOWSQL_PROJECT_URL'),
99
- ENV.fetch('WOWSQL_ANON_KEY'),
100
- base_domain: 'wowsql.com'
101
- )
140
+ ## Database Operations
102
141
 
103
- res = auth.sign_up(
104
- email: 'user@example.com',
105
- password: 'SecurePass123!',
106
- full_name: 'Ada Lovelace'
107
- )
142
+ ### get — Query Records
108
143
 
109
- auth.set_session(
110
- access_token: res.session.access_token,
111
- refresh_token: res.session.refresh_token
112
- )
144
+ ```ruby
145
+ # All records
146
+ result = client.table("products").get
147
+
148
+ # Chained query
149
+ result = client.table("products")
150
+ .select("id", "name", "price")
151
+ .eq("category", "electronics")
152
+ .order_by("price", "asc")
153
+ .limit(20)
154
+ .offset(0)
155
+ .get
156
+
157
+ puts result["data"] # Array of hashes
158
+ puts result["count"] # Records returned
159
+ puts result["total"] # Total matching records
160
+ puts result["limit"] # Applied limit
161
+ puts result["offset"] # Applied offset
162
+ ```
113
163
 
114
- user = auth.get_user
115
- puts user.email
164
+ ### get_by_id — Fetch by Primary Key
116
165
 
117
- auth.close
166
+ ```ruby
167
+ user = client.table("users").get_by_id("550e8400-e29b-41d4-a716-446655440000")
168
+ puts user["email"]
118
169
  ```
119
170
 
120
- ### Storage (buckets & files)
171
+ ### create / insert — Insert a Record
121
172
 
122
173
  ```ruby
123
- storage = WOWSQL::WOWSQLStorage.new(
124
- ENV.fetch('WOWSQL_PROJECT_URL'),
125
- ENV.fetch('WOWSQL_SERVICE_KEY'),
126
- base_domain: 'wowsql.com',
127
- timeout: 60
128
- )
174
+ product = client.table("products").create({
175
+ name: "Widget Pro",
176
+ price: 29.99,
177
+ category: "tools",
178
+ in_stock: true
179
+ })
180
+ puts product["id"]
181
+ ```
182
+
183
+ `insert` is an alias for `create`.
129
184
 
130
- bucket = storage.create_bucket('avatars', public: true)
131
- storage.upload(bucket.name, File.binread('face.png'), path: 'u/1.png', file_name: '1.png')
132
- puts storage.get_public_url('avatars', 'u/1.png')
185
+ ### bulk_insert Insert Multiple Records
133
186
 
134
- storage.close
187
+ ```ruby
188
+ records = [
189
+ { name: "Item A", price: 10.00 },
190
+ { name: "Item B", price: 20.00 },
191
+ { name: "Item C", price: 30.00 }
192
+ ]
193
+ results = client.table("products").bulk_insert(records)
194
+ puts "Inserted #{results.length} records"
135
195
  ```
136
196
 
137
- ### Schema (service role only)
197
+ ### upsert Insert or Update
138
198
 
139
199
  ```ruby
140
- schema = WOWSQL::WOWSQLSchema.new(
141
- ENV.fetch('WOWSQL_PROJECT_URL'),
142
- ENV.fetch('WOWSQL_SERVICE_KEY')
200
+ # Inserts if not exists; updates if the id already exists
201
+ record = client.table("settings").upsert(
202
+ { id: "user-uuid", theme: "dark", language: "en" },
203
+ on_conflict: "id"
143
204
  )
205
+ ```
144
206
 
145
- schema.create_table(
146
- 'notes',
147
- [
148
- { 'name' => 'id', 'type' => 'SERIAL', 'auto_increment' => true },
149
- { 'name' => 'body', 'type' => 'TEXT', 'nullable' => false }
150
- ],
151
- primary_key: 'id'
207
+ ### update — Update by ID
208
+
209
+ ```ruby
210
+ updated = client.table("users").update(
211
+ "550e8400-e29b-41d4-a716-446655440000",
212
+ { name: "Alice Smith", updated_at: Time.now.iso8601 }
152
213
  )
214
+ puts updated["name"]
215
+ ```
153
216
 
154
- schema.close
217
+ ### delete — Delete by ID
218
+
219
+ ```ruby
220
+ deleted = client.table("users").delete("550e8400-e29b-41d4-a716-446655440000")
155
221
  ```
156
222
 
157
223
  ---
158
224
 
159
- ## Concepts & API keys
160
-
161
- | Key | Prefix | Typical use |
162
- |-----|--------|-------------|
163
- | **Anonymous** | `wowsql_anon_…` | Browser/mobile auth flows, limited DB access |
164
- | **Service role** | `wowsql_service_…` | **Server only** — full DB, storage, schema DDL |
225
+ ## Query Builder
165
226
 
166
- - **`WOWSQLClient`** / **`WOWSQLStorage`**: use anon or service key depending on RLS and server vs client.
167
- - **`WOWSQLSchema`**: **requires service role** (403 otherwise).
168
- - **`ProjectAuthClient`**: usually **anon** on clients; service key only on trusted servers.
227
+ All query builder methods return `self` and are fully chainable. Call `get` at the end to execute.
169
228
 
170
- Store keys in `ENV`, never commit them.
229
+ ### select Choose Columns
171
230
 
172
- **Project URL:** full `https://{slug}.wowsql.com` or just the slug; the client normalizes against `base_domain` and `secure`.
231
+ ```ruby
232
+ # Specific columns
233
+ client.table("users").select("id", "email", "name").get
173
234
 
174
- ---
235
+ # All columns (default)
236
+ client.table("users").get
237
+ ```
175
238
 
176
- ## Database: `WOWSQLClient`
239
+ ### Filtering
240
+
241
+ #### Available operators
242
+
243
+ | Method | PostgREST operator | Description |
244
+ |--------|--------------------|-------------|
245
+ | `eq(col, val)` | `eq` | Equals |
246
+ | `neq(col, val)` | `neq` | Not equals |
247
+ | `gt(col, val)` | `gt` | Greater than |
248
+ | `gte(col, val)` | `gte` | Greater than or equal |
249
+ | `lt(col, val)` | `lt` | Less than |
250
+ | `lte(col, val)` | `lte` | Less than or equal |
251
+ | `like(col, pat)` | `like` | SQL LIKE pattern |
252
+ | `ilike(col, pat)` | `ilike` | Case-insensitive LIKE |
253
+ | `is_null(col)` | `is.null` | Column is NULL |
254
+ | `is_not_null(col)` | `not.is.null` | Column is not NULL |
255
+ | `in_list(col, arr)` | `in.(...)` | Column in list |
256
+ | `not_in(col, arr)` | `not.in.(...)` | Column not in list |
257
+ | `between(col, min, max)` | `gte+lte` | Inclusive range |
258
+ | `not_between(col, min, max)` | `lt+gt` | Outside range |
259
+ | `filter(col, op, val)` | any above | Generic filter |
260
+ | `or_filter(col, op, val)` | OR | OR condition |
177
261
 
178
262
  ```ruby
179
- WOWSQL::WOWSQLClient.new(
180
- project_url,
181
- api_key,
182
- base_domain: 'wowsql.com',
183
- secure: true,
184
- timeout: 30,
185
- verify_ssl: true
186
- )
263
+ # Chained filters (all AND by default)
264
+ result = client.table("orders")
265
+ .gte("total", 100)
266
+ .lte("total", 500)
267
+ .eq("status", "shipped")
268
+ .get
269
+
270
+ # LIKE / ILIKE
271
+ result = client.table("products")
272
+ .ilike("name", "%widget%")
273
+ .get
274
+
275
+ # IN list
276
+ result = client.table("users")
277
+ .in_list("role", ["admin", "manager"])
278
+ .get
279
+
280
+ # NULL check
281
+ result = client.table("users").is_null("deleted_at").get
282
+
283
+ # Date range
284
+ result = client.table("orders")
285
+ .between("created_at", "2025-01-01", "2025-12-31")
286
+ .get
187
287
  ```
188
288
 
189
- | Method | Returns | Description |
190
- |--------|---------|-------------|
191
- | `table(table_name)` | `Table` | Fluent access to one table (`public` schema via API). |
192
- | `list_tables` | `Array<String>` | Table names in the project DB. |
193
- | `get_table_schema(table_name)` | `Hash` | Column metadata from the API. |
194
- | `request(method, path, params, json)` | `Hash` | Low-level JSON request (advanced). |
195
- | `close` | — | Release resources. |
289
+ ### order_by Sort Results
196
290
 
197
- **Reader:** `api_url`, `api_key`, `timeout`, `verify_ssl`.
291
+ ```ruby
292
+ # Single column
293
+ client.table("products").order_by("price", "asc").get
294
+ client.table("products").order_by("created_at", "desc").get
295
+
296
+ # Multiple columns
297
+ client.table("products")
298
+ .order_by("category", "asc")
299
+ .order_by("price", "desc")
300
+ .get
301
+ ```
198
302
 
199
- ---
303
+ ### group_by — Aggregate Groups
200
304
 
201
- ## Table & `QueryBuilder`
305
+ ```ruby
306
+ result = client.table("orders")
307
+ .select("status", "sum(total)", "count(*)")
308
+ .group_by("status")
309
+ .get
310
+ ```
202
311
 
203
- ### `Table`
312
+ ### limit / offset — Pagination
204
313
 
205
- Obtained via `client.table('name')`.
314
+ ```ruby
315
+ result = client.table("products").limit(20).offset(40).get
316
+ ```
206
317
 
207
- | Method | Returns | Notes |
208
- |--------|---------|------|
209
- | `select(*columns)` | `QueryBuilder` | Column list or `'*'`-style strings per your API. |
210
- | `filter(column, operator, value, logical_op: 'AND')` | `QueryBuilder` | See operators below. |
211
- | `get(options = nil)` | `Hash` | Executes SELECT pipeline. |
212
- | `get_by_id(record_id)` | `Hash` | Single row by PK. |
213
- | `create(data)` / `insert(data)` | `Hash` | Insert one row. |
214
- | `bulk_insert(records)` | `Array` | Multiple inserts. |
215
- | `upsert(data, on_conflict: 'id')` | `Hash` | Upsert semantics per backend. |
216
- | `update(record_id, data)` | `Hash` | Update by id. |
217
- | `delete(record_id)` | `Hash` | Delete by id. |
218
- | `eq` / `neq` / `gt` / `gte` / `lt` / `lte` | `QueryBuilder` | Shorthand filters. |
219
- | `order_by(column, direction = 'asc')` | `QueryBuilder` | |
220
- | `count` | `Integer` | |
221
- | `paginate(page: 1, per_page: 20)` | `Hash` | Paginated result envelope. |
318
+ ### paginate Page-Based Pagination
222
319
 
223
- ### `QueryBuilder`
320
+ ```ruby
321
+ result = client.table("products").paginate(page: 3, per_page: 20)
224
322
 
225
- Chainable; ends with `get`, `execute`, `first`, `single`, `count`, or `paginate`.
323
+ puts result["data"] # Array of records
324
+ puts result["page"] # 3
325
+ puts result["per_page"] # 20
326
+ puts result["total"] # Total records matching filters
327
+ puts result["total_pages"] # Total pages
328
+ ```
226
329
 
227
- **Filters:** `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `like`, `is_null`, `is_not_null`, `in_list`, `not_in`, `between`, `not_between`, `or_filter`.
330
+ ### first Single Record
228
331
 
229
- **Grouping / aggregates:** `group_by(*columns)`, `having(column, operator, value)`.
332
+ ```ruby
333
+ user = client.table("users").eq("email", "alice@example.com").first
334
+ puts user["name"] # nil if not found
335
+ ```
230
336
 
231
- **Sort / page:** `order_by`, `order`, `limit`, `offset`.
337
+ ### single Exactly One Record
232
338
 
233
- **Terminal:**
339
+ ```ruby
340
+ begin
341
+ user = client.table("users").eq("email", "alice@example.com").single
342
+ rescue WOWSQL::WOWSQLError => e
343
+ puts "Not found or multiple records: #{e.message}"
344
+ end
345
+ ```
346
+
347
+ ### count — Total Count
234
348
 
235
- | Method | Behavior |
236
- |--------|----------|
237
- | `get` / `execute` | Full result hash (`data`, counts, etc.). |
238
- | `first` | First row or `nil`. |
239
- | `single` | Exactly one row; raises `WOWSQLError` if not one row. |
240
- | `count` | Integer count. |
241
- | `paginate(page:, per_page:)` | Paginated structure. |
349
+ ```ruby
350
+ total = client.table("users").eq("is_active", true).count
351
+ puts "Active users: #{total}"
352
+ ```
242
353
 
243
- ### Filter operators (string)
354
+ ### sum / avg — Aggregates
244
355
 
245
- Use with `filter` or the shorthand methods:
356
+ ```ruby
357
+ total_revenue = client.table("orders").eq("status", "completed").sum("total")
358
+ avg_price = client.table("products").eq("category", "electronics").avg("price")
246
359
 
247
- `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `like`, `is`, `in`, `not_in`, `between`, `not_between`, `is_not` — as supported by the WowSQL REST API for your project.
360
+ puts "Revenue: #{total_revenue}"
361
+ puts "Average price: #{avg_price}"
362
+ ```
248
363
 
249
364
  ---
250
365
 
251
- ## Authentication: `ProjectAuthClient`
366
+ ## Authentication
252
367
 
253
368
  ```ruby
254
- WOWSQL::ProjectAuthClient.new(
255
- project_url,
256
- api_key,
257
- base_domain: 'wowsql.com',
369
+ auth = WOWSQL::ProjectAuthClient.new(
370
+ "myproject",
371
+ "wowsql_anon_...",
372
+ base_domain: "wowsqlconnect.com",
258
373
  secure: true,
259
374
  timeout: 30,
260
375
  verify_ssl: true,
261
- token_storage: nil # optional WOWSQL::MemoryTokenStorage or custom
376
+ token_storage: nil # Optional custom token storage (implements TokenStorage)
262
377
  )
263
378
  ```
264
379
 
265
- ### `TokenStorage` (module)
380
+ ### sign_up
266
381
 
267
- Implement in your app for Redis/DB/session persistence:
382
+ ```ruby
383
+ response = auth.sign_up(
384
+ email: "alice@example.com",
385
+ password: "SecurePass123!",
386
+ full_name: "Alice Smith",
387
+ user_metadata: { plan: "pro" }
388
+ )
268
389
 
269
- - `get_access_token` / `set_access_token(token)`
270
- - `get_refresh_token` / `set_refresh_token(token)`
390
+ puts response.session.access_token
391
+ puts response.user.id
392
+ puts response.user.email
393
+ ```
271
394
 
272
- `WOWSQL::MemoryTokenStorage` is in-memory only.
395
+ ### sign_in
273
396
 
274
- ### Methods
397
+ ```ruby
398
+ response = auth.sign_in(
399
+ email: "alice@example.com",
400
+ password: "SecurePass123!"
401
+ )
275
402
 
276
- | Method | Returns |
277
- |--------|---------|
278
- | `sign_up(email:, password:, full_name:, user_metadata:)` | `AuthResponse` |
279
- | `sign_in(email:, password:)` | `AuthResponse` |
280
- | `get_user(access_token: nil)` | `AuthUser` |
281
- | `get_oauth_authorization_url(provider:, redirect_uri:)` | `Hash` |
282
- | `exchange_oauth_callback(provider:, code:, redirect_uri:)` | `AuthResponse` |
283
- | `forgot_password(email:)` | `Hash` |
284
- | `reset_password(token:, new_password:)` | `Hash` |
285
- | `send_otp(email:, purpose: 'login')` | `Hash` |
286
- | `verify_otp(email:, otp:, purpose: 'login', new_password: nil)` | `AuthResponse` or `Hash` |
287
- | `send_magic_link(email:, purpose: 'login')` | `Hash` |
288
- | `verify_email(token:)` | `Hash` |
289
- | `resend_verification(email:)` | `Hash` |
290
- | `logout(access_token: nil)` | `Hash` |
291
- | `refresh_token(refresh_token: nil)` | `AuthResponse` |
292
- | `change_password(current_password:, new_password:, access_token: nil)` | `Hash` |
293
- | `update_user(full_name:, avatar_url:, username:, user_metadata:, access_token:)` | `AuthUser` |
294
- | `get_session` | `Hash` with token keys |
295
- | `set_session(access_token:, refresh_token: nil)` | |
296
- | `clear_session` | |
297
- | `close` | |
403
+ puts response.session.access_token
404
+ puts response.session.refresh_token
405
+ ```
298
406
 
299
- **Structs:** `AuthUser`, `AuthSession`, `AuthResponse` (see [Models](#models--types)).
407
+ ### get_user
300
408
 
301
- ---
409
+ ```ruby
410
+ user = auth.get_user
411
+ puts user.id
412
+ puts user.email
413
+ puts user.full_name
414
+ puts user.email_verified
415
+ puts user.user_metadata.inspect
416
+ ```
417
+
418
+ ### OAuth — Google, GitHub, etc.
302
419
 
303
- ## Storage: `WOWSQLStorage`
420
+ **Step 1: Get the authorization URL**
304
421
 
305
422
  ```ruby
306
- WOWSQL::WOWSQLStorage.new(
307
- project_url = '',
308
- api_key = '',
309
- project_slug: '',
310
- base_url: '',
311
- base_domain: 'wowsql.com',
312
- secure: true,
313
- timeout: 60,
314
- verify_ssl: true
423
+ oauth = auth.get_oauth_authorization_url(
424
+ provider: "google",
425
+ redirect_uri: "https://myapp.com/auth/callback"
315
426
  )
427
+
428
+ # Redirect the browser to:
429
+ puts oauth["authorization_url"]
316
430
  ```
317
431
 
318
- | Method | Returns |
319
- |--------|---------|
320
- | `create_bucket(name, public: false, file_size_limit: nil, allowed_mime_types: nil)` | `StorageBucket` |
321
- | `list_buckets` | `Array<StorageBucket>` |
322
- | `get_bucket(name)` | `StorageBucket` |
323
- | `update_bucket(name, **options)` | `StorageBucket` |
324
- | `delete_bucket(name)` | `Hash` |
325
- | `upload(bucket_name, file_data, path: nil, file_name: nil)` | `StorageFile` |
326
- | `upload_from_path(file_path, bucket_name: 'default', path: nil)` | `StorageFile` |
327
- | `list_files(bucket_name, prefix: nil, limit: 100, offset: 0)` | `Array<StorageFile>` |
328
- | `download(bucket_name, file_path)` | `String` (binary string) |
329
- | `download_to_file(bucket_name, file_path, local_path)` | `String` (path) |
330
- | `delete_file(bucket_name, file_path)` | `Hash` |
331
- | `get_public_url(bucket_name, file_path)` | `String` |
332
- | `get_stats` | `StorageQuota` |
333
- | `get_quota(force_refresh: false)` | `StorageQuota` |
334
- | `close` | |
335
-
336
- **Value objects:** `StorageBucket`, `StorageFile` (`size_mb`, `size_gb`), `StorageQuota`.
432
+ **Step 2: Exchange the callback code**
337
433
 
338
- ---
434
+ ```ruby
435
+ result = auth.exchange_oauth_callback(
436
+ provider: "google",
437
+ code: params[:code],
438
+ redirect_uri: "https://myapp.com/auth/callback"
439
+ )
339
440
 
340
- ## Schema: `WOWSQLSchema`
441
+ puts result.session.access_token
442
+ puts result.user.email
443
+ ```
341
444
 
342
- **Requires service role key.**
445
+ ### forgot_password / reset_password
343
446
 
344
447
  ```ruby
345
- WOWSQL::WOWSQLSchema.new(
346
- project_url,
347
- service_key,
348
- base_domain: 'wowsql.com',
349
- secure: true,
350
- timeout: 30,
351
- verify_ssl: true
448
+ # Send reset email
449
+ auth.forgot_password(email: "alice@example.com")
450
+
451
+ # Reset with token from email
452
+ auth.reset_password(
453
+ token: "reset_token_from_email",
454
+ new_password: "NewSecurePass456!"
455
+ )
456
+ ```
457
+
458
+ ### send_otp / verify_otp
459
+
460
+ ```ruby
461
+ # Send OTP
462
+ auth.send_otp(email: "alice@example.com", purpose: "login")
463
+
464
+ # Verify OTP
465
+ response = auth.verify_otp(
466
+ email: "alice@example.com",
467
+ otp: "123456",
468
+ purpose: "login"
352
469
  )
470
+ puts response.session.access_token
353
471
  ```
354
472
 
355
- | Method | Returns |
356
- |--------|---------|
357
- | `create_table(table_name, columns, primary_key: nil, indexes: nil)` | `Hash` |
358
- | `alter_table(table_name, operation, column_name: nil, column_type: nil, new_column_name: nil, nullable: true, default: nil)` | `Hash` |
359
- | `drop_table(table_name, cascade: false)` | `Hash` |
360
- | `execute_sql(sql)` | `Hash` |
361
- | `add_column(table_name, column_name, column_type, nullable: true, default: nil)` | `Hash` |
362
- | `drop_column(table_name, column_name)` | `Hash` |
363
- | `rename_column(table_name, old_name, new_name)` | `Hash` |
364
- | `modify_column(table_name, column_name, column_type: nil, nullable: nil, default: nil)` | `Hash` |
365
- | `create_index(table_name, columns, unique: false, name: nil, using: nil)` | `Hash` |
366
- | `list_tables` | `Array<String>` |
367
- | `get_table_schema(table_name)` | `Hash` |
368
- | `close` | |
473
+ Purposes: `"login"`, `"signup"`, `"password_reset"`.
369
474
 
370
- `operation` examples: `add_column`, `drop_column`, `modify_column`, `rename_column` (per API).
475
+ ### send_magic_link
371
476
 
372
- ---
477
+ ```ruby
478
+ auth.send_magic_link(email: "alice@example.com", purpose: "login")
479
+ ```
480
+
481
+ Purposes: `"login"`, `"signup"`, `"email_verification"`.
373
482
 
374
- ## Models & types
483
+ ### verify_email / resend_verification
375
484
 
376
- ### Auth (structs)
485
+ ```ruby
486
+ # Verify email from link token
487
+ result = auth.verify_email(token: "verification_token_from_email")
488
+ puts result["success"]
489
+
490
+ # Resend if needed
491
+ auth.resend_verification(email: "alice@example.com")
492
+ ```
377
493
 
378
- - **`AuthUser`**: `id`, `email`, `full_name`, `avatar_url`, `email_verified`, `user_metadata`, `app_metadata`, `created_at`
379
- - **`AuthSession`**: `access_token`, `refresh_token`, `token_type`, `expires_in`
380
- - **`AuthResponse`**: `session`, `user`
494
+ ### refresh_token
381
495
 
382
- ### Storage
496
+ ```ruby
497
+ response = auth.refresh_token
498
+ puts response.session.access_token
499
+ ```
383
500
 
384
- - **`StorageBucket`**: `id`, `name`, `public`, `file_size_limit`, `allowed_mime_types`, `created_at`, `object_count`, `total_size`
385
- - **`StorageFile`**: `id`, `bucket_id`, `name`, `path`, `mime_type`, `size`, `metadata`, `created_at`, `public_url` — `size_mb`, `size_gb`
386
- - **`StorageQuota`**: `total_files`, `total_size_bytes`, `total_size_gb`, `file_types`
501
+ ### change_password / update_user
502
+
503
+ ```ruby
504
+ # Change password
505
+ auth.change_password(
506
+ current_password: "OldPass123!",
507
+ new_password: "NewPass456!"
508
+ )
509
+
510
+ # Update profile
511
+ user = auth.update_user(
512
+ full_name: "Alice Smith",
513
+ avatar_url: "https://cdn.example.com/avatar.jpg",
514
+ user_metadata: { bio: "Developer" }
515
+ )
516
+ ```
517
+
518
+ ### logout
519
+
520
+ ```ruby
521
+ auth.logout
522
+ ```
387
523
 
388
524
  ---
389
525
 
390
- ## Exceptions
526
+ ## File Storage
391
527
 
392
- | Class | Typical HTTP | When |
393
- |-------|----------------|------|
394
- | `WOWSQLError` | any | Base error; `message`, `status_code`, `response`. |
395
- | `StorageError` | 4xx/5xx | Storage API failures. |
396
- | `StorageLimitExceededError` | 413 | Quota / size limit. |
397
- | `SchemaPermissionError` | 403 | Schema call without service key. |
528
+ ```ruby
529
+ storage = WOWSQL::WOWSQLStorage.new(
530
+ "myproject",
531
+ "wowsql_anon_...",
532
+ base_domain: "wowsqlconnect.com",
533
+ secure: true,
534
+ timeout: 60,
535
+ verify_ssl: true
536
+ )
537
+ ```
398
538
 
399
- Aliases: `WOWSQLException`, `StorageException`, `StorageLimitExceededException`, `PermissionException` → map to the classes above.
539
+ ### create_bucket
400
540
 
401
541
  ```ruby
402
- begin
403
- client.table('x').get
404
- rescue WOWSQL::WOWSQLError => e
405
- warn "#{e.status_code}: #{e.message}"
542
+ bucket = storage.create_bucket(
543
+ "avatars",
544
+ public: true,
545
+ file_size_limit: 5 * 1024 * 1024, # 5 MB
546
+ allowed_mime_types: ["image/jpeg", "image/png"]
547
+ )
548
+ puts bucket.name
549
+ puts bucket.public
550
+ ```
551
+
552
+ ### upload / upload_from_path
553
+
554
+ ```ruby
555
+ # Upload from IO
556
+ File.open("photo.jpg", "rb") do |f|
557
+ file = storage.upload("avatars", f, path: "users/alice.jpg")
558
+ puts file.path
559
+ puts file.size
406
560
  end
561
+
562
+ # Upload from filesystem path
563
+ file = storage.upload_from_path(
564
+ "/local/path/photo.jpg",
565
+ bucket_name: "avatars",
566
+ path: "users/alice.jpg"
567
+ )
407
568
  ```
408
569
 
409
- ---
570
+ ### list_files / download / delete_file
410
571
 
411
- ## Configuration
572
+ ```ruby
573
+ # List files
574
+ files = storage.list_files("avatars", prefix: "users/", limit: 50)
575
+ files.each { |f| puts "#{f.path} (#{f.size_mb.round(2)} MB)" }
412
576
 
413
- | Option | Default | Notes |
414
- |--------|---------|------|
415
- | `base_domain` | `wowsql.com` | Custom cloud domain if applicable. |
416
- | `secure` | `true` | Use HTTPS. |
417
- | `timeout` | 30 (DB/auth/schema), 60 (storage) | Seconds. |
418
- | `verify_ssl` | `true` | Set `false` only for local dev with self-signed certs. |
577
+ # Download to memory
578
+ content = storage.download("avatars", "users/alice.jpg")
419
579
 
420
- ---
580
+ # Download to disk
581
+ storage.download_to_file("avatars", "users/alice.jpg", "/local/alice.jpg")
421
582
 
422
- ## Rails integration
583
+ # Delete
584
+ storage.delete_file("avatars", "users/alice.jpg")
585
+ ```
423
586
 
424
- **config/initializers/wowsql.rb**
587
+ ### get_public_url
425
588
 
426
589
  ```ruby
427
- # frozen_string_literal: true
590
+ url = storage.get_public_url("avatars", "users/alice.jpg")
591
+ puts url # https://myproject.wowsqlconnect.com/api/v1/storage/...
592
+ ```
593
+
594
+ ---
428
595
 
429
- WOWSQL_CLIENT = WOWSQL::WOWSQLClient.new(
430
- ENV.fetch('WOWSQL_PROJECT_URL'),
431
- ENV.fetch('WOWSQL_SERVICE_KEY')
596
+ ## Schema Management
597
+
598
+ Schema operations require a **service role key** (`wowsql_service_...`).
599
+
600
+ ```ruby
601
+ schema = WOWSQL::WOWSQLSchema.new(
602
+ "myproject",
603
+ "wowsql_service_...",
604
+ base_domain: "wowsqlconnect.com",
605
+ secure: true
432
606
  )
607
+ ```
608
+
609
+ ### create_table
433
610
 
434
- WOWSQL_AUTH = WOWSQL::ProjectAuthClient.new(
435
- ENV.fetch('WOWSQL_PROJECT_URL'),
436
- ENV.fetch('WOWSQL_ANON_KEY')
611
+ ```ruby
612
+ schema.create_table(
613
+ "products",
614
+ [
615
+ { "name" => "id", "type" => "UUID", "auto_increment" => true },
616
+ { "name" => "name", "type" => "VARCHAR(255)", "nullable" => false },
617
+ { "name" => "price", "type" => "DECIMAL(10,2)", "nullable" => false },
618
+ { "name" => "category", "type" => "VARCHAR(100)" },
619
+ { "name" => "metadata", "type" => "JSONB", "default" => "'{}'" },
620
+ { "name" => "created_at", "type" => "TIMESTAMPTZ", "default" => "CURRENT_TIMESTAMP" }
621
+ ],
622
+ primary_key: "id",
623
+ indexes: ["category", "name"]
437
624
  )
438
625
  ```
439
626
 
440
- Use **service key** only in server-side code (jobs, controllers that must bypass restrictions). Prefer **anon** for end-user auth flows.
627
+ ### add_column / drop_column / rename_column
628
+
629
+ ```ruby
630
+ # Add
631
+ schema.add_column("products", "sku", "VARCHAR(50)", nullable: true)
632
+
633
+ # Drop
634
+ schema.drop_column("products", "old_field")
635
+
636
+ # Rename
637
+ schema.rename_column("products", "sku", "product_sku")
638
+
639
+ # Modify type / nullability / default
640
+ schema.modify_column("products", "price", column_type: "NUMERIC(12,2)", nullable: false)
641
+ ```
642
+
643
+ ### drop_table / execute_sql
644
+
645
+ ```ruby
646
+ # Drop table (irreversible)
647
+ schema.drop_table("products", cascade: false)
648
+
649
+ # Execute raw DDL SQL
650
+ schema.execute_sql("CREATE INDEX idx_products_category ON products (category)")
651
+ schema.execute_sql("ALTER TABLE users ADD COLUMN last_login TIMESTAMPTZ")
652
+ ```
441
653
 
442
654
  ---
443
655
 
444
- ## Examples
656
+ ## Error Handling
445
657
 
446
- ### Blog: posts and comments
658
+ All SDK errors are subclasses of `WOWSQL::WOWSQLError`.
447
659
 
448
660
  ```ruby
449
- posts = WOWSQL_CLIENT.table('posts')
450
- .select('id', 'title')
451
- .eq('published', true)
452
- .order_by('created_at', 'desc')
453
- .limit(20)
454
- .get
661
+ begin
662
+ result = client.table("orders").get_by_id("some-id")
663
+ rescue WOWSQL::WOWSQLError => e
664
+ puts e.message # Human-readable error message
665
+ puts e.status_code # HTTP status code (e.g., 400, 401, 403, 404, 500)
666
+ puts e.response # Raw response body hash
667
+ end
668
+ ```
669
+
670
+ **Storage-specific errors:**
455
671
 
456
- posts['data'].each do |p|
457
- puts p['title']
672
+ ```ruby
673
+ begin
674
+ storage.upload("avatars", large_file, path: "big.mov")
675
+ rescue WOWSQL::StorageLimitExceededError => e
676
+ puts "File too large: #{e.message}"
677
+ rescue WOWSQL::StorageError => e
678
+ puts "Storage error: #{e.message}"
458
679
  end
459
680
  ```
460
681
 
461
- ### Upload avatar then save URL in `public` table
682
+ **Schema-specific errors:**
462
683
 
463
684
  ```ruby
464
- storage = WOWSQL::WOWSQLStorage.new(ENV['WOWSQL_PROJECT_URL'], ENV['WOWSQL_SERVICE_KEY'])
465
- path = "avatars/#{user_id}.jpg"
466
- storage.upload('default', File.binread(local_path), path: path, file_name: 'avatar.jpg')
467
- url = storage.get_public_url('default', path)
468
- WOWSQL_CLIENT.table('profiles').update(user_id, 'avatar_url' => url)
469
- storage.close
685
+ begin
686
+ schema.drop_table("important_table")
687
+ rescue WOWSQL::SchemaPermissionError => e
688
+ puts "Permission denied — use a service role key"
689
+ rescue WOWSQL::WOWSQLError => e
690
+ puts "Schema error: #{e.message}"
691
+ end
470
692
  ```
471
693
 
472
694
  ---
473
695
 
474
- ## Troubleshooting
696
+ ## Response Format
475
697
 
476
- | Issue | Check |
477
- |-------|------|
478
- | `cannot load such file -- faraday/multipart` | Install **`faraday-multipart`** (`gem install faraday-multipart`) or upgrade to **wowsql-sdk ≥ 3.0.1**, which declares this dependency. Faraday 2 moved multipart into that gem. |
479
- | 401 Invalid API key | Key matches project; no extra spaces; key active in dashboard. |
480
- | 403 Schema | Using **service role** for `WOWSQLSchema`. |
481
- | 413 Storage | `StorageLimitExceededError` — plan / quota / object size. |
482
- | SSL errors | `verify_ssl: false` temporarily on dev only. |
698
+ All `get` and query builder calls return a consistent hash:
483
699
 
484
- ---
700
+ ```ruby
701
+ {
702
+ "data" => [...], # Array of record hashes
703
+ "count" => 10, # Number of records in this response
704
+ "total" => 120, # Total matching records (from Content-Range)
705
+ "limit" => 20, # Applied limit
706
+ "offset" => 0 # Applied offset
707
+ }
708
+ ```
709
+
710
+ Single-record operations (`create`, `update`, `delete`, `get_by_id`, `upsert`) return a plain `Hash` representing the record.
485
711
 
486
- ## Links
712
+ Pagination (`paginate`) returns:
487
713
 
488
- - [WowSQL Docs](https://wowsql.com/docs)
489
- - [Dashboard](https://wowsql.com)
490
- - [Support](mailto:support@wowsql.com)
714
+ ```ruby
715
+ {
716
+ "data" => [...],
717
+ "page" => 2,
718
+ "per_page" => 20,
719
+ "total" => 120,
720
+ "total_pages" => 6
721
+ }
722
+ ```
491
723
 
492
724
  ---
493
725
 
494
- **License:** MIT — see included `LICENSE`.
726
+ ## License
495
727
 
496
- *WowSQL Team*
728
+ MIT License — see [LICENSE](LICENSE) for details.