wowsql-sdk 1.3.0 โ 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +359 -417
- data/lib/wowmysql.rb +3 -2
- data/lib/wowsql/auth.rb +313 -164
- data/lib/wowsql/client.rb +54 -25
- data/lib/wowsql/exceptions.rb +15 -14
- data/lib/wowsql/query_builder.rb +229 -120
- data/lib/wowsql/schema.rb +250 -0
- data/lib/wowsql/storage.rb +327 -176
- data/lib/wowsql/table.rb +111 -10
- data/lib/wowsql/version.rb +1 -1
- data/lib/wowsql.rb +2 -2
- metadata +20 -5
data/README.md
CHANGED
|
@@ -1,554 +1,496 @@
|
|
|
1
|
-
#
|
|
1
|
+
# WowSQL Ruby SDK
|
|
2
2
|
|
|
3
|
-
Official Ruby client for [
|
|
3
|
+
Official Ruby client for [WowSQL](https://wowsql.com) โ PostgreSQL backend-as-a-service with project auth and object storage.
|
|
4
|
+
|
|
5
|
+
**Gem:** `wowsql-sdk` ยท **Module:** `WOWSQL` ยท **Ruby:** 2.6+
|
|
4
6
|
|
|
5
|
-
[](https://rubygems.org/gems/wowsql-sdk)
|
|
6
7
|
[](https://opensource.org/licenses/MIT)
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
9
|
+
---
|
|
10
|
+
|
|
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)
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
30
32
|
|
|
31
33
|
### Gemfile
|
|
32
34
|
|
|
33
35
|
```ruby
|
|
34
|
-
gem 'wowsql-sdk', '~> 1.
|
|
36
|
+
gem 'wowsql-sdk', '~> 1.3'
|
|
35
37
|
```
|
|
36
38
|
|
|
37
|
-
Then run:
|
|
38
|
-
|
|
39
39
|
```bash
|
|
40
40
|
bundle install
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
-
### Manual
|
|
43
|
+
### Manual
|
|
44
44
|
|
|
45
45
|
```bash
|
|
46
46
|
gem install wowsql-sdk
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
### Require
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
```ruby
|
|
52
|
+
require 'wowsql'
|
|
53
|
+
# or
|
|
54
|
+
require 'wowmysql' # legacy alias entry (same library)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Quick start
|
|
60
|
+
|
|
61
|
+
### Database (CRUD + query builder)
|
|
52
62
|
|
|
53
63
|
```ruby
|
|
54
64
|
require 'wowsql'
|
|
55
65
|
|
|
56
|
-
# Initialize client
|
|
57
66
|
client = WOWSQL::WOWSQLClient.new(
|
|
58
67
|
'https://your-project.wowsql.com',
|
|
59
|
-
'
|
|
68
|
+
ENV.fetch('WOWSQL_SERVICE_KEY'),
|
|
69
|
+
base_domain: 'wowsql.com',
|
|
70
|
+
secure: true,
|
|
71
|
+
timeout: 30,
|
|
72
|
+
verify_ssl: true
|
|
60
73
|
)
|
|
61
74
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
.
|
|
65
|
-
.
|
|
75
|
+
rows = client.table('posts')
|
|
76
|
+
.select('id', 'title', 'created_at')
|
|
77
|
+
.eq('published', true)
|
|
78
|
+
.order_by('created_at', 'desc')
|
|
66
79
|
.limit(10)
|
|
67
80
|
.get
|
|
68
81
|
|
|
69
|
-
|
|
70
|
-
puts "#{user['name']} (#{user['email']})"
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
# Insert data
|
|
74
|
-
new_user = {
|
|
75
|
-
'name' => 'Jane Doe',
|
|
76
|
-
'email' => 'jane@example.com',
|
|
77
|
-
'age' => 25
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
result = client.table('users').create(new_user)
|
|
81
|
-
puts "Created user with ID: #{result['id']}"
|
|
82
|
+
puts rows['data'].inspect
|
|
82
83
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
84
|
+
created = client.table('posts').create(
|
|
85
|
+
'title' => 'Hello',
|
|
86
|
+
'body' => 'World',
|
|
87
|
+
'published' => true
|
|
88
|
+
)
|
|
89
|
+
puts created['id']
|
|
86
90
|
|
|
87
|
-
|
|
88
|
-
client.table('users').delete(result['id'])
|
|
91
|
+
client.close
|
|
89
92
|
```
|
|
90
93
|
|
|
91
|
-
###
|
|
94
|
+
### Project authentication
|
|
92
95
|
|
|
93
96
|
```ruby
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
'your-project-slug',
|
|
99
|
-
'your-api-key'
|
|
97
|
+
auth = WOWSQL::ProjectAuthClient.new(
|
|
98
|
+
ENV.fetch('WOWSQL_PROJECT_URL'),
|
|
99
|
+
ENV.fetch('WOWSQL_ANON_KEY'),
|
|
100
|
+
base_domain: 'wowsql.com'
|
|
100
101
|
)
|
|
101
102
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
puts "File URL: #{url_data['file_url']}"
|
|
103
|
+
res = auth.sign_up(
|
|
104
|
+
email: 'user@example.com',
|
|
105
|
+
password: 'SecurePass123!',
|
|
106
|
+
full_name: 'Ada Lovelace'
|
|
107
|
+
)
|
|
108
108
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
end
|
|
109
|
+
auth.set_session(
|
|
110
|
+
access_token: res.session.access_token,
|
|
111
|
+
refresh_token: res.session.refresh_token
|
|
112
|
+
)
|
|
114
113
|
|
|
115
|
-
|
|
116
|
-
|
|
114
|
+
user = auth.get_user
|
|
115
|
+
puts user.email
|
|
117
116
|
|
|
118
|
-
|
|
119
|
-
quota = storage.get_quota
|
|
120
|
-
puts "Used: #{quota['used_gb']} GB"
|
|
121
|
-
puts "Available: #{quota['available_gb']} GB"
|
|
117
|
+
auth.close
|
|
122
118
|
```
|
|
123
119
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
### Select Queries
|
|
120
|
+
### Storage (buckets & files)
|
|
127
121
|
|
|
128
122
|
```ruby
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
.get
|
|
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
|
+
)
|
|
136
129
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
.eq('status', 'active')
|
|
141
|
-
.gt('age', 18)
|
|
142
|
-
.get
|
|
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')
|
|
143
133
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
.select('*')
|
|
147
|
-
.order_by('created_at', desc: true)
|
|
148
|
-
.limit(10)
|
|
149
|
-
.get
|
|
134
|
+
storage.close
|
|
135
|
+
```
|
|
150
136
|
|
|
151
|
-
|
|
152
|
-
page1 = client.table('users')
|
|
153
|
-
.select('*')
|
|
154
|
-
.limit(20)
|
|
155
|
-
.offset(0)
|
|
156
|
-
.get
|
|
137
|
+
### Schema (service role only)
|
|
157
138
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
.
|
|
161
|
-
.
|
|
162
|
-
|
|
139
|
+
```ruby
|
|
140
|
+
schema = WOWSQL::WOWSQLSchema.new(
|
|
141
|
+
ENV.fetch('WOWSQL_PROJECT_URL'),
|
|
142
|
+
ENV.fetch('WOWSQL_SERVICE_KEY')
|
|
143
|
+
)
|
|
163
144
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
+
)
|
|
169
153
|
|
|
170
|
-
|
|
171
|
-
user = client.table('users').get_by_id(123)
|
|
154
|
+
schema.close
|
|
172
155
|
```
|
|
173
156
|
|
|
174
|
-
|
|
157
|
+
---
|
|
175
158
|
|
|
176
|
-
|
|
177
|
-
# Insert single record
|
|
178
|
-
new_user = {
|
|
179
|
-
'name' => 'John Doe',
|
|
180
|
-
'email' => 'john@example.com',
|
|
181
|
-
'age' => 30,
|
|
182
|
-
'status' => 'active'
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
result = client.table('users').create(new_user)
|
|
186
|
-
puts "New user ID: #{result['id']}"
|
|
187
|
-
```
|
|
159
|
+
## Concepts & API keys
|
|
188
160
|
|
|
189
|
-
|
|
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 |
|
|
190
165
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
'name' => 'Jane Smith',
|
|
195
|
-
'age' => 26
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
result = client.table('users').update(1, updates)
|
|
199
|
-
puts "Updated #{result['affected_rows']} row(s)"
|
|
200
|
-
```
|
|
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.
|
|
201
169
|
|
|
202
|
-
|
|
170
|
+
Store keys in `ENV`, never commit them.
|
|
203
171
|
|
|
204
|
-
|
|
205
|
-
# Delete by ID
|
|
206
|
-
result = client.table('users').delete(1)
|
|
207
|
-
puts "Deleted #{result['affected_rows']} row(s)"
|
|
208
|
-
|
|
209
|
-
# Delete with conditions
|
|
210
|
-
deleted = client.table('users')
|
|
211
|
-
.eq('status', 'deleted')
|
|
212
|
-
.delete
|
|
213
|
-
```
|
|
172
|
+
**Project URL:** full `https://{slug}.wowsql.com` or just the slug; the client normalizes against `base_domain` and `secure`.
|
|
214
173
|
|
|
215
|
-
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## Database: `WOWSQLClient`
|
|
216
177
|
|
|
217
178
|
```ruby
|
|
218
|
-
|
|
219
|
-
|
|
179
|
+
WOWSQL::WOWSQLClient.new(
|
|
180
|
+
project_url,
|
|
181
|
+
api_key,
|
|
182
|
+
base_domain: 'wowsql.com',
|
|
183
|
+
secure: true,
|
|
184
|
+
timeout: 30,
|
|
185
|
+
verify_ssl: true
|
|
186
|
+
)
|
|
187
|
+
```
|
|
220
188
|
|
|
221
|
-
|
|
222
|
-
|
|
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. |
|
|
223
196
|
|
|
224
|
-
|
|
225
|
-
.gt('age', 18)
|
|
197
|
+
**Reader:** `api_url`, `api_key`, `timeout`, `verify_ssl`.
|
|
226
198
|
|
|
227
|
-
|
|
228
|
-
.gte('age', 18)
|
|
199
|
+
---
|
|
229
200
|
|
|
230
|
-
|
|
231
|
-
.lt('age', 65)
|
|
201
|
+
## Table & `QueryBuilder`
|
|
232
202
|
|
|
233
|
-
|
|
234
|
-
.lte('age', 65)
|
|
203
|
+
### `Table`
|
|
235
204
|
|
|
236
|
-
|
|
237
|
-
.like('email', '%@gmail.com')
|
|
205
|
+
Obtained via `client.table('name')`.
|
|
238
206
|
|
|
239
|
-
|
|
240
|
-
|
|
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. |
|
|
241
222
|
|
|
242
|
-
|
|
243
|
-
.is_not_null('email')
|
|
244
|
-
```
|
|
223
|
+
### `QueryBuilder`
|
|
245
224
|
|
|
246
|
-
|
|
225
|
+
Chainable; ends with `get`, `execute`, `first`, `single`, `count`, or `paginate`.
|
|
247
226
|
|
|
248
|
-
|
|
249
|
-
# Multiple filters with sorting
|
|
250
|
-
active_users = client.table('users')
|
|
251
|
-
.select('id', 'name', 'email')
|
|
252
|
-
.eq('status', 'active')
|
|
253
|
-
.gt('age', 18)
|
|
254
|
-
.order_by('created_at', desc: true)
|
|
255
|
-
.limit(20)
|
|
256
|
-
.get
|
|
227
|
+
**Filters:** `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `like`, `is_null`, `is_not_null`, `in_list`, `not_in`, `between`, `not_between`, `or_filter`.
|
|
257
228
|
|
|
258
|
-
|
|
259
|
-
results = client.table('products')
|
|
260
|
-
.select('*')
|
|
261
|
-
.gt('price', 100)
|
|
262
|
-
.lt('price', 1000)
|
|
263
|
-
.like('name', '%widget%')
|
|
264
|
-
.is_not_null('category')
|
|
265
|
-
.order_by('price', desc: false) # asc
|
|
266
|
-
.limit(50)
|
|
267
|
-
.offset(0)
|
|
268
|
-
.get
|
|
269
|
-
```
|
|
229
|
+
**Grouping / aggregates:** `group_by(*columns)`, `having(column, operator, value)`.
|
|
270
230
|
|
|
271
|
-
|
|
231
|
+
**Sort / page:** `order_by`, `order`, `limit`, `offset`.
|
|
272
232
|
|
|
273
|
-
|
|
274
|
-
storage = WOWSQL::WOWSQLStorage.new(
|
|
275
|
-
'your-project-slug',
|
|
276
|
-
'your-api-key'
|
|
277
|
-
)
|
|
233
|
+
**Terminal:**
|
|
278
234
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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. |
|
|
285
242
|
|
|
286
|
-
|
|
287
|
-
if storage.file_exists?('uploads/document.pdf')
|
|
288
|
-
puts "File exists!"
|
|
289
|
-
end
|
|
243
|
+
### Filter operators (string)
|
|
290
244
|
|
|
291
|
-
|
|
292
|
-
info = storage.get_file_info('uploads/document.pdf')
|
|
293
|
-
puts "Size: #{info['size']} bytes"
|
|
294
|
-
puts "Modified: #{info['last_modified']}"
|
|
245
|
+
Use with `filter` or the shorthand methods:
|
|
295
246
|
|
|
296
|
-
|
|
297
|
-
files = storage.list_files('uploads/2024/')
|
|
298
|
-
files.each do |file|
|
|
299
|
-
puts "#{file['key']}: #{file['size']} bytes"
|
|
300
|
-
end
|
|
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.
|
|
301
248
|
|
|
302
|
-
|
|
303
|
-
url_data = storage.get_file_url('uploads/document.pdf', 3600)
|
|
304
|
-
puts "Download URL: #{url_data['file_url']}"
|
|
305
|
-
# URL is valid for 1 hour (3600 seconds)
|
|
306
|
-
|
|
307
|
-
# Delete single file
|
|
308
|
-
storage.delete_file('uploads/old-file.pdf')
|
|
309
|
-
|
|
310
|
-
# Delete multiple files
|
|
311
|
-
storage.delete_files([
|
|
312
|
-
'uploads/file1.pdf',
|
|
313
|
-
'uploads/file2.pdf',
|
|
314
|
-
'uploads/file3.pdf'
|
|
315
|
-
])
|
|
316
|
-
|
|
317
|
-
# Check quota
|
|
318
|
-
quota = storage.get_quota
|
|
319
|
-
puts "Used: #{quota['used_gb'].round(2)} GB"
|
|
320
|
-
puts "Available: #{quota['available_gb'].round(2)} GB"
|
|
321
|
-
puts "Usage: #{quota['usage_percentage'].round(1)}%"
|
|
322
|
-
|
|
323
|
-
# Check if enough storage before upload
|
|
324
|
-
file_size = File.size('large-file.zip')
|
|
325
|
-
if quota['available_bytes'] < file_size
|
|
326
|
-
puts "Not enough storage!"
|
|
327
|
-
else
|
|
328
|
-
storage.upload_from_path('large-file.zip', 'uploads/large-file.zip')
|
|
329
|
-
end
|
|
330
|
-
```
|
|
249
|
+
---
|
|
331
250
|
|
|
332
|
-
|
|
251
|
+
## Authentication: `ProjectAuthClient`
|
|
333
252
|
|
|
334
253
|
```ruby
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
rescue WOWSQL::WOWSQLException => e
|
|
345
|
-
puts "Database error (#{e.status_code}): #{e.message}"
|
|
346
|
-
end
|
|
347
|
-
|
|
348
|
-
begin
|
|
349
|
-
storage.upload_from_path('file.pdf', 'uploads/file.pdf')
|
|
350
|
-
rescue WOWSQL::StorageLimitExceededException => e
|
|
351
|
-
puts "Storage full: #{e.message}"
|
|
352
|
-
puts "Please upgrade your plan or delete old files"
|
|
353
|
-
rescue WOWSQL::StorageException => e
|
|
354
|
-
puts "Storage error: #{e.message}"
|
|
355
|
-
end
|
|
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
|
|
262
|
+
)
|
|
356
263
|
```
|
|
357
264
|
|
|
358
|
-
###
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
265
|
+
### `TokenStorage` (module)
|
|
266
|
+
|
|
267
|
+
Implement in your app for Redis/DB/session persistence:
|
|
268
|
+
|
|
269
|
+
- `get_access_token` / `set_access_token(token)`
|
|
270
|
+
- `get_refresh_token` / `set_refresh_token(token)`
|
|
271
|
+
|
|
272
|
+
`WOWSQL::MemoryTokenStorage` is in-memory only.
|
|
273
|
+
|
|
274
|
+
### Methods
|
|
275
|
+
|
|
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` | |
|
|
298
|
+
|
|
299
|
+
**Structs:** `AuthUser`, `AuthSession`, `AuthResponse` (see [Models](#models--types)).
|
|
376
300
|
|
|
377
|
-
|
|
301
|
+
---
|
|
378
302
|
|
|
379
|
-
|
|
303
|
+
## Storage: `WOWSQLStorage`
|
|
380
304
|
|
|
381
305
|
```ruby
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
'
|
|
385
|
-
'
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
'your-project-slug',
|
|
392
|
-
'your-api-key',
|
|
393
|
-
120 # 2 minutes for large files
|
|
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
|
|
394
315
|
)
|
|
395
316
|
```
|
|
396
317
|
|
|
397
|
-
|
|
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`.
|
|
398
337
|
|
|
399
|
-
|
|
400
|
-
# Best practice: Use environment variables for API keys
|
|
401
|
-
client = WOWSQL::WOWSQLClient.new(
|
|
402
|
-
ENV['WOWSQL_PROJECT_URL'],
|
|
403
|
-
ENV['WOWSQL_API_KEY']
|
|
404
|
-
)
|
|
338
|
+
---
|
|
405
339
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
340
|
+
## Schema: `WOWSQLSchema`
|
|
341
|
+
|
|
342
|
+
**Requires service role key.**
|
|
343
|
+
|
|
344
|
+
```ruby
|
|
345
|
+
WOWSQL::WOWSQLSchema.new(
|
|
346
|
+
project_url,
|
|
347
|
+
service_key,
|
|
348
|
+
base_domain: 'wowsql.com',
|
|
349
|
+
secure: true,
|
|
350
|
+
timeout: 30,
|
|
351
|
+
verify_ssl: true
|
|
409
352
|
)
|
|
410
353
|
```
|
|
411
354
|
|
|
412
|
-
|
|
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` | |
|
|
369
|
+
|
|
370
|
+
`operation` examples: `add_column`, `drop_column`, `modify_column`, `rename_column` (per API).
|
|
413
371
|
|
|
414
|
-
|
|
372
|
+
---
|
|
373
|
+
|
|
374
|
+
## Models & types
|
|
415
375
|
|
|
416
|
-
###
|
|
376
|
+
### Auth (structs)
|
|
417
377
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
| **Storage Operations** | Service Role Key (`wowbase_service_...`) | Anonymous Key (`wowbase_anon_...`) | `WOWSQLStorage` |
|
|
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`
|
|
422
381
|
|
|
423
|
-
###
|
|
382
|
+
### Storage
|
|
424
383
|
|
|
425
|
-
|
|
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`
|
|
426
387
|
|
|
427
|
-
|
|
428
|
-
- Location: "Service Role Key (keep secret)"
|
|
429
|
-
- Used for: Database CRUD operations and storage (recommended for server-side)
|
|
430
|
-
- **Important**: Click the eye icon to reveal this key
|
|
388
|
+
---
|
|
431
389
|
|
|
432
|
-
|
|
433
|
-
- Location: "Anonymous Key"
|
|
434
|
-
- Used for: Public/client-side database operations with limited permissions
|
|
435
|
-
- Optional: Use when exposing database access to frontend/client
|
|
390
|
+
## Exceptions
|
|
436
391
|
|
|
437
|
-
|
|
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. |
|
|
438
398
|
|
|
439
|
-
|
|
399
|
+
Aliases: `WOWSQLException`, `StorageException`, `StorageLimitExceededException`, `PermissionException` โ map to the classes above.
|
|
440
400
|
|
|
441
401
|
```ruby
|
|
442
|
-
|
|
443
|
-
client
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
402
|
+
begin
|
|
403
|
+
client.table('x').get
|
|
404
|
+
rescue WOWSQL::WOWSQLError => e
|
|
405
|
+
warn "#{e.status_code}: #{e.message}"
|
|
406
|
+
end
|
|
407
|
+
```
|
|
447
408
|
|
|
448
|
-
|
|
449
|
-
client = WOWSQL::WOWSQLClient.new(
|
|
450
|
-
'https://your-project.wowsql.com',
|
|
451
|
-
'wowbase_anon_your-anon-key-here' # Anonymous Key
|
|
452
|
-
)
|
|
409
|
+
---
|
|
453
410
|
|
|
454
|
-
|
|
455
|
-
users = client.table('users').get
|
|
456
|
-
```
|
|
411
|
+
## Configuration
|
|
457
412
|
|
|
458
|
-
|
|
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. |
|
|
459
419
|
|
|
460
|
-
|
|
461
|
-
2. **Use Anonymous Key** for public database access with limited permissions
|
|
462
|
-
3. **Store keys in environment variables**, never hardcode them
|
|
463
|
-
4. **Rotate keys regularly** if compromised
|
|
420
|
+
---
|
|
464
421
|
|
|
465
|
-
##
|
|
422
|
+
## Rails integration
|
|
466
423
|
|
|
467
|
-
|
|
424
|
+
**config/initializers/wowsql.rb**
|
|
468
425
|
|
|
469
426
|
```ruby
|
|
470
|
-
#
|
|
427
|
+
# frozen_string_literal: true
|
|
428
|
+
|
|
471
429
|
WOWSQL_CLIENT = WOWSQL::WOWSQLClient.new(
|
|
472
|
-
|
|
473
|
-
|
|
430
|
+
ENV.fetch('WOWSQL_PROJECT_URL'),
|
|
431
|
+
ENV.fetch('WOWSQL_SERVICE_KEY')
|
|
474
432
|
)
|
|
475
433
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
.select('*')
|
|
481
|
-
.get
|
|
482
|
-
|
|
483
|
-
response['data'].each do |user_data|
|
|
484
|
-
User.find_or_create_by(external_id: user_data['id']) do |user|
|
|
485
|
-
user.name = user_data['name']
|
|
486
|
-
user.email = user_data['email']
|
|
487
|
-
end
|
|
488
|
-
end
|
|
489
|
-
end
|
|
490
|
-
end
|
|
491
|
-
|
|
492
|
-
# Usage in Controller
|
|
493
|
-
class UsersController < ApplicationController
|
|
494
|
-
def index
|
|
495
|
-
@users = WOWSQL_CLIENT.table('users')
|
|
496
|
-
.select('id', 'name', 'email')
|
|
497
|
-
.get['data']
|
|
498
|
-
end
|
|
499
|
-
end
|
|
434
|
+
WOWSQL_AUTH = WOWSQL::ProjectAuthClient.new(
|
|
435
|
+
ENV.fetch('WOWSQL_PROJECT_URL'),
|
|
436
|
+
ENV.fetch('WOWSQL_ANON_KEY')
|
|
437
|
+
)
|
|
500
438
|
```
|
|
501
439
|
|
|
502
|
-
|
|
440
|
+
Use **service key** only in server-side code (jobs, controllers that must bypass restrictions). Prefer **anon** for end-user auth flows.
|
|
503
441
|
|
|
504
|
-
|
|
505
|
-
require 'sinatra'
|
|
506
|
-
require 'wowsql'
|
|
442
|
+
---
|
|
507
443
|
|
|
508
|
-
|
|
509
|
-
set :wowsql_client, WOWSQL::WOWSQLClient.new(
|
|
510
|
-
ENV['WOWSQL_PROJECT_URL'],
|
|
511
|
-
ENV['WOWSQL_API_KEY']
|
|
512
|
-
)
|
|
513
|
-
end
|
|
444
|
+
## Examples
|
|
514
445
|
|
|
515
|
-
|
|
516
|
-
users = settings.wowsql_client.table('users')
|
|
517
|
-
.select('id', 'name', 'email')
|
|
518
|
-
.get['data']
|
|
519
|
-
|
|
520
|
-
content_type :json
|
|
521
|
-
users.to_json
|
|
522
|
-
end
|
|
523
|
-
```
|
|
446
|
+
### Blog: posts and comments
|
|
524
447
|
|
|
525
|
-
|
|
448
|
+
```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
|
|
526
455
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
456
|
+
posts['data'].each do |p|
|
|
457
|
+
puts p['title']
|
|
458
|
+
end
|
|
459
|
+
```
|
|
530
460
|
|
|
531
|
-
|
|
461
|
+
### Upload avatar then save URL in `public` table
|
|
532
462
|
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
463
|
+
```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
|
|
470
|
+
```
|
|
537
471
|
|
|
538
|
-
|
|
472
|
+
---
|
|
539
473
|
|
|
540
|
-
|
|
474
|
+
## Troubleshooting
|
|
541
475
|
|
|
542
|
-
|
|
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. |
|
|
543
483
|
|
|
544
|
-
|
|
484
|
+
---
|
|
545
485
|
|
|
546
|
-
##
|
|
486
|
+
## Links
|
|
547
487
|
|
|
548
|
-
-
|
|
549
|
-
-
|
|
550
|
-
-
|
|
488
|
+
- [WowSQL Docs](https://wowsql.com/docs)
|
|
489
|
+
- [Dashboard](https://wowsql.com)
|
|
490
|
+
- [Support](mailto:support@wowsql.com)
|
|
551
491
|
|
|
552
492
|
---
|
|
553
493
|
|
|
554
|
-
|
|
494
|
+
**License:** MIT โ see included `LICENSE`.
|
|
495
|
+
|
|
496
|
+
*WowSQL Team*
|