tina4ruby 3.10.67 → 3.10.68
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/lib/tina4/auth.rb +19 -10
- data/lib/tina4/database.rb +25 -1
- data/lib/tina4/graphql.rb +36 -0
- data/lib/tina4/orm.rb +8 -1
- data/lib/tina4/queue.rb +35 -14
- data/lib/tina4/response.rb +17 -0
- data/lib/tina4/session.rb +8 -1
- data/lib/tina4/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 616f1c89797014131b2b43e8c7b5a9ca6f95ddb20627375e82043c8c2fd26698
|
|
4
|
+
data.tar.gz: f846cc82475f82545be62e916eb7bf9cc38acf6b00792bc4453da4a46b39abfb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e4a317f3c88f2353a6a59312c02d50a130eac8b25b4925bb03160d54c2792012069fc84f0edef0c12510447f7e391e5e54a0af98edb91b57a3797a78d58b7584
|
|
7
|
+
data.tar.gz: 646e0e66ec3cb40f91c7a9978ee8e3ce35149740696872a078bf6b8b1f50bb5e1365640e3aa289eba035d78d877f851b4afc716f3337a905c5bc144282f0e8d1
|
data/lib/tina4/auth.rb
CHANGED
|
@@ -89,11 +89,11 @@ module Tina4
|
|
|
89
89
|
|
|
90
90
|
# ── Token API (auto-selects HS256 or RS256) ─────────────────
|
|
91
91
|
|
|
92
|
-
def get_token(payload, expires_in:
|
|
92
|
+
def get_token(payload, expires_in: 60)
|
|
93
93
|
now = Time.now.to_i
|
|
94
94
|
claims = payload.merge(
|
|
95
95
|
"iat" => now,
|
|
96
|
-
"exp" => now + expires_in,
|
|
96
|
+
"exp" => now + (expires_in * 60).to_i,
|
|
97
97
|
"nbf" => now
|
|
98
98
|
)
|
|
99
99
|
|
|
@@ -142,15 +142,23 @@ module Tina4
|
|
|
142
142
|
{ valid: false, error: e.message }
|
|
143
143
|
end
|
|
144
144
|
|
|
145
|
-
def hash_password(password)
|
|
146
|
-
|
|
147
|
-
|
|
145
|
+
def hash_password(password, salt = nil, iterations = 260000)
|
|
146
|
+
salt ||= SecureRandom.hex(16)
|
|
147
|
+
dk = OpenSSL::KDF.pbkdf2_hmac(password, salt: salt, iterations: iterations, length: 32, hash: "sha256")
|
|
148
|
+
"pbkdf2_sha256$#{iterations}$#{salt}$#{dk.unpack1('H*')}"
|
|
148
149
|
end
|
|
149
150
|
|
|
150
151
|
def check_password(password, hash)
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
152
|
+
parts = hash.split('$')
|
|
153
|
+
return false unless parts.length == 4 && parts[0] == 'pbkdf2_sha256'
|
|
154
|
+
iterations = parts[1].to_i
|
|
155
|
+
salt = parts[2]
|
|
156
|
+
expected = parts[3]
|
|
157
|
+
dk = OpenSSL::KDF.pbkdf2_hmac(password, salt: salt, iterations: iterations, length: 32, hash: "sha256")
|
|
158
|
+
actual = dk.unpack1('H*')
|
|
159
|
+
# Timing-safe comparison
|
|
160
|
+
OpenSSL.fixed_length_secure_compare(actual, expected)
|
|
161
|
+
rescue
|
|
154
162
|
false
|
|
155
163
|
end
|
|
156
164
|
|
|
@@ -165,7 +173,7 @@ module Tina4
|
|
|
165
173
|
nil
|
|
166
174
|
end
|
|
167
175
|
|
|
168
|
-
def refresh_token(token, expires_in:
|
|
176
|
+
def refresh_token(token, expires_in: 60)
|
|
169
177
|
payload = valid_token(token)
|
|
170
178
|
return nil unless payload
|
|
171
179
|
|
|
@@ -192,8 +200,9 @@ module Tina4
|
|
|
192
200
|
expected ||= ENV["TINA4_API_KEY"] || ENV["API_KEY"]
|
|
193
201
|
return false if expected.nil? || expected.empty?
|
|
194
202
|
return false if provided.nil? || provided.empty?
|
|
203
|
+
return false if provided.length != expected.length
|
|
195
204
|
|
|
196
|
-
provided
|
|
205
|
+
OpenSSL.fixed_length_secure_compare(provided, expected)
|
|
197
206
|
end
|
|
198
207
|
|
|
199
208
|
def auth_handler(&block)
|
data/lib/tina4/database.rb
CHANGED
|
@@ -280,9 +280,33 @@ module Tina4
|
|
|
280
280
|
{ success: true }
|
|
281
281
|
end
|
|
282
282
|
|
|
283
|
+
# Return the last execute() error message, or nil.
|
|
284
|
+
def get_error
|
|
285
|
+
@last_error
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
# Return the last insert ID from execute() or insert().
|
|
289
|
+
def get_last_id
|
|
290
|
+
current_driver.last_insert_id
|
|
291
|
+
rescue
|
|
292
|
+
nil
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# Execute a write statement. Returns true/false for simple writes.
|
|
296
|
+
# Returns DatabaseResult if SQL contains RETURNING, CALL, EXEC, or SELECT.
|
|
283
297
|
def execute(sql, params = [])
|
|
284
298
|
cache_invalidate if @cache_enabled
|
|
285
|
-
current_driver.execute(sql, params)
|
|
299
|
+
result = current_driver.execute(sql, params)
|
|
300
|
+
@last_error = nil
|
|
301
|
+
sql_upper = sql.strip.upcase
|
|
302
|
+
if sql_upper.include?("RETURNING") || sql_upper.start_with?("CALL ") ||
|
|
303
|
+
sql_upper.start_with?("EXEC ") || sql_upper.start_with?("SELECT ")
|
|
304
|
+
return result
|
|
305
|
+
end
|
|
306
|
+
true
|
|
307
|
+
rescue => e
|
|
308
|
+
@last_error = e.message
|
|
309
|
+
false
|
|
286
310
|
end
|
|
287
311
|
|
|
288
312
|
def execute_many(sql, params_list = [])
|
data/lib/tina4/graphql.rb
CHANGED
|
@@ -768,6 +768,42 @@ module Tina4
|
|
|
768
768
|
{ "data" => nil, "errors" => [{ "message" => "Internal error: #{e.message}" }] }
|
|
769
769
|
end
|
|
770
770
|
|
|
771
|
+
# Return schema as GraphQL SDL string.
|
|
772
|
+
def schema
|
|
773
|
+
sdl = ""
|
|
774
|
+
@schema.types.each do |name, type_obj|
|
|
775
|
+
sdl += "type #{name} {\n"
|
|
776
|
+
type_obj.fields.each { |f| sdl += " #{f[:name]}: #{f[:type]}\n" }
|
|
777
|
+
sdl += "}\n\n"
|
|
778
|
+
end
|
|
779
|
+
unless @schema.queries.empty?
|
|
780
|
+
sdl += "type Query {\n"
|
|
781
|
+
@schema.queries.each do |name, config|
|
|
782
|
+
args = (config[:args] || {}).map { |k, v| "#{k}: #{v}" }.join(", ")
|
|
783
|
+
arg_str = args.empty? ? "" : "(#{args})"
|
|
784
|
+
sdl += " #{name}#{arg_str}: #{config[:type]}\n"
|
|
785
|
+
end
|
|
786
|
+
sdl += "}\n\n"
|
|
787
|
+
end
|
|
788
|
+
unless @schema.mutations.empty?
|
|
789
|
+
sdl += "type Mutation {\n"
|
|
790
|
+
@schema.mutations.each do |name, config|
|
|
791
|
+
args = (config[:args] || {}).map { |k, v| "#{k}: #{v}" }.join(", ")
|
|
792
|
+
arg_str = args.empty? ? "" : "(#{args})"
|
|
793
|
+
sdl += " #{name}#{arg_str}: #{config[:type]}\n"
|
|
794
|
+
end
|
|
795
|
+
sdl += "}\n\n"
|
|
796
|
+
end
|
|
797
|
+
sdl
|
|
798
|
+
end
|
|
799
|
+
|
|
800
|
+
# Return schema metadata for debugging.
|
|
801
|
+
def introspect
|
|
802
|
+
queries = @schema.queries.transform_values { |v| { type: v[:type], args: v[:args] || {} } }
|
|
803
|
+
mutations = @schema.mutations.transform_values { |v| { type: v[:type], args: v[:args] || {} } }
|
|
804
|
+
{ types: @schema.types.keys, queries: queries, mutations: mutations }
|
|
805
|
+
end
|
|
806
|
+
|
|
771
807
|
# Handle an HTTP request body (JSON string)
|
|
772
808
|
def handle_request(body, context: {})
|
|
773
809
|
payload = JSON.parse(body)
|
data/lib/tina4/orm.rb
CHANGED
|
@@ -414,7 +414,7 @@ module Tina4
|
|
|
414
414
|
@persisted = true
|
|
415
415
|
end
|
|
416
416
|
end
|
|
417
|
-
|
|
417
|
+
self
|
|
418
418
|
rescue => e
|
|
419
419
|
@errors << e.message
|
|
420
420
|
false
|
|
@@ -542,6 +542,7 @@ module Tina4
|
|
|
542
542
|
|
|
543
543
|
alias to_hash to_h
|
|
544
544
|
alias to_dict to_h
|
|
545
|
+
alias to_assoc to_h
|
|
545
546
|
alias to_object to_h
|
|
546
547
|
|
|
547
548
|
def to_array
|
|
@@ -672,5 +673,11 @@ module Tina4
|
|
|
672
673
|
|
|
673
674
|
related_class.find(fk_value)
|
|
674
675
|
end
|
|
676
|
+
|
|
677
|
+
# Instance-level aliases matching Python/PHP/Node.js naming
|
|
678
|
+
# These are imperative relationship queries (not class-level declarations)
|
|
679
|
+
alias imperative_has_one query_has_one
|
|
680
|
+
alias imperative_has_many query_has_many
|
|
681
|
+
alias imperative_belongs_to query_belongs_to
|
|
675
682
|
end
|
|
676
683
|
end
|
data/lib/tina4/queue.rb
CHANGED
|
@@ -147,28 +147,49 @@ module Tina4
|
|
|
147
147
|
# # Or as an enumerator:
|
|
148
148
|
# queue.consume("emails").each { |job| process(job) }
|
|
149
149
|
#
|
|
150
|
-
|
|
150
|
+
# Consume jobs from a topic using a long-running generator.
|
|
151
|
+
#
|
|
152
|
+
# Polls the queue continuously. When empty, sleeps for poll_interval
|
|
153
|
+
# seconds before polling again. No external while-loop or sleep needed.
|
|
154
|
+
#
|
|
155
|
+
# queue.consume("emails") { |job| process(job) }
|
|
156
|
+
# queue.consume("emails", poll_interval: 5) { |job| process(job) }
|
|
157
|
+
# queue.consume("emails", id: "abc-123") { |job| process(job) }
|
|
158
|
+
#
|
|
159
|
+
def consume(topic = nil, id: nil, poll_interval: 1.0, &block)
|
|
151
160
|
topic ||= @topic
|
|
152
161
|
|
|
162
|
+
if id
|
|
163
|
+
# Single job by ID — no polling
|
|
164
|
+
job = pop_by_id(topic, id)
|
|
165
|
+
if job
|
|
166
|
+
block_given? ? yield(job) : (return Enumerator.new { |y| y << job })
|
|
167
|
+
end
|
|
168
|
+
return block_given? ? nil : Enumerator.new { |_| }
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# poll_interval=0 → single-pass drain (returns when empty)
|
|
172
|
+
# poll_interval>0 → long-running poll (sleeps when empty, never returns)
|
|
153
173
|
if block_given?
|
|
154
|
-
|
|
155
|
-
job =
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
174
|
+
loop do
|
|
175
|
+
job = @backend.dequeue(topic)
|
|
176
|
+
if job.nil?
|
|
177
|
+
break if poll_interval <= 0
|
|
178
|
+
sleep(poll_interval)
|
|
179
|
+
next
|
|
160
180
|
end
|
|
181
|
+
yield job
|
|
161
182
|
end
|
|
162
183
|
else
|
|
163
|
-
# Return an Enumerator when no block given
|
|
164
184
|
Enumerator.new do |yielder|
|
|
165
|
-
|
|
166
|
-
job =
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
185
|
+
loop do
|
|
186
|
+
job = @backend.dequeue(topic)
|
|
187
|
+
if job.nil?
|
|
188
|
+
break if poll_interval <= 0
|
|
189
|
+
sleep(poll_interval)
|
|
190
|
+
next
|
|
171
191
|
end
|
|
192
|
+
yielder << job
|
|
172
193
|
end
|
|
173
194
|
end
|
|
174
195
|
end
|
data/lib/tina4/response.rb
CHANGED
|
@@ -45,6 +45,23 @@ module Tina4
|
|
|
45
45
|
end
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
+
# Callable response — auto-detects content type from data.
|
|
49
|
+
# Matches Python __call__ / PHP __invoke / Node response() pattern.
|
|
50
|
+
def call(data = nil, status_code = 200, content_type = nil)
|
|
51
|
+
@status_code = status_code
|
|
52
|
+
if content_type
|
|
53
|
+
@headers["content-type"] = content_type
|
|
54
|
+
@body = data.to_s
|
|
55
|
+
elsif data.is_a?(Hash) || data.is_a?(Array)
|
|
56
|
+
@headers["content-type"] = JSON_CONTENT_TYPE
|
|
57
|
+
@body = JSON.generate(data)
|
|
58
|
+
else
|
|
59
|
+
@headers["content-type"] = HTML_CONTENT_TYPE
|
|
60
|
+
@body = data.to_s
|
|
61
|
+
end
|
|
62
|
+
self
|
|
63
|
+
end
|
|
64
|
+
|
|
48
65
|
def json(data, status_or_opts = nil, status: nil)
|
|
49
66
|
@status_code = status || (status_or_opts.is_a?(Integer) ? status_or_opts : 200)
|
|
50
67
|
@headers["content-type"] = JSON_CONTENT_TYPE
|
data/lib/tina4/session.rb
CHANGED
|
@@ -93,12 +93,19 @@ module Tina4
|
|
|
93
93
|
end
|
|
94
94
|
end
|
|
95
95
|
|
|
96
|
-
#
|
|
96
|
+
# Get flash data by key (alias for flash(key) without value)
|
|
97
|
+
def get_flash(key, default = nil)
|
|
98
|
+
result = flash(key)
|
|
99
|
+
result.nil? ? default : result
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Regenerate the session ID while preserving data — returns new ID
|
|
97
103
|
def regenerate
|
|
98
104
|
old_id = @id
|
|
99
105
|
@id = SecureRandom.hex(32)
|
|
100
106
|
@handler.destroy(old_id)
|
|
101
107
|
@modified = true
|
|
108
|
+
@id
|
|
102
109
|
end
|
|
103
110
|
|
|
104
111
|
# Garbage collection: remove expired sessions from the handler
|
data/lib/tina4/version.rb
CHANGED