wowsql-sdk 3.1.0 → 3.9.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6af7e027585348a21d26fd70a40e2bfa2ccc7502ae68ee311564a4c8091fbc48
4
- data.tar.gz: 3367f7b010bb6fcea604af499cf9f60b16f0725cc0060fa37a44821e3f5e2fca
3
+ metadata.gz: 9d46c9cb43ff0d1ff729ee19c2384dec05476d286c4d5fb74041429fd4b5fc09
4
+ data.tar.gz: edb308e3ffa9e0be0ca4d7e7f54639da839c137f7eb52ce2eb977f8c9b7eae3b
5
5
  SHA512:
6
- metadata.gz: eb28912ac3e115c0afe97492493d5832b26cee3dd4f9f7d2e2934c733847f0766d74b32bdbe776525b0192a2a4661553df7e63591c6da05f609c6e3b2972d841
7
- data.tar.gz: c351aeaeb6113c5639fb3dff407150bdece3b12b0bd13d079123bee1d30e4d58a933cc38508485bbe190c387d2f40e8916261ac6642a3c9d3cf9cee5ba51e74a
6
+ metadata.gz: 24d72bdaedce237c50ed1fcd43326d9cb54a013ee541569ae49a1aa422896b10583d04af7c01a8bb512a11044012ea8865ce5a671c8364891dad7ecb36d320e3
7
+ data.tar.gz: 9ff8164572fbf57cb644c83c9345b74d18ba1763e91c49e372980a550854837777e7b1122414e8afea08f7873ad2e1b254f08402f66f8f6b31e25db870e955e6
data/CHANGELOG.md ADDED
@@ -0,0 +1,27 @@
1
+ # Changelog
2
+
3
+ All notable changes to the WowSQL Ruby SDK will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [3.9.1] - 2026-10-07
9
+
10
+ ### Added - Phone OTP (SMS)
11
+ - `sendOtp` / `verifyOtp` (and language equivalents) accept either **email** or **phone** (exactly one)
12
+ - Existing email OTP call sites remain backward compatible
13
+
14
+
15
+ ## [3.9.0] - 2026-08-20
16
+
17
+ ### Added - Realtime
18
+
19
+ - `client.realtime.subscribe` for Postgres `INSERT` / `UPDATE` / `DELETE` (and `*`)
20
+ - WebSocket auth: `wss://<project>/realtime/v1/websocket?apikey=<anon or service_role key>`
21
+ - Auto-reconnect after disconnect; unsubscribe / disconnect clean local and server state
22
+ - `client.realtime.channel(name)` — ephemeral broadcast (`send`) and presence (`track` / `presence_state`)
23
+ - `websocket-client-simple` dependency
24
+
25
+ ### Documentation
26
+
27
+ - README realtime section
data/LICENSE CHANGED
@@ -1,22 +1,86 @@
1
- MIT License
1
+ Elastic License 2.0
2
2
 
3
- Copyright (c) 2024 WowSQL
3
+ Copyright 2026 WowSQL Team
4
4
 
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
5
+ ## Acceptance
11
6
 
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
7
+ By using the software, you agree to all of the terms and conditions below.
14
8
 
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
9
+ ## Copyright License
22
10
 
