tina4ruby 0.5.2 → 3.2.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.
Files changed (90) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1 -1
  3. data/README.md +434 -544
  4. data/exe/{tina4 → tina4ruby} +1 -0
  5. data/lib/tina4/ai.rb +312 -0
  6. data/lib/tina4/auth.rb +44 -3
  7. data/lib/tina4/auto_crud.rb +163 -0
  8. data/lib/tina4/cli.rb +389 -97
  9. data/lib/tina4/constants.rb +46 -0
  10. data/lib/tina4/cors.rb +74 -0
  11. data/lib/tina4/database/sqlite3_adapter.rb +139 -0
  12. data/lib/tina4/database.rb +144 -7
  13. data/lib/tina4/debug.rb +4 -79
  14. data/lib/tina4/dev_admin.rb +1162 -0
  15. data/lib/tina4/dev_mailbox.rb +191 -0
  16. data/lib/tina4/dev_reload.rb +9 -9
  17. data/lib/tina4/drivers/firebird_driver.rb +19 -3
  18. data/lib/tina4/drivers/mssql_driver.rb +3 -3
  19. data/lib/tina4/drivers/mysql_driver.rb +4 -4
  20. data/lib/tina4/drivers/postgres_driver.rb +9 -2
  21. data/lib/tina4/drivers/sqlite_driver.rb +1 -1
  22. data/lib/tina4/env.rb +42 -2
  23. data/lib/tina4/error_overlay.rb +252 -0
  24. data/lib/tina4/events.rb +90 -0
  25. data/lib/tina4/field_types.rb +4 -0
  26. data/lib/tina4/frond.rb +1497 -0
  27. data/lib/tina4/gallery/auth/meta.json +1 -0
  28. data/lib/tina4/gallery/auth/src/routes/api/gallery_auth.rb +114 -0
  29. data/lib/tina4/gallery/database/meta.json +1 -0
  30. data/lib/tina4/gallery/database/src/routes/api/gallery_db.rb +43 -0
  31. data/lib/tina4/gallery/error-overlay/meta.json +1 -0
  32. data/lib/tina4/gallery/error-overlay/src/routes/api/gallery_crash.rb +17 -0
  33. data/lib/tina4/gallery/orm/meta.json +1 -0
  34. data/lib/tina4/gallery/orm/src/routes/api/gallery_products.rb +16 -0
  35. data/lib/tina4/gallery/queue/meta.json +1 -0
  36. data/lib/tina4/gallery/queue/src/routes/api/gallery_queue.rb +325 -0
  37. data/lib/tina4/gallery/rest-api/meta.json +1 -0
  38. data/lib/tina4/gallery/rest-api/src/routes/api/gallery_hello.rb +14 -0
  39. data/lib/tina4/gallery/templates/meta.json +1 -0
  40. data/lib/tina4/gallery/templates/src/routes/gallery_page.rb +12 -0
  41. data/lib/tina4/gallery/templates/src/templates/gallery_page.twig +257 -0
  42. data/lib/tina4/health.rb +39 -0
  43. data/lib/tina4/html_element.rb +148 -0
  44. data/lib/tina4/localization.rb +2 -2
  45. data/lib/tina4/log.rb +203 -0
  46. data/lib/tina4/messenger.rb +562 -0
  47. data/lib/tina4/migration.rb +132 -29
  48. data/lib/tina4/orm.rb +463 -35
  49. data/lib/tina4/public/css/tina4.css +178 -1
  50. data/lib/tina4/public/css/tina4.min.css +1 -2
  51. data/lib/tina4/public/favicon.ico +0 -0
  52. data/lib/tina4/public/images/logo.svg +5 -0
  53. data/lib/tina4/public/images/tina4-logo-icon.webp +0 -0
  54. data/lib/tina4/public/js/frond.min.js +420 -0
  55. data/lib/tina4/public/js/tina4-dev-admin.min.js +367 -0
  56. data/lib/tina4/public/js/tina4.min.js +93 -0
  57. data/lib/tina4/public/swagger/index.html +90 -0
  58. data/lib/tina4/public/swagger/oauth2-redirect.html +63 -0
  59. data/lib/tina4/queue.rb +162 -6
  60. data/lib/tina4/queue_backends/lite_backend.rb +88 -0
  61. data/lib/tina4/rack_app.rb +331 -27
  62. data/lib/tina4/rate_limiter.rb +123 -0
  63. data/lib/tina4/request.rb +61 -15
  64. data/lib/tina4/response.rb +54 -24
  65. data/lib/tina4/response_cache.rb +551 -0
  66. data/lib/tina4/router.rb +90 -15
  67. data/lib/tina4/scss_compiler.rb +2 -2
  68. data/lib/tina4/seeder.rb +56 -61
  69. data/lib/tina4/service_runner.rb +303 -0
  70. data/lib/tina4/session.rb +85 -0
  71. data/lib/tina4/session_handlers/mongo_handler.rb +1 -1
  72. data/lib/tina4/session_handlers/valkey_handler.rb +43 -0
  73. data/lib/tina4/shutdown.rb +84 -0
  74. data/lib/tina4/sql_translation.rb +295 -0
  75. data/lib/tina4/template.rb +36 -6
  76. data/lib/tina4/templates/base.twig +2 -2
  77. data/lib/tina4/templates/errors/302.twig +14 -0
  78. data/lib/tina4/templates/errors/401.twig +9 -0
  79. data/lib/tina4/templates/errors/403.twig +22 -15
  80. data/lib/tina4/templates/errors/404.twig +22 -15
  81. data/lib/tina4/templates/errors/500.twig +31 -15
  82. data/lib/tina4/templates/errors/502.twig +9 -0
  83. data/lib/tina4/templates/errors/503.twig +12 -0
  84. data/lib/tina4/templates/errors/base.twig +37 -0
  85. data/lib/tina4/version.rb +1 -1
  86. data/lib/tina4/webserver.rb +28 -18
  87. data/lib/tina4.rb +118 -21
  88. metadata +68 -8
  89. data/lib/tina4/public/js/tina4.js +0 -134
  90. data/lib/tina4/public/js/tina4helper.js +0 -387
