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