tina4ruby 3.10.83 → 3.10.84

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: e7f1721eaefd19400dedcfdb298a6af42772aa824b9663a17d79bced2169161b
4
- data.tar.gz: a712e6a50ea1ece57a4e4bc9e0e49c49dcb0f5e5c56471c1da26bd4f65c42d79
3
+ metadata.gz: 85d4f3e7d47f44f07e2ab8780a19dd082dcc05e0ba7551e3e8df4b486faa151e
4
+ data.tar.gz: d8662f6770852844ff213be6f1d57329bec5cc243f2d02f2690a04f248c11bf3
5
5
  SHA512:
6
- metadata.gz: 8f50ab9dbaf48c592766f8b13debe1086e7288184a69849346fae318aa270a275c28fb28399eac9a834ba3c18fb884babefc8d1f860b9326c06fdba3623b4a43
7
- data.tar.gz: ab1063c1b9402c3445ee4d75fdf98eab9342560e3d1689085879a48309fbe3a9761a0ee1759f517ae0641b82f46f801b6585244f45aadd83f7dbdddfd4c54f03
6
+ metadata.gz: 3955df89a811694cbadb74f30c8f0cebed30e9626b735a4cc6ad172c08acaf2e0b33458593c3700416b11a9b453e76d604a0a34a09b2fff46ec0610d0f58043f
7
+ data.tar.gz: '0103297c9fe0bcde8be0d82b5c71a9fef458f467c16d9ee654c5d806ae4ea82bff69430ca438da5cee7d12c7d33c721bd0bc6e819e06bd8f6de7e85ab0e1141c'
data/lib/tina4/auth.rb CHANGED
@@ -107,19 +107,19 @@ module Tina4
107
107
  end
108
108
 
109
109
 
110
- def valid_token(token)
110
+ def valid_token(token) # -> bool
111
111
  if use_hmac?
112
- hmac_decode(token, hmac_secret)
112
+ !hmac_decode(token, hmac_secret).nil?
113
113
  else
114
114
  ensure_keys
115
115
  require "jwt"
116
- decoded = JWT.decode(token, public_key, true, algorithm: "RS256")
117
- decoded[0]
116
+ JWT.decode(token, public_key, true, algorithm: "RS256")
117
+ true
118
118
  end
119
119
  rescue JWT::ExpiredSignature
120
- nil
120
+ false
121
121
  rescue JWT::DecodeError
122
- nil
122
+ false
123
123
  end
124
124
 
125
125
  def valid_token_detail(token)
@@ -174,9 +174,10 @@ module Tina4
174
174
  end
175
175
 
176
176
  def refresh_token(token, expires_in: 60)
177
- payload = valid_token(token)
178
- return nil unless payload
177
+ return nil unless valid_token(token)
179
178
 
179
+ payload = get_payload(token)
180
+ return nil unless payload
180
181
  payload = payload.reject { |k, _| %w[iat exp nbf].include?(k) }
181
182
  get_token(payload, expires_in: expires_in)
182
183
  end
@@ -193,7 +194,7 @@ module Tina4
193
194
  return { "api_key" => true }
194
195
  end
195
196
 
196
- valid_token(token)
197
+ valid_token(token) ? get_payload(token) : nil
197
198
  end
198
199
 
199
200
  def validate_api_key(provided, expected: nil)
@@ -227,9 +228,8 @@ module Tina4
227
228
  return true
228
229
  end
229
230
 
230
- payload = valid_token(token)
231
- if payload
232
- env["tina4.auth"] = payload
231
+ if valid_token(token)
232
+ env["tina4.auth"] = get_payload(token)
233
233
  true
234
234
  else
235
235
  false
@@ -287,7 +287,7 @@ module Tina4
287
287
  return true if auth_header.empty?
288
288
 
289
289
  if auth_header =~ /\ABearer\s+(.+)\z/i