data/lib/tina4/queue.rb CHANGED
@@ -38,25 +38,181 @@ module Tina4
38
38
 
39
39
  class Producer
40
40
  def initialize(backend: nil)
41
- @backend = backend || Tina4::QueueBackends::LiteBackend.new
41
+ @backend = backend || Tina4::Queue.resolve_backend
42
42
  end
43
43
 
44
44
  def publish(topic, payload)
45
45
  message = QueueMessage.new(topic: topic, payload: payload)
46
46
  @backend.enqueue(message)
47
- Tina4::Debug.debug("Message published to #{topic}: #{message.id}")
47
+ Tina4::Log.debug("Message published to #{topic}: #{message.id}")
48
48
  message
49
49
  end
50
50
 
51
51
  def publish_batch(topic, payloads)
52
52
  payloads.map { |p| publish(topic, p) }
53
53
  end
54
+
55
+ # Unified push method matching the cross-framework API.
56
+ def push(topic, payload)
57
+ publish(topic, payload)
58
+ end
59
+ end
60
+
61
+ # Queue — unified wrapper for queue management operations.
62
+ # Auto-detects backend from TINA4_QUEUE_BACKEND env var.
63
+ #
64
+ # Usage:
65
+ # # Auto-detect from env (default: lite/file backend)
66
+ # queue = Queue.new(topic: "tasks")
67
+ #
68
+ # # Explicit backend
69
+ # queue = Queue.new(topic: "tasks", backend: :rabbitmq)
70
+ #
71
+ # # Or pass a backend instance directly (legacy)
72
+ # queue = Queue.new(topic: "tasks", backend: my_backend)
73
+ class Queue
74
+ attr_reader :topic, :max_retries
75
+
76
+ def initialize(topic:, backend: nil, max_retries: 3)
77
+ @topic = topic
78
+ @max_retries = max_retries
79
+ @backend = resolve_backend_arg(backend)
80
+ end
81
+
82
+ # Push a job onto the queue. Returns the QueueMessage.
83
+ def push(payload)
84
+ message = QueueMessage.new(topic: @topic, payload: payload)
85
+ @backend.enqueue(message)
86
+ message
87
+ end
88
+
89
+ # Pop the next available job. Returns QueueMessage or nil.
90
+ def pop
91
+ @backend.dequeue(@topic)
92
+ end
93
+
94
+ # Get dead letter jobs — messages that exceeded max retries.
95
+ def dead_letters
96
+ return [] unless @backend.respond_to?(:dead_letters)
97
+ @backend.dead_letters(@topic, max_retries: @max_retries)
98
+ end
99
+
100
+ # Delete messages by status (completed, failed, dead).
101
+ def purge(status)
102
+ return 0 unless @backend.respond_to?(:purge)
103
+ @backend.purge(@topic, status)
104
+ end
105
+
106
+ # Re-queue failed messages (under max_retries) back to pending.
107
+ # Returns the number of jobs re-queued.
108
+ def retry_failed
109
+ return 0 unless @backend.respond_to?(:retry_failed)
110
+ @backend.retry_failed(@topic, max_retries: @max_retries)
111
+ end
112
+
113
+ # Get the number of pending messages.
114
+ def size
115
+ @backend.size(@topic)
116
+ end
117
+
118
+ # Get the underlying backend instance.
119
+ def backend
120
+ @backend
121
+ end
122
+
123
+ # Resolve the default backend from env vars.
124
+ # Class method so Producer can also use it.
125
+ def self.resolve_backend(name = nil)
126
+ chosen = name || ENV.fetch("TINA4_QUEUE_BACKEND", "lite").downcase.strip
127
+
128
+ case chosen.to_s
129
+ when "lite", "file", "default"
130
+ Tina4::QueueBackends::LiteBackend.new
131
+ when "rabbitmq"
132
+ config = resolve_rabbitmq_config
133
+ Tina4::QueueBackends::RabbitmqBackend.new(config)
134
+ when "kafka"
135
+ config = resolve_kafka_config
136
+ Tina4::QueueBackends::KafkaBackend.new(config)
137
+ else
138
+ raise ArgumentError, "Unknown queue backend: #{chosen.inspect}. Use 'lite', 'rabbitmq', or 'kafka'."
139
+ end
140
+ end
141
+
142
+ private
143
+
144
+ def resolve_backend_arg(backend)
145
+ # If a backend instance is passed directly (legacy), use it
146
+ return backend if backend && !backend.is_a?(Symbol) && !backend.is_a?(String)
147
+ # If a symbol or string name is passed, resolve it
148
+ Queue.resolve_backend(backend)
149
+ end
150
+
151
+ def self.resolve_rabbitmq_config
152
+ config = {}
153
+ url = ENV["TINA4_QUEUE_URL"]
154
+ if url
155
+ config = parse_amqp_url(url)
156
+ end
157
+ config[:host] ||= ENV.fetch("TINA4_RABBITMQ_HOST", "localhost")
158
+ config[:port] ||= (ENV["TINA4_RABBITMQ_PORT"] || 5672).to_i
159
+ config[:username] ||= ENV.fetch("TINA4_RABBITMQ_USERNAME", "guest")
160
+ config[:password] ||= ENV.fetch("TINA4_RABBITMQ_PASSWORD", "guest")
161
+ config[:vhost] ||= ENV.fetch("TINA4_RABBITMQ_VHOST", "/")
162
+ config
163
+ end
164
+
165
+ def self.resolve_kafka_config
166
+ config = {}
167
+ url = ENV["TINA4_QUEUE_URL"]
168
+ if url
169
+ config[:brokers] = url.sub("kafka://", "")
170
+ end
171
+ brokers = ENV["TINA4_KAFKA_BROKERS"]
172
+ config[:brokers] = brokers if brokers
173
+ config[:brokers] ||= "localhost:9092"
174
+ config[:group_id] = ENV.fetch("TINA4_KAFKA_GROUP_ID", "tina4_consumer_group")
175
+ config
176
+ end
177
+
178
+ def self.parse_amqp_url(url)
179
+ config = {}
180
+ url = url.sub("amqp://", "").sub("amqps://", "")
181
+
182
+ if url.include?("@")
183
+ creds, rest = url.split("@", 2)
184
+ if creds.include?(":")
185
+ config[:username], config[:password] = creds.split(":", 2)
186
+ else
187
+ config[:username] = creds
188
+ end
189
+ else
190
+ rest = url
191
+ end
192
+
193
+ if rest.include?("/")
194
+ hostport, vhost = rest.split("/", 2)
195
+ config[:vhost] = vhost.start_with?("/") ? vhost : "/#{vhost}" if vhost && !vhost.empty?
196
+ else
197
+ hostport = rest
198
+ end
199
+
200
+ if hostport.include?(":")
201
+ host, port = hostport.split(":", 2)
202
+ config[:host] = host
203
+ config[:port] = port.to_i
204
+ elsif hostport && !hostport.empty?
205
+ config[:host] = hostport
206
+ end
207
+
208
+ config
209
+ end
54
210
  end