11
+ The licensor grants you a non-exclusive, royalty-free, worldwide,
12
+ non-sublicensable, non-transferable license to use, copy, distribute, make
13
+ available, and prepare derivative works of the software, in each case subject
14
+ to the limitations and conditions below.
15
+
16
+ ## Limitations
17
+
18
+ You may not provide the software to third parties as a hosted or managed
19
+ service, where the service provides users with access to any substantial set of
20
+ the features or functionality of the software.
21
+
22
+ You may not move, change, disable, or circumvent the license key functionality
23
+ in the software, and you may not remove or obscure any functionality in the
24
+ software that is protected by the license key.
25
+
26
+ You may not alter, remove, or obscure any licensing, copyright, or other
27
+ notices of the licensor in the software. Any use of the licensor's trademarks
28
+ is subject to applicable law.
29
+
30
+ ## Patents
31
+
32
+ The licensor grants you a license, under any patent claims the licensor can
33
+ license, or becomes able to license, to make, have made, use, practice,
34
+ import, compile, run, and execute the software. If you institute, or help
35
+ institute, patent litigation against any person over claims that your use of
36
+ the software infringes or contributes to the infringement of a patent, then
37
+ your patent license for the software ends immediately.
38
+
39
+ ## Notices
40
+
41
+ You must ensure that anyone who gets a copy of any part of the software from
42
+ you also gets a copy of these terms.
43
+
44
+ If you modify the software, you must include in any modified copies of the
45
+ software prominent notices stating that you have modified the software.
46
+
47
+ ## No Other Rights
48
+
49
+ These terms do not imply any licenses other than those expressly granted in
50
+ these terms.
51
+
52
+ ## Termination
53
+
54
+ If you use the software in violation of these terms, such use is not licensed,
55
+ and your licenses will automatically terminate. If the licensor provides you
56
+ with a notice of your violation, and you cease all violation of this license no
57
+ later than 30 days after you receive that notice, your licenses will be
58
+ reinstated retroactively. However, if you violate these terms after such
59
+ reinstatement, any additional violation of these terms will cause your licenses
60
+ to terminate automatically and permanently.
61
+
62
+ ## No Liability
63
+
64
+ As far as the law allows, the software comes as is, without any warranty or
65
+ condition, and the licensor will not be liable to you for any damages arising
66
+ out of these terms or the use or nature of the software, under any kind of
67
+ legal claim.
68
+
69
+ ## Definitions
70
+
71
+ The "licensor" is the entity offering these terms, and the "software" is the
72
+ software the licensor makes available under these terms, including any portion
73
+ of it.
74
+
75
+ "you" refers to the individual or entity agreeing to these terms.
76
+
77
+ "your company" is any legal entity, sole proprietorship, or other kind of
78
+ organization that you work for, plus all organizations that have control over,
79
+ are controlled by, or are under common control with that organization.
80
+
81
+ "your licenses" are all the licenses granted to you for the software under
82
+ these terms.
83
+
84
+ "use" means anything you do with the software requiring one of your licenses.
85
+
86
+ "trademark" means trademarks, service marks, and similar rights.
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # WowSQL Ruby SDK
2
2
 
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.
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
 
