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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +20 -0
- data/LICENSE +81 -17
- data/README.md +587 -322
- data/lib/wowsql/auth.rb +2 -2
- data/lib/wowsql/client.rb +62 -58
- data/lib/wowsql/query_builder.rb +197 -226
- data/lib/wowsql/realtime.rb +201 -0
- data/lib/wowsql/schema.rb +2 -2
- data/lib/wowsql/storage.rb +2 -2
- data/lib/wowsql/table.rb +123 -110
- data/lib/wowsql/version.rb +1 -1
- data/lib/wowsql.rb +1 -0
- metadata +17 -1
data/README.md
CHANGED
|
@@ -1,496 +1,761 @@
|
|
|
1
1
|
# WowSQL Ruby SDK
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
5
|
+
---
|
|
6
6
|
|
|
7
|
-
|
|
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
|
-
##
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
70
|
+
Add to your `Gemfile`:
|
|
34
71
|
|
|
35
72
|
```ruby
|
|
36
|
-
gem 'wowsql-sdk'
|
|
73
|
+
gem 'wowsql-sdk'
|
|
37
74
|
```
|
|
38
75
|
|
|
39
|
-
|
|
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
|
-
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Quick Start
|
|
50
85
|
|
|
51
86
|
```ruby
|
|
52
87
|
require 'wowsql'
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
base_domain:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
85
|
-
'title' => 'Hello',
|
|
86
|
-
'body' => 'World',
|
|
87
|
-
'published' => true
|
|
88
|
-
)
|
|
89
|
-
puts created['id']
|
|
133
|
+
**API Key types:**
|
|
90
134
|
|
|
91
|
-
|
|
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
|
-
###
|
|
166
|
+
### get_by_id — Fetch by Primary Key
|
|
95
167
|
|
|
96
168
|
```ruby
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
115
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
305
|
+
### group_by — Aggregate Groups
|
|
138
306
|
|
|
139
307
|
```ruby
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
308
|
+
result = client.table("orders")
|
|
309
|
+
.select("status", "sum(total)", "count(*)")
|
|
310
|
+
.group_by("status")
|
|
311
|
+
.get
|
|
312
|
+
```
|
|
144
313
|
|
|
145
|
-
|
|
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
|
-
|
|
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
|
-
|
|
322
|
+
```ruby
|
|
323
|
+
result = client.table("products").paginate(page: 3, per_page: 20)
|
|
160
324
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
368
|
+
## Authentication
|
|
177
369
|
|
|
178
370
|
```ruby
|
|
179
|
-
WOWSQL::
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
base_domain:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
397
|
+
### sign_in
|
|
202
398
|
|
|
203
|
-
|
|
399
|
+
```ruby
|
|
400
|
+
response = auth.sign_in(
|
|
401
|
+
email: "alice@example.com",
|
|
402
|
+
password: "SecurePass123!"
|
|
403
|
+
)
|
|
204
404
|
|
|
205
|
-
|
|
405
|
+
puts response.session.access_token
|
|
406
|
+
puts response.session.refresh_token
|
|
407
|
+
```
|
|
206
408
|
|
|
207
|
-
|
|
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
|
-
|
|
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
|
-
|
|
420
|
+
### OAuth — Google, GitHub, etc.
|
|
226
421
|
|
|
227
|
-
**
|
|
422
|
+
**Step 1: Get the authorization URL**
|
|
228
423
|
|
|
229
|
-
|
|
424
|
+
```ruby
|
|
425
|
+
oauth = auth.get_oauth_authorization_url(
|
|
426
|
+
provider: "google",
|
|
427
|
+
redirect_uri: "https://myapp.com/auth/callback"
|
|
428
|
+
)
|
|
230
429
|
|
|
231
|
-
|
|
430
|
+
# Redirect the browser to:
|
|
431
|
+
puts oauth["authorization_url"]
|
|
432
|
+
```
|
|
232
433
|
|
|
233
|
-
**
|
|
434
|
+
**Step 2: Exchange the callback code**
|
|
234
435
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
|
|
443
|
+
puts result.session.access_token
|
|
444
|
+
puts result.user.email
|
|
445
|
+
```
|
|
244
446
|
|
|
245
|
-
|
|
447
|
+
### forgot_password / reset_password
|
|
246
448
|
|
|
247
|
-
|
|
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
|
-
|
|
460
|
+
### send_otp / verify_otp
|
|
252
461
|
|
|
253
462
|
```ruby
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
-
|
|
475
|
+
Purposes: `"login"`, `"signup"`, `"password_reset"`.
|
|
266
476
|
|
|
267
|
-
|
|
477
|
+
### send_magic_link
|
|
268
478
|
|
|
269
|
-
|
|
270
|
-
|
|
479
|
+
```ruby
|
|
480
|
+
auth.send_magic_link(email: "alice@example.com", purpose: "login")
|
|
481
|
+
```
|
|
271
482
|
|
|
272
|
-
`
|
|
483
|
+
Purposes: `"login"`, `"signup"`, `"email_verification"`.
|
|
273
484
|
|
|
274
|
-
###
|
|
485
|
+
### verify_email / resend_verification
|
|
275
486
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
-
|
|
492
|
+
# Resend if needed
|
|
493
|
+
auth.resend_verification(email: "alice@example.com")
|
|
494
|
+
```
|
|
300
495
|
|
|
301
|
-
|
|
496
|
+
### refresh_token
|
|
302
497
|
|
|
303
|
-
|
|
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
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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
|
-
|
|
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
|
-
|
|
526
|
+
---
|
|
341
527
|
|
|
342
|
-
|
|
528
|
+
## File Storage
|
|
343
529
|
|
|
344
530
|
```ruby
|
|
345
|
-
WOWSQL::
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
base_domain:
|
|
531
|
+
storage = WOWSQL::WOWSQLStorage.new(
|
|
532
|
+
"myproject",
|
|
533
|
+
"wowsql_anon_...",
|
|
534
|
+
base_domain: "wowsqlconnect.com",
|
|
349
535
|
secure: true,
|
|
350
|
-
timeout:
|
|
536
|
+
timeout: 60,
|
|
351
537
|
verify_ssl: true
|
|
352
538
|
)
|
|
353
539
|
```
|
|
354
540
|
|
|
355
|
-
|
|
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
|
-
|
|
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
|
-
|
|
572
|
+
### list_files / download / delete_file
|
|
375
573
|
|
|
376
|
-
|
|
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
|
-
|
|
379
|
-
|
|
380
|
-
- **`AuthResponse`**: `session`, `user`
|
|
582
|
+
# Download to disk
|
|
583
|
+
storage.download_to_file("avatars", "users/alice.jpg", "/local/alice.jpg")
|
|
381
584
|
|
|
382
|
-
|
|
585
|
+
# Delete
|
|
586
|
+
storage.delete_file("avatars", "users/alice.jpg")
|
|
587
|
+
```
|
|
383
588
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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
|
-
##
|
|
598
|
+
## Realtime
|
|
391
599
|
|
|
392
|
-
|
|
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
|
-
|
|
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
|
-
|
|
403
|
-
|
|
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
|
-
##
|
|
629
|
+
## Schema Management
|
|
412
630
|
|
|
413
|
-
|
|
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
|
-
|
|
642
|
+
### create_table
|
|
423
643
|
|
|
424
|
-
|
|
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
|
-
#
|
|
663
|
+
# Add
|
|
664
|
+
schema.add_column("products", "sku", "VARCHAR(50)", nullable: true)
|
|
428
665
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
ENV.fetch('WOWSQL_SERVICE_KEY')
|
|
432
|
-
)
|
|
666
|
+
# Drop
|
|
667
|
+
schema.drop_column("products", "old_field")
|
|
433
668
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
689
|
+
## Error Handling
|
|
445
690
|
|
|
446
|
-
|
|
691
|
+
All SDK errors are subclasses of `WOWSQL::WOWSQLError`.
|
|
447
692
|
|
|
448
693
|
```ruby
|
|
449
|
-
|
|
450
|
-
.
|
|
451
|
-
|
|
452
|
-
.
|
|
453
|
-
.
|
|
454
|
-
.
|
|
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
|
-
|
|
457
|
-
|
|
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
|
-
|
|
715
|
+
**Schema-specific errors:**
|
|
462
716
|
|
|
463
717
|
```ruby
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
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
|
-
##
|
|
729
|
+
## Response Format
|
|
475
730
|
|
|
476
|
-
|
|
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
|
-
|
|
743
|
+
Single-record operations (`create`, `update`, `delete`, `get_by_id`, `upsert`) return a plain `Hash` representing the record.
|
|
487
744
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
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
|
-
|
|
759
|
+
## License
|
|
495
760
|
|
|
496
|
-
|
|
761
|
+
MIT License — see [LICENSE](LICENSE) for details.
|