55
211
 
56
212
  class Consumer
57
213
  def initialize(topic:, backend: nil, max_retries: 3)
58
214
  @topic = topic
59
- @backend = backend || Tina4::QueueBackends::LiteBackend.new
215
+ @backend = backend || Tina4::Queue.resolve_backend
60
216
  @max_retries = max_retries
61
217
  @handlers = []
62
218
  @running = false
@@ -68,7 +224,7 @@ module Tina4
68
224
 
69
225
  def start(poll_interval: 1)
70
226
  @running = true
71
- Tina4::Debug.info("Consumer started for topic: #{@topic}")
227
+ Tina4::Log.info("Consumer started for topic: #{@topic}")
72
228
 
73
229
  while @running
74
230
  message = @backend.dequeue(@topic)
@@ -82,7 +238,7 @@ module Tina4
82
238
 
83
239
  def stop
84
240
  @running = false
85
- Tina4::Debug.info("Consumer stopped for topic: #{@topic}")
241
+ Tina4::Log.info("Consumer stopped for topic: #{@topic}")
86
242
  end
87
243
 
88
244
  def process_one
@@ -103,7 +259,7 @@ module Tina4
103
259
  message.status = :completed
104
260
  @backend.acknowledge(message)
105
261
  rescue => e
106
- Tina4::Debug.error("Queue message failed: #{message.id} - #{e.message}")
262
+ Tina4::Log.error("Queue message failed: #{message.id} - #{e.message}")
107
263
  message.status = :failed