@@ -46,6 +46,7 @@ The official Ruby SDK for [WowSQL](https://wowsqlconnect.com). Provides a clean,
46
46
  - [upload / upload_from_path](#upload--upload_from_path)
47
47
  - [list_files / download / delete_file](#list_files--download--delete_file)
48
48
  - [get_public_url](#get_public_url)
49
+ - [Realtime](#realtime)
49
50
  - [Schema Management](#schema-management)
50
51
  - [create_table](#create_table)
51
52
  - [add_column / drop_column / rename_column](#add_column--drop_column--rename_column)
@@ -60,6 +61,7 @@ The official Ruby SDK for [WowSQL](https://wowsqlconnect.com). Provides a clean,
60
61
  - Ruby 2.7 or higher
61
62
  - `faraday` >= 1.0
62
63
  - `faraday-multipart` >= 1.0
64
+ - `websocket-client-simple` (realtime; declared in the gemspec)
63
65
 
64
66
  ---
65
67
 
@@ -593,6 +595,37 @@ puts url # https://myproject.wowsqlconnect.com/api/v1/storage/...
593
595
 
594
596
  ---
595
597
 
598
+ ## Realtime
599
+
600
+ Subscribe to INSERT / UPDATE / DELETE, broadcast ephemeral events, and track presence. Uses the **same anon or service_role key** as REST.
601
+
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`.
609
+
610
+ ```ruby
611
+ unsub = client.realtime.subscribe("messages") do |change|
612
+ puts "#{change['event']} #{change['new'] || change['old']}"
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
625
+ ```
626
+
627
+ ---
628
+
596
629
  ## Schema Management
597
630
 
598
631
  Schema operations require a **service role key** (`wowsql_service_...`).
data/lib/wowsql/auth.rb CHANGED
@@ -242,41 +242,54 @@ module WOWSQL
242
242
  }
243
243
  end
244
244
 
245
- # Send OTP code to user's email.
245
+ # Send OTP via email or phone (SMS). Provide exactly one of email or phone.
246
246
  #
247
- # @param email [String] User's email address
247
+ # @param email [String, nil] User's email address
248
+ # @param phone [String, nil] E.164 phone number
248
249
  # @param purpose [String] 'login', 'signup', or 'password_reset'
249
250
  # @return [Hash]
250
- def send_otp(email:, purpose: 'login')
251
+ def send_otp(email: nil, phone: nil, purpose: 'login')
251
252
  unless %w[login signup password_reset].include?(purpose)
252
253
  raise WOWSQLError.new("Purpose must be 'login', 'signup', or 'password_reset'")
253
254
  end
255
+ if email.to_s.empty? == phone.to_s.empty?
256
+ raise WOWSQLError.new('Provide exactly one of email or phone')
257
+ end
258
+
259
+ payload = { purpose: purpose }
260
+ payload[:email] = email unless email.to_s.empty?
261
+ payload[:phone] = phone unless phone.to_s.empty?
254
262
 
255
- payload = { email: email, purpose: purpose }
256
263
  data = request('POST', '/otp/send', nil, payload)
257
264
  {
258
265
  'success' => data['success'] != false,
259
- 'message' => data['message'] || 'If that email exists, an OTP code has been sent'
266
+ 'message' => data['message'] || (phone.to_s.empty? ? 'If that email exists, an OTP code has been sent' : 'If that phone exists, an OTP code has been sent')
260
267
  }
261
268
  end
262
269
 
263
- # Verify OTP and complete authentication.
270
+ # Verify OTP via email or phone. Provide exactly one of email or phone.
264
271
  #
265
- # @param email [String] User's email address
266
- # @param otp [String] 6-digit OTP code
272
+ # @param email [String, nil] User's email address
273
+ # @param phone [String, nil] E.164 phone number
274
+ # @param otp [String] OTP code
267
275
  # @param purpose [String] 'login', 'signup', or 'password_reset'
268
276
  # @param new_password [String, nil] Required for password_reset purpose
269
277
  # @return [AuthResponse, Hash]
270
- def verify_otp(email:, otp:, purpose: 'login', new_password: nil)
278
+ def verify_otp(email: nil, phone: nil, otp:, purpose: 'login', new_password: nil)
271
279
  unless %w[login signup password_reset].include?(purpose)
272
280
  raise WOWSQLError.new("Purpose must be 'login', 'signup', or 'password_reset'")
273
281
  end
282
+ if email.to_s.empty? == phone.to_s.empty?
283
+ raise WOWSQLError.new('Provide exactly one of email or phone')
284
+ end
274
285
 
275
286
  if purpose == 'password_reset' && new_password.nil?
276
287
  raise WOWSQLError.new('new_password is required for password_reset purpose')
277
288
  end
278
289
 
279
- payload = { email: email, otp: otp, purpose: purpose }
290
+ payload = { otp: otp, purpose: purpose }
291
+ payload[:email] = email unless email.to_s.empty?
292
+ payload[:phone] = phone unless phone.to_s.empty?
280
293
  payload[:new_password] = new_password if new_password
281
294
 
282
295
  data = request('POST', '/otp/verify', nil, payload)
data/lib/wowsql/client.rb CHANGED
@@ -66,6 +66,10 @@ module WOWSQL
66
66
  Table.new(self, table_name)
67
67
  end
68
68
 
69
+ def realtime
70
+ @realtime ||= WowSQLRealtime.new(@base_url, @api_key)
71
+ end
72
+
69
73
  # Close the HTTP connection (no-op, provided for API parity).
70
74
  def close
71
75
  @conn.close if @conn.respond_to?(:close)
@@ -0,0 +1,201 @@
1
+ require 'json'
2
+ require 'thread'
3
+ require 'uri'
4
+
5
+ module WOWSQL
6
+ def self.build_realtime_websocket_url(project_url, api_key)
7
+ origin = project_url.to_s.sub(%r{/$}, '')
8
+ ws = origin.sub(/\Ahttp/, 'ws')
9
+ "#{ws}/realtime/v1/websocket?apikey=#{URI.encode_www_form_component(api_key)}"
10
+ end
11
+
12
+ class RealtimeChannel
13
+ attr_reader :name
14
+
15
+ def initialize(rt, name)
16
+ @rt = rt
17
+ @name = name
18
+ @joined = false
19
+ @state = {}
20
+ @broadcast = []
21
+ @presence = []
22
+ @tracked = nil
23
+ end
24
+
25
+ def on(kind, filter = {}, &cb)
26
+ if kind.to_s == 'broadcast'
27
+ ev = (filter[:event] || filter['event'] || '*').to_s
28
+ @broadcast << ->(msg) { cb.call(msg) if ev == '*' || ev == msg['event'].to_s }
29
+ else
30
+ @presence << cb
31
+ end
32
+ self
33
+ end
34
+
35
+ def subscribe(&on_status)
36
+ @rt.ensure_connected
37
+ @rt.send_json(type: 'join', channel: @name)
38
+ @joined = true
39
+ on_status&.call('SUBSCRIBED')
40
+ self
41
+ end
42
+
43
+ def send(event:, payload: {})
44
+ @rt.ensure_connected
45
+ unless @joined
46
+ @rt.send_json(type: 'join', channel: @name)
47
+ @joined = true
48
+ end
49
+ @rt.send_json(type: 'broadcast', channel: @name, event: event, payload: payload || {})
50
+ end
51
+
52
+ def track(payload)
53
+ @tracked = payload
54
+ @rt.ensure_connected
55
+ unless @joined
56
+ @rt.send_json(type: 'join', channel: @name)
57
+ @joined = true
58
+ end
59
+ @rt.send_json(type: 'presence', event: 'track', channel: @name, payload: payload)
60
+ end
61
+
62
+ def presence_state
63
+ @state.dup
64
+ end
65
+
66
+ def unsubscribe
67
+ @rt.send_json(type: 'leave', channel: @name)
68
+ @joined = false
69
+ @rt.drop_channel(@name)
70
+ end
71
+
72
+ def rejoin
73
+ @rt.send_json(type: 'join', channel: @name)
74
+ @rt.send_json(type: 'presence', event: 'track', channel: @name, payload: @tracked) if @tracked
75
+ end
76
+
77
+ def handle_server(message)
78
+ case message['type']
79
+ when 'joined' then @joined = true
80
+ when 'presence'
81
+ ev = message['event']
82
+ if ev == 'sync' && message['state'].is_a?(Hash)
83
+ @state = message['state']
84
+ elsif ev == 'join' && message['key']
85
+ @state[message['key']] = message['payload']
86
+ elsif ev == 'leave' && message['key']
87
+ @state.delete(message['key'])
88
+ end
89
+ @presence.each { |cb| cb.call(message) }
90
+ when 'broadcast'
91
+ wrapped = { 'event' => message['event'], 'payload' => message['payload'] || {}, 'channel' => @name }
92
+ @broadcast.each { |cb| cb.call(wrapped) }
93
+ end
94
+ end
95
+ end
96
+
97
+ class WowSQLRealtime
98
+ def initialize(project_url, api_key)
99
+ @project_url = project_url.to_s.sub(%r{/$}, '')
100
+ @api_key = api_key
101
+ @ws = nil
102
+ @manual = false
103
+ @subs = []
104
+ @channels = {}
105
+ @mutex = Mutex.new
106
+ end
107
+
108
+ def url
109
+ WOWSQL.build_realtime_websocket_url(@project_url, @api_key)
110
+ end
111
+
112
+ def channel(name)
113
+ @channels[name] ||= RealtimeChannel.new(self, name)
114
+ end
115
+
116
+ def subscribe(table, schema: 'public', event: '*', &callback)
117
+ sub = { schema: schema, table: table, event: event, callback: callback }
118
+ @subs << sub
119
+ Thread.new do
120
+ ensure_connected
121
+ send_json(type: 'subscribe', schema: schema, table: table, event: event)
122
+ end
123
+ -> { unsubscribe(sub) }
124
+ end
125
+
126
+ def disconnect
127
+ @manual = true
128
+ @ws&.close
129
+ @ws = nil
130
+ end
131
+
132
+ def drop_channel(name)
133
+ @channels.delete(name)
134
+ end
135
+
136
+ def send_json(obj)
137
+ @ws&.send(JSON.dump(obj))
138
+ end
139
+
140
+ def ensure_connected
141
+ return if @ws
142
+ begin
143
+ require 'websocket-client-simple'
144
+ rescue LoadError
145
+ raise LoadError, 'Realtime requires the websocket-client-simple gem'
146
+ end
147
+ @manual = false
148
+ rt = self
149
+ @ws = WebSocket::Client::Simple.connect(url) do |ws|
150
+ ws.on :message do |msg|
151
+ rt.send(:handle, msg.data)
152
+ end
153
+ ws.on :open do
154
+ rt.send(:on_open)
155
+ end
156
+ ws.on :close do |_e|
157
+ rt.send(:on_close)
158
+ end
159
+ end
160
+ sleep 0.2
161
+ end
162
+
163
+ private
164
+
165
+ def on_open
166
+ @subs.each { |s| send_json(type: 'subscribe', schema: s[:schema], table: s[:table], event: s[:event]) }
167
+ @channels.each_value(&:rejoin)
168
+ end
169
+
170
+ def on_close
171
+ @ws = nil
172
+ ensure_connected if !@manual && (!@subs.empty? || !@channels.empty?)
173
+ end
174
+
175
+ def unsubscribe(sub)
176
+ @subs.delete(sub)
177
+ send_json(type: 'unsubscribe', schema: sub[:schema], table: sub[:table])
178
+ end
179
+
180
+ def handle(raw)
181
+ message = JSON.parse(raw)
182
+ name = message['channel']
183
+ @channels[name]&.handle_server(message) if name
184
+ return unless message['type'] == 'broadcast'
185
+ return if message['channel'] && !message['table']
186
+ nested = message['payload'].is_a?(Hash) ? message['payload'] : {}
187
+ event = (message['event'] || nested['type'] || '').to_s.upcase
188
+ schema = (message['schema'] || nested['schema'] || 'public').to_s
189
+ table = (message['table'] || nested['table'] || '').to_s
190
+ return if table.empty? || !%w[INSERT UPDATE DELETE].include?(event)
191
+ change = { 'event' => event, 'schema' => schema, 'table' => table, 'payload' => nested }
192
+ change['new'] = nested['new'] if nested.key?('new')
193
+ change['old'] = nested['old'] if nested.key?('old')
194
+ @subs.each do |s|
195
+ next unless s[:schema] == schema && s[:table] == table
196
+ next unless s[:event] == '*' || s[:event].to_s.upcase == event
197
+ s[:callback].call(change)
198
+ end
199
+ end
200
+ end
201
+ end
@@ -1,4 +1,4 @@
1
1
  module WOWSQL
2
- VERSION = "3.0.1"
2
+ VERSION = "3.9.1"
3
3
  end
4
4
 
data/lib/wowsql.rb CHANGED
@@ -6,6 +6,7 @@ require_relative 'wowsql/query_builder'
6
6
  require_relative 'wowsql/auth'
7
7
  require_relative 'wowsql/storage'
8
8
  require_relative 'wowsql/schema'
9
+ require_relative 'wowsql/realtime'
9
10
 
10
11
  module WOWSQL
11
12
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: wowsql-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.1.0
4
+ version: 3.9.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - WOWSQL Team
@@ -51,6 +51,20 @@ dependencies:
51
51
  - - "~>"
52
52
  - !ruby/object:Gem::Version
53
53
  version: '2.0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: websocket-client-simple
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '0.3'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '0.3'
54
68
  - !ruby/object:Gem::Dependency
55
69
  name: rspec
56
70
  requirement: !ruby/object:Gem::Requirement
@@ -87,6 +101,7 @@ executables: []
87
101
  extensions: []
88
102
  extra_rdoc_files: []
89
103
  files:
104
+ - CHANGELOG.md
90
105
  - LICENSE
91
106
  - README.md
92
107
  - lib/wowmysql.rb
@@ -95,6 +110,7 @@ files:
95
110
  - lib/wowsql/client.rb
96
111
  - lib/wowsql/exceptions.rb
97
112
  - lib/wowsql/query_builder.rb
113
+ - lib/wowsql/realtime.rb
98
114
  - lib/wowsql/schema.rb
99
115
  - lib/wowsql/storage.rb
100
116
  - lib/wowsql/table.rb