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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e5ba81bb86f837e23521b7b13d2ee313a0ac7e34bbf04371c4256d00b077c7f3
4
- data.tar.gz: 9bb8a43825801b904b30bddcfbc51fedc74694bb04d37d404d33702338d40806
3
+ metadata.gz: 616f1c89797014131b2b43e8c7b5a9ca6f95ddb20627375e82043c8c2fd26698
4
+ data.tar.gz: f846cc82475f82545be62e916eb7bf9cc38acf6b00792bc4453da4a46b39abfb
5
5
  SHA512:
6
- metadata.gz: 29819ac550ac637dbc6bd3fb3456e426a2311cdbabc6cbe9953d4b03317ecb5eca6f55f5f70b69f515db433135b221fb45320f7aad3ded354b56a9772f150da2
7
- data.tar.gz: 2ae5fc934297b48c3352aab9bc6606dba1553adc91e989efe8b301a6163e291eeec9f79d592a2a781ea34528b13b75a6cf8d55714f4795b5da80984c9be6e165
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: 3600)
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
- require "bcrypt"
147
- BCrypt::Password.create(password)
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
- require "bcrypt"
152
- BCrypt::Password.new(hash) == password
153
- rescue BCrypt::Errors::InvalidHash
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: 3600)
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 == expected
205
+ OpenSSL.fixed_length_secure_compare(provided, expected)
197
206
  end
198
207
 
199
208
  def auth_handler(&block)
@@ -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
- true
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
- def consume(topic = nil, id: nil, &block)
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
- if id
155
- job = pop_by_id(topic, id)
156
- yield job if job
157
- else
158
- while (job = @backend.dequeue(topic))
159
- yield job
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
- if id
166
- job = pop_by_id(topic, id)
167
- yielder << job if job
168
- else
169
- while (job = @backend.dequeue(topic))
170
- yielder << job
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
@@ -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
- # Regenerate the session ID while preserving data
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Tina4
4
- VERSION = "3.10.67"
4
+ VERSION = "3.10.68"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tina4ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.10.67
4
+ version: 3.10.68
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tina4 Team