108
264
 
109
265
  if message.attempts < @max_retries
@@ -68,6 +68,94 @@ module Tina4
68
68
  .select { |d| File.directory?(File.join(@dir, d)) }
69
69
  end
70
70
 
71
+ # Get dead letter jobs for a topic — messages that exceeded max retries.
72
+ def dead_letters(topic, max_retries: 3)
73
+ return [] unless Dir.exist?(@dead_letter_dir)
74
+
75
+ files = Dir.glob(File.join(@dead_letter_dir, "*.json")).sort_by { |f| File.mtime(f) }
76
+ jobs = []
77
+
78
+ files.each do |file|
79
+ data = JSON.parse(File.read(file))
80
+ next unless data["topic"] == topic.to_s
81
+ data["status"] = "dead"
82
+ jobs << data
83
+ rescue JSON::ParserError
84
+ next
85
+ end
86
+
87
+ jobs
88
+ end
89
+
90
+ # Delete messages by status (completed, failed, dead).
91
+ # For 'dead', removes from the dead_letter directory.
92
+ # For 'failed', removes from the topic directory (re-queued failed messages).
93
+ # Returns the number of jobs purged.
94
+ def purge(topic, status)
95
+ count = 0
96
+
97
+ if status.to_s == "dead"
98
+ return 0 unless Dir.exist?(@dead_letter_dir)
99
+
100
+ Dir.glob(File.join(@dead_letter_dir, "*.json")).each do |file|
101
+ data = JSON.parse(File.read(file))
102
+ if data["topic"] == topic.to_s
103
+ File.delete(file)
104
+ count += 1
105
+ end
106
+ rescue JSON::ParserError
107
+ next
108
+ end
109
+ elsif status.to_s == "failed" || status.to_s == "completed" || status.to_s == "pending"
110
+ dir = topic_path(topic)
111
+ return 0 unless Dir.exist?(dir)
112
+
113
+ Dir.glob(File.join(dir, "*.json")).each do |file|
114
+ data = JSON.parse(File.read(file))
115
+ if data["status"] == status.to_s
116
+ File.delete(file)
117
+ count += 1
118
+ end
119
+ rescue JSON::ParserError
120
+ next
121
+ end
122
+ end
123
+
124
+ count
125
+ end
126
+
127
+ # Re-queue failed messages (under max_retries) back to pending.
128
+ # Returns the number of jobs re-queued.
129
+ def retry_failed(topic, max_retries: 3)
130
+ return 0 unless Dir.exist?(@dead_letter_dir)
131
+
132
+ dir = topic_path(topic)
133
+ FileUtils.mkdir_p(dir)
134
+ count = 0
135
+
136
+ # Dead letter directory contains messages that the Consumer moved there.
137
+ # Only retry those whose attempts are under max_retries.
138
+ Dir.glob(File.join(@dead_letter_dir, "*.json")).each do |file|
139
+ data = JSON.parse(File.read(file))
140
+ next unless data["topic"] == topic.to_s
141
+ next if (data["attempts"] || 0) >= max_retries
142
+
143
+ data["status"] = "pending"
144
+ msg = Tina4::QueueMessage.new(
145
+ topic: data["topic"],
146
+ payload: data["payload"],
147
+ id: data["id"]
148
+ )
149
+ enqueue(msg)
150
+ File.delete(file)
151
+ count += 1
152
+ rescue JSON::ParserError
153
+ next
154
+ end
155
+
156
+ count
157
+ end
158
+
71
159
  private
72
160
 
73
161
  def topic_path(topic)