290
- !valid_token(Regexp.last_match(1)).nil?
290
+ valid_token(Regexp.last_match(1))
291
291
  else
292
292
  false
293
293
  end
@@ -307,8 +307,7 @@ module Tina4
307
307
  if auth_header.start_with?("Bearer ")
308
308
  bearer_token = auth_header[7..].strip
309
309
  unless bearer_token.empty?
310
- payload = Tina4::Auth.valid_token(bearer_token)
311
- return [request, response] if payload
310
+ return [request, response] if Tina4::Auth.valid_token(bearer_token)
312
311
  end
313
312
  end
314
313
 
@@ -344,14 +343,13 @@ module Tina4
344
343
  end
345
344
 
346
345
  # Validate the token
347
- payload = Tina4::Auth.valid_token(token.to_s)
348
-
349
- if payload.nil?
346
+ unless Tina4::Auth.valid_token(token.to_s)
350
347
  response.json({ error: "CSRF_INVALID", message: "Invalid or missing form token" }, 403)
351
348
  return [request, response]
352
349
  end
353
350
 
354
351
  # Session binding — if token has session_id, verify it matches
352
+ payload = Tina4::Auth.get_payload(token.to_s) || {}
355
353
  token_session_id = payload["session_id"]
356
354
  if token_session_id
357
355
  current_session_id = nil
@@ -87,6 +87,23 @@ module Tina4
87
87
  filepath
88
88
  end
89
89
 
90
+ # Get list of applied migration records (public alias for completed_migrations)
91
+ def get_applied
92
+ completed_migrations
93
+ end
94
+
95
+ # Get list of pending migration filenames (public alias for pending_migrations)
96
+ def get_pending
97
+ pending_migrations.map { |f| File.basename(f) }
98
+ end
99
+
100
+ # Get all migration files on disk, excluding .down files
101
+ def get_files
102
+ migration_files = Dir.glob(File.join(@migrations_dir, "*.sql")).reject { |f| f.end_with?(".down.sql") }
103
+ migration_files += Dir.glob(File.join(@migrations_dir, "*.rb"))
104
+ migration_files.map { |f| File.basename(f) }.sort
105
+ end
106
+
90
107
  private
91
108
 
92
109
  # Resolve migrations directory: prefer src/migrations, fall back to migrations/
data/lib/tina4/orm.rb CHANGED
@@ -79,7 +79,7 @@ module Tina4
79
79
  end
80
80
 
81
81
  # has_one :profile, class_name: "Profile", foreign_key: "user_id"
