wowsql-sdk 3.0.1 → 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.
- checksums.yaml +4 -4
- data/README.md +557 -325
- data/lib/wowsql/auth.rb +2 -2
- data/lib/wowsql/client.rb +60 -60
- data/lib/wowsql/query_builder.rb +197 -226
- data/lib/wowsql/schema.rb +22 -5
- data/lib/wowsql/storage.rb +2 -2
- data/lib/wowsql/table.rb +123 -110
- metadata +1 -1
data/README.md
CHANGED
|
@@ -1,496 +1,728 @@
|
|
|
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, 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
|
+
- [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
|
-
##
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
68
|
+
Add to your `Gemfile`:
|
|
34
69
|
|
|
35
70
|
```ruby
|
|
36
|
-
gem 'wowsql-sdk'
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
```bash
|
|
40
|
-
bundle install
|
|
71
|
+
gem 'wowsql-sdk'
|
|
41
72
|
```
|
|
42
73
|
|
|
43
|
-
|
|
74
|
+
Or install directly:
|
|
44
75
|
|
|
45
76
|
```bash
|
|
46
77
|
gem install wowsql-sdk
|
|
47
78
|
```
|
|
48
79
|
|
|
49
|
-
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Quick Start
|
|
50
83
|
|
|
51
84
|
```ruby
|
|
52
85
|
require 'wowsql'
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
base_domain:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
85
|
-
'title' => 'Hello',
|
|
86
|
-
'body' => 'World',
|
|
87
|
-
'published' => true
|
|
88
|
-
)
|
|
89
|
-
puts created['id']
|
|
131
|
+
**API Key types:**
|
|
90
132
|
|
|
91
|
-
|
|
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
|
-
|
|
138
|
+
---
|
|
95
139
|
|
|
96
|
-
|
|
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
|
-
|
|
104
|
-
email: 'user@example.com',
|
|
105
|
-
password: 'SecurePass123!',
|
|
106
|
-
full_name: 'Ada Lovelace'
|
|
107
|
-
)
|
|
142
|
+
### get — Query Records
|
|
108
143
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
115
|
-
puts user.email
|
|
164
|
+
### get_by_id — Fetch by Primary Key
|
|
116
165
|
|
|
117
|
-
|
|
166
|
+
```ruby
|
|
167
|
+
user = client.table("users").get_by_id("550e8400-e29b-41d4-a716-446655440000")
|
|
168
|
+
puts user["email"]
|
|
118
169
|
```
|
|
119
170
|
|
|
120
|
-
###
|
|
171
|
+
### create / insert — Insert a Record
|
|
121
172
|
|
|
122
173
|
```ruby
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
197
|
+
### upsert — Insert or Update
|
|
138
198
|
|
|
139
199
|
```ruby
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
229
|
+
### select — Choose Columns
|
|
171
230
|
|
|
172
|
-
|
|
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
|
-
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
305
|
+
```ruby
|
|
306
|
+
result = client.table("orders")
|
|
307
|
+
.select("status", "sum(total)", "count(*)")
|
|
308
|
+
.group_by("status")
|
|
309
|
+
.get
|
|
310
|
+
```
|
|
202
311
|
|
|
203
|
-
###
|
|
312
|
+
### limit / offset — Pagination
|
|
204
313
|
|
|
205
|
-
|
|
314
|
+
```ruby
|
|
315
|
+
result = client.table("products").limit(20).offset(40).get
|
|
316
|
+
```
|
|
206
317
|
|
|
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. |
|
|
318
|
+
### paginate — Page-Based Pagination
|
|
222
319
|
|
|
223
|
-
|
|
320
|
+
```ruby
|
|
321
|
+
result = client.table("products").paginate(page: 3, per_page: 20)
|
|
224
322
|
|
|
225
|
-
|
|
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
|
-
|
|
330
|
+
### first — Single Record
|
|
228
331
|
|
|
229
|
-
|
|
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
|
-
|
|
337
|
+
### single — Exactly One Record
|
|
232
338
|
|
|
233
|
-
|
|
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
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
###
|
|
354
|
+
### sum / avg — Aggregates
|
|
244
355
|
|
|
245
|
-
|
|
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
|
-
|
|
360
|
+
puts "Revenue: #{total_revenue}"
|
|
361
|
+
puts "Average price: #{avg_price}"
|
|
362
|
+
```
|
|
248
363
|
|
|
249
364
|
---
|
|
250
365
|
|
|
251
|
-
## Authentication
|
|
366
|
+
## Authentication
|
|
252
367
|
|
|
253
368
|
```ruby
|
|
254
|
-
WOWSQL::ProjectAuthClient.new(
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
base_domain:
|
|
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
|
|
376
|
+
token_storage: nil # Optional custom token storage (implements TokenStorage)
|
|
262
377
|
)
|
|
263
378
|
```
|
|
264
379
|
|
|
265
|
-
###
|
|
380
|
+
### sign_up
|
|
266
381
|
|
|
267
|
-
|
|
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
|
-
|
|
270
|
-
|
|
390
|
+
puts response.session.access_token
|
|
391
|
+
puts response.user.id
|
|
392
|
+
puts response.user.email
|
|
393
|
+
```
|
|
271
394
|
|
|
272
|
-
|
|
395
|
+
### sign_in
|
|
273
396
|
|
|
274
|
-
|
|
397
|
+
```ruby
|
|
398
|
+
response = auth.sign_in(
|
|
399
|
+
email: "alice@example.com",
|
|
400
|
+
password: "SecurePass123!"
|
|
401
|
+
)
|
|
275
402
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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
|
-
|
|
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
|
-
|
|
420
|
+
**Step 1: Get the authorization URL**
|
|
304
421
|
|
|
305
422
|
```ruby
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
-
|
|
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
|
-
|
|
441
|
+
puts result.session.access_token
|
|
442
|
+
puts result.user.email
|
|
443
|
+
```
|
|
341
444
|
|
|
342
|
-
|
|
445
|
+
### forgot_password / reset_password
|
|
343
446
|
|
|
344
447
|
```ruby
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
483
|
+
### verify_email / resend_verification
|
|
375
484
|
|
|
376
|
-
|
|
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
|
-
|
|
379
|
-
- **`AuthSession`**: `access_token`, `refresh_token`, `token_type`, `expires_in`
|
|
380
|
-
- **`AuthResponse`**: `session`, `user`
|
|
494
|
+
### refresh_token
|
|
381
495
|
|
|
382
|
-
|
|
496
|
+
```ruby
|
|
497
|
+
response = auth.refresh_token
|
|
498
|
+
puts response.session.access_token
|
|
499
|
+
```
|
|
383
500
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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
|
-
##
|
|
526
|
+
## File Storage
|
|
391
527
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
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
|
-
|
|
539
|
+
### create_bucket
|
|
400
540
|
|
|
401
541
|
```ruby
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
583
|
+
# Delete
|
|
584
|
+
storage.delete_file("avatars", "users/alice.jpg")
|
|
585
|
+
```
|
|
423
586
|
|
|
424
|
-
|
|
587
|
+
### get_public_url
|
|
425
588
|
|
|
426
589
|
```ruby
|
|
427
|
-
|
|
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
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
656
|
+
## Error Handling
|
|
445
657
|
|
|
446
|
-
|
|
658
|
+
All SDK errors are subclasses of `WOWSQL::WOWSQLError`.
|
|
447
659
|
|
|
448
660
|
```ruby
|
|
449
|
-
|
|
450
|
-
.
|
|
451
|
-
|
|
452
|
-
.
|
|
453
|
-
.
|
|
454
|
-
.
|
|
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
|
-
|
|
457
|
-
|
|
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
|
-
|
|
682
|
+
**Schema-specific errors:**
|
|
462
683
|
|
|
463
684
|
```ruby
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
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
|
-
##
|
|
696
|
+
## Response Format
|
|
475
697
|
|
|
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. |
|
|
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
|
-
|
|
712
|
+
Pagination (`paginate`) returns:
|
|
487
713
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
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
|
-
|
|
726
|
+
## License
|
|
495
727
|
|
|
496
|
-
|
|
728
|
+
MIT License — see [LICENSE](LICENSE) for details.
|