82
- def has_one(name, class_name: nil, foreign_key: nil)
82
+ def has_one(name, class_name: nil, foreign_key: nil) # -> nil
83
83
  relationship_definitions[name] = {
84
84
  type: :has_one,
85
85
  class_name: class_name || name.to_s.split("_").map(&:capitalize).join,
@@ -92,7 +92,7 @@ module Tina4
92
92
  end
93
93
 
94
94
  # has_many :posts, class_name: "Post", foreign_key: "user_id"
95
- def has_many(name, class_name: nil, foreign_key: nil)
95
+ def has_many(name, class_name: nil, foreign_key: nil) # -> nil
96
96
  relationship_definitions[name] = {
97
97
  type: :has_many,
98
98
  class_name: class_name || name.to_s.sub(/s$/, "").split("_").map(&:capitalize).join,
@@ -105,7 +105,7 @@ module Tina4
105
105
  end
106
106
 
107
107
  # belongs_to :user, class_name: "User", foreign_key: "user_id"
108
- def belongs_to(name, class_name: nil, foreign_key: nil)
108
+ def belongs_to(name, class_name: nil, foreign_key: nil) # -> nil
109
109
  relationship_definitions[name] = {
110
110
  type: :belongs_to,
111
111
  class_name: class_name || name.to_s.split("_").map(&:capitalize).join,
@@ -123,7 +123,7 @@ module Tina4
123
123
  # results = User.query.where("active = ?", [1]).order_by("name").get
124
124
  #
125
125
  # @return [Tina4::QueryBuilder]
126
- def query
126
+ def query # -> QueryBuilder
127
127
  QueryBuilder.from(table_name, db: db)
128
128
  end
129
129
 
@@ -136,7 +136,7 @@ module Tina4
136
136
  # User.find → all records
137
137
  #
138
138
  # Use find_by_id(id) for single-record primary key lookup.
139
- def find(filter = {}, limit: 100, offset: 0, order_by: nil, include: nil, **extra_filter)
139
+ def find(filter = {}, limit: 100, offset: 0, order_by: nil, include: nil, **extra_filter) # -> list[Self]
140
140
  # Integer or string-digit argument → primary key lookup (returns single record or nil)
141
141
  return find_by_id(filter) if filter.is_a?(Integer)
142
142
 
@@ -243,7 +243,7 @@ module Tina4
243
243
  end
244
244
  end
245
245
 
246
- def where(conditions, params = [], include: nil)
246
+ def where(conditions, params = [], include: nil) # -> list[Self]
247
247
  sql = "SELECT * FROM #{table_name}"
248
248
  if soft_delete
249
249
  sql += " WHERE (#{soft_delete_field} IS NULL OR #{soft_delete_field} = 0) AND (#{conditions})"
@@ -256,7 +256,7 @@ module Tina4
256
256
  instances
257
257
  end
258
258
 
259
- def all(limit: nil, offset: nil, order_by: nil, include: nil)
259
+ def all(limit: nil, offset: nil, order_by: nil, include: nil) # -> list[Self]
260
260
  sql = "SELECT * FROM #{table_name}"
261
261
  if soft_delete
262
262
  sql += " WHERE #{soft_delete_field} IS NULL OR #{soft_delete_field} = 0"
@@ -268,19 +268,19 @@ module Tina4
268
268
  instances
269
269
  end
270
270
 
271
- def select(sql, params = [], limit: nil, offset: nil, include: nil)
271
+ def select(sql, params = [], limit: nil, offset: nil, include: nil) # -> list[Self]
272
272
  results = db.fetch(sql, params, limit: limit, offset: offset)
273
273
  instances = results.map { |row| from_hash(row) }
274
274
  eager_load(instances, include) if include
275
275
  instances
276
276
  end
277
277
 
278
- def select_one(sql, params = [], include: nil)
278
+ def select_one(sql, params = [], include: nil) # -> Self | nil
279
279
  results = select(sql, params, limit: 1, include: include)
280
280
  results.first
281
281
  end
282
282
 
283
- def count(conditions = nil, params = [])
283
+ def count(conditions = nil, params = []) # -> int
284
284
  sql = "SELECT COUNT(*) as cnt FROM #{table_name}"
285
285
  where_parts = []
286
286
  if soft_delete
@@ -292,25 +292,48 @@ module Tina4
292
292
  result[:cnt].to_i
293
293
  end
294
294
 
295
- def create(attributes = {})
295
+ def create(attributes = {}) # -> Self
296
296
  instance = new(attributes)
297
297
  instance.save
298
298
  instance
299
299
  end
300
300
 
301
- def find_or_fail(id)
301
+ def find_or_fail(id) # -> Self
302
302
  result = find(id)
303
303
  raise "#{name} with #{primary_key_field || :id}=#{id} not found" if result.nil?
304
304
  result
305
305
  end
306
306
 
307
- def with_trashed(conditions = "1=1", params = [], limit: 20, offset: 0)
307
+ # Return true if a record with the given primary key exists.
308
+ def exists(pk_value) # -> bool
309
+ find(pk_value) != nil
310
+ end
311
+
312
+ # SQL query with in-memory result caching.
313
+ # Results are cached by (class, sql, params, limit, offset) for +ttl+ seconds.
314
+ def cached(sql, params = [], ttl: 60, limit: 20, offset: 0, include: nil) # -> list[Self]
315
+ @_query_cache ||= Tina4::QueryCache.new(default_ttl: ttl, max_size: 500)
316
+ cache_key = Tina4::QueryCache.query_key("#{name}:#{sql}", params + [limit, offset])
317
+ hit = @_query_cache.get(cache_key)
318
+ return hit unless hit.nil?
319
+
320
+ results = select(sql, params, limit: limit, offset: offset, include: include)
321
+ @_query_cache.set(cache_key, results, ttl: ttl, tags: [name])
322
+ results
323
+ end
324
+
325
+ # Clear all cached query results for this model.
326
+ def clear_cache # -> nil
327
+ @_query_cache&.clear_tag(name)
328
+ end
329
+
330
+ def with_trashed(conditions = "1=1", params = [], limit: 20, offset: 0) # -> list[Self]
308
331
  sql = "SELECT * FROM #{table_name} WHERE #{conditions}"
309
332
  results = db.fetch(sql, params, limit: limit, offset: offset)
310
333
  results.map { |row| from_hash(row) }
311
334
  end
312
335
 
313
- def create_table
336
+ def create_table # -> bool
314
337
  return true if db.table_exists?(table_name)
315
338
 
316
339
  type_map = {
@@ -351,7 +374,7 @@ module Tina4
351
374
  true
352
375
  end
353
376
 
354
- def scope(name, filter_sql, params = [])
377
+ def scope(name, filter_sql, params = []) # -> nil
355
378
  define_singleton_method(name) do |limit: 20, offset: 0|
356
379
  where(filter_sql, params)
357
380
  end
@@ -371,7 +394,7 @@ module Tina4
371
394
  end
372
395
 
373
396
  # Find a single record by primary key. Returns instance or nil.
374
- def find_by_id(id)
397
+ def find_by_id(id) # -> Self | nil
375
398
  pk = primary_key_field || :id
376
399
  sql = "SELECT * FROM #{table_name} WHERE #{pk} = ?"
377
400
  if soft_delete
@@ -416,7 +439,7 @@ module Tina4
416
439
  end
417
440
  end
418
441
 
419
- def save
442
+ def save # -> Self | bool
420
443
  @errors = []
421
444
  @relationship_cache = {} # Clear relationship cache on save
422
445
  validate_fields
@@ -448,7 +471,7 @@ module Tina4
448
471
  false
449
472
  end
450
473
 
451
- def delete
474
+ def delete # -> bool
452
475
  pk = self.class.primary_key_field || :id
453
476
  pk_value = __send__(pk)
454
477
  return false unless pk_value
@@ -468,7 +491,7 @@ module Tina4
468
491
  true
469
492
  end
470
493
 
471
- def force_delete
494
+ def force_delete # -> bool
472
495
  pk = self.class.primary_key_field || :id
473
496
  pk_value = __send__(pk)
474
497
  raise "Cannot delete: no primary key value" unless pk_value
@@ -480,7 +503,7 @@ module Tina4
480
503
  true
481
504
  end
482
505
 
483
- def restore
506
+ def restore # -> bool
484
507
  raise "Model does not support soft delete" unless self.class.soft_delete
485
508
 
486
509
  pk = self.class.primary_key_field || :id
@@ -498,7 +521,7 @@ module Tina4
498
521
  true
499
522
  end
500
523
 
501
- def validate
524
+ def validate # -> list[str]
502
525
  errors = []
503
526
  self.class.field_definitions.each do |name, opts|
504
527
  value = __send__(name)
@@ -519,7 +542,7 @@ module Tina4
519
542
  # orm.load("id = 1") — filter string
520
543
  #
521
544
  # Returns true if a record was found, false otherwise.
522
- def load(filter = nil, params = [], include: nil)
545
+ def load(filter = nil, params = [], include: nil) # -> bool
523
546
  @relationship_cache = {}
524
547
  table = self.class.table_name
525
548
 
@@ -557,7 +580,7 @@ module Tina4
557
580
 
558
581
  # Convert to hash using Ruby attribute names.
559
582
  # Optionally include relationships via the include keyword.
560
- def to_h(include: nil)
583
+ def to_h(include: nil) # -> dict
561
584
  hash = {}
562
585
  self.class.field_definitions.each_key do |name|
563
586
  hash[name] = __send__(name)
@@ -594,13 +617,13 @@ module Tina4
594
617
  alias to_assoc to_h
595
618
  alias to_object to_h
596
619
 
597
- def to_array
620
+ def to_array # -> list
598
621
  to_h.values
599
622
  end
600
623
 
601
624
  alias to_list to_array
602
625
 
603
- def to_json(include: nil, **_args)
626
+ def to_json(include: nil, **_args) # -> str
604
627
  JSON.generate(to_h(include: include))
605
628
  end
606
629
 
data/lib/tina4/queue.rb CHANGED
@@ -36,49 +36,49 @@ module Tina4
36
36
  end
37
37
 
38
38
  # Pop the next available job. Returns Job or nil.
39
- def pop
39
+ def pop # -> Job|None
40
40
  @backend.dequeue(@topic)
41
41
  end
42
42
 
43
43
  # Clear all pending jobs from this queue's topic. Returns count removed.
44
- def clear
44
+ def clear # -> int
45
45
  return 0 unless @backend.respond_to?(:clear)
46
46
  @backend.clear(@topic)
47
47
  end
48
48
 
49
49
  # Get jobs that failed but are still eligible for retry (under max_retries).
50
- def failed
50
+ def failed # -> list[dict]
51
51
  return [] unless @backend.respond_to?(:failed)
52
52
  @backend.failed(@topic, max_retries: @max_retries)
53
53
  end
54
54
 
55
- # Retry a specific failed job by ID. Returns true if found and re-queued.
56
- def retry(job_id, delay_seconds: 0)
55
+ # Retry failed jobs on this queue's topic. Returns true if re-queued.
56
+ def retry(delay_seconds = 0) # -> bool
57
57
  return false unless @backend.respond_to?(:retry_job)
58
- @backend.retry_job(@topic, job_id, delay_seconds: delay_seconds)
58
+ @backend.retry_job(@topic, delay_seconds: delay_seconds)
59
59
  end
60
60
 
61
61
  # Get dead letter jobs — messages that exceeded max retries.
62
- def dead_letters
62
+ def dead_letters # -> list[dict]
63
63
  return [] unless @backend.respond_to?(:dead_letters)
64
64
  @backend.dead_letters(@topic, max_retries: @max_retries)
65
65
  end
66
66
 
67
67
  # Delete messages by status (completed, failed, dead).
68
- def purge(status)
68
+ def purge(status) # -> int
69
69
  return 0 unless @backend.respond_to?(:purge)
70
70
  @backend.purge(@topic, status)
71
71
  end
72
72
 
73
73
  # Re-queue failed messages (under max_retries) back to pending.
74
74
  # Returns the number of jobs re-queued.
75
- def retry_failed
75
+ def retry_failed # -> int
76
76
  return 0 unless @backend.respond_to?(:retry_failed)
77
77
  @backend.retry_failed(@topic, max_retries: @max_retries)
78
78
  end
79
79
 
80
80
  # Produce a message onto a topic. Convenience wrapper around push().
81
- def produce(topic, payload, priority: 0)
81
+ def produce(topic, payload, priority = 0)
82
82
  message = Job.new(topic: topic, payload: payload, priority: priority)
83
83
  @backend.enqueue(message)
84
84
  message
@@ -218,28 +218,32 @@ module Tina4
218
218
  jobs
219
219
  end
220
220
 
221
- # Retry a specific failed job by ID. Returns true if found and re-queued.
222
- def retry_job(topic, job_id, delay_seconds: 0)
221
+ # Retry all dead letter jobs for this topic. Returns true if any were re-queued.
222
+ def retry_job(topic, delay_seconds: 0)
223
223
  return false unless Dir.exist?(@dead_letter_dir)
224
- file = File.join(@dead_letter_dir, "#{job_id}.json")
225
- return false unless File.exist?(file)
226
-
227
- data = JSON.parse(File.read(file))
228
- return false unless data["topic"] == topic.to_s
229
224
 
230
225
  available_at = delay_seconds > 0 ? Time.now + delay_seconds : nil
231
- msg = Tina4::Job.new(
232
- topic: data["topic"],
233
- payload: data["payload"],
234
- id: data["id"],
235
- attempts: (data["attempts"] || 0) + 1,
236
- available_at: available_at
237
- )
238
- enqueue(msg)
239
- File.delete(file)
240
- true
241
- rescue JSON::ParserError
242
- false
226
+ count = 0
227
+
228
+ Dir.glob(File.join(@dead_letter_dir, "*.json")).each do |file|
229
+ data = JSON.parse(File.read(file))
230
+ next unless data["topic"] == topic.to_s
231
+
232
+ msg = Tina4::Job.new(
233
+ topic: data["topic"],
234
+ payload: data["payload"],
235
+ id: data["id"],
236
+ attempts: (data["attempts"] || 0) + 1,
237
+ available_at: available_at
238
+ )
239
+ enqueue(msg)
240
+ File.delete(file)
241
+ count += 1
242
+ rescue JSON::ParserError
243
+ next
244
+ end
245
+
246
+ count > 0
243
247
  end
244
248
 
245
249
  private
@@ -176,11 +176,10 @@ module Tina4
176
176
  if api_key && !api_key.empty? && token == api_key
177
177
  env["tina4.auth_payload"] = { "api_key" => true }
178
178
  elsif token
179
- payload = Tina4::Auth.valid_token(token)
180
- unless payload
179
+ unless Tina4::Auth.valid_token(token)
181
180
  return [401, { "content-type" => "application/json" }, [JSON.generate({ error: "Unauthorized" })]]
182
181
  end
183
- env["tina4.auth_payload"] = payload
182
+ env["tina4.auth_payload"] = Tina4::Auth.get_payload(token)
184
183
  else
185
184
  return [401, { "content-type" => "application/json" }, [JSON.generate({ error: "Unauthorized" })]]
186
185
  end
data/lib/tina4/session.rb CHANGED
@@ -114,9 +114,10 @@ module Tina4
114
114
  @handler.gc(max_age) if @handler.respond_to?(:gc)
115
115
  end
116
116
 
117
- def cookie_header
117
+ def cookie_header(cookie_name = nil)
118
+ name = cookie_name || @options[:cookie_name]
118
119
  samesite = ENV["TINA4_SESSION_SAMESITE"] || "Lax"
119
- "#{@options[:cookie_name]}=#{@id}; Path=/; HttpOnly; SameSite=#{samesite}; Max-Age=#{@options[:max_age]}"
120
+ "#{name}=#{@id}; Path=/; HttpOnly; SameSite=#{samesite}; Max-Age=#{@options[:max_age]}"
120
121
  end
121
122
 
122
123
  private
@@ -223,9 +224,9 @@ module Tina4
223
224
  @session.gc(max_age)
224
225
  end
225
226
 
226
- def cookie_header
227
+ def cookie_header(cookie_name = nil)
227
228
  ensure_loaded
228
- @session.cookie_header
229
+ @session.cookie_header(cookie_name)
229
230
  end
230
231
 
231
232
  def to_hash
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.83"
4
+ VERSION = "3.10.84"
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.83
4
+ version: 3.10.84
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tina4 Team