tina4ruby 0.4.0
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 +7 -0
- data/CHANGELOG.md +80 -0
- data/LICENSE.txt +21 -0
- data/README.md +768 -0
- data/exe/tina4 +4 -0
- data/lib/tina4/api.rb +152 -0
- data/lib/tina4/auth.rb +139 -0
- data/lib/tina4/cli.rb +349 -0
- data/lib/tina4/crud.rb +124 -0
- data/lib/tina4/database.rb +135 -0
- data/lib/tina4/database_result.rb +89 -0
- data/lib/tina4/debug.rb +83 -0
- data/lib/tina4/dev.rb +15 -0
- data/lib/tina4/dev_reload.rb +68 -0
- data/lib/tina4/drivers/firebird_driver.rb +94 -0
- data/lib/tina4/drivers/mssql_driver.rb +112 -0
- data/lib/tina4/drivers/mysql_driver.rb +90 -0
- data/lib/tina4/drivers/postgres_driver.rb +99 -0
- data/lib/tina4/drivers/sqlite_driver.rb +85 -0
- data/lib/tina4/env.rb +55 -0
- data/lib/tina4/field_types.rb +84 -0
- data/lib/tina4/graphql.rb +837 -0
- data/lib/tina4/localization.rb +100 -0
- data/lib/tina4/middleware.rb +59 -0
- data/lib/tina4/migration.rb +124 -0
- data/lib/tina4/orm.rb +168 -0
- data/lib/tina4/public/css/tina4.css +2286 -0
- data/lib/tina4/public/css/tina4.min.css +2 -0
- data/lib/tina4/public/js/tina4.js +134 -0
- data/lib/tina4/public/js/tina4helper.js +387 -0
- data/lib/tina4/queue.rb +117 -0
- data/lib/tina4/queue_backends/kafka_backend.rb +80 -0
- data/lib/tina4/queue_backends/lite_backend.rb +79 -0
- data/lib/tina4/queue_backends/rabbitmq_backend.rb +73 -0
- data/lib/tina4/rack_app.rb +150 -0
- data/lib/tina4/request.rb +158 -0
- data/lib/tina4/response.rb +172 -0
- data/lib/tina4/router.rb +148 -0
- data/lib/tina4/scss/tina4css/_alerts.scss +34 -0
- data/lib/tina4/scss/tina4css/_badges.scss +22 -0
- data/lib/tina4/scss/tina4css/_buttons.scss +69 -0
- data/lib/tina4/scss/tina4css/_cards.scss +49 -0
- data/lib/tina4/scss/tina4css/_forms.scss +156 -0
- data/lib/tina4/scss/tina4css/_grid.scss +81 -0
- data/lib/tina4/scss/tina4css/_modals.scss +84 -0
- data/lib/tina4/scss/tina4css/_nav.scss +149 -0
- data/lib/tina4/scss/tina4css/_reset.scss +94 -0
- data/lib/tina4/scss/tina4css/_tables.scss +54 -0
- data/lib/tina4/scss/tina4css/_typography.scss +55 -0
- data/lib/tina4/scss/tina4css/_utilities.scss +197 -0
- data/lib/tina4/scss/tina4css/_variables.scss +117 -0
- data/lib/tina4/scss/tina4css/base.scss +1 -0
- data/lib/tina4/scss/tina4css/colors.scss +48 -0
- data/lib/tina4/scss/tina4css/tina4.scss +17 -0
- data/lib/tina4/scss_compiler.rb +131 -0
- data/lib/tina4/seeder.rb +529 -0
- data/lib/tina4/session.rb +145 -0
- data/lib/tina4/session_handlers/file_handler.rb +55 -0
- data/lib/tina4/session_handlers/mongo_handler.rb +49 -0
- data/lib/tina4/session_handlers/redis_handler.rb +43 -0
- data/lib/tina4/swagger.rb +123 -0
- data/lib/tina4/template.rb +478 -0
- data/lib/tina4/templates/base.twig +26 -0
- data/lib/tina4/templates/errors/403.twig +22 -0
- data/lib/tina4/templates/errors/404.twig +22 -0
- data/lib/tina4/templates/errors/500.twig +22 -0
- data/lib/tina4/testing.rb +213 -0
- data/lib/tina4/version.rb +5 -0
- data/lib/tina4/webserver.rb +101 -0
- data/lib/tina4/websocket.rb +167 -0
- data/lib/tina4/wsdl.rb +164 -0
- data/lib/tina4.rb +259 -0
- data/lib/tina4ruby.rb +4 -0
- metadata +324 -0
data/lib/tina4/crud.rb
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tina4
|
|
4
|
+
module Crud
|
|
5
|
+
class << self
|
|
6
|
+
def generate_table(records, table_name: "data", primary_key: "id", editable: true)
|
|
7
|
+
return "<p>No records found.</p>" if records.nil? || records.empty?
|
|
8
|
+
|
|
9
|
+
columns = records.first.keys
|
|
10
|
+
|
|
11
|
+
html = <<~HTML
|
|
12
|
+
<div class="table-responsive">
|
|
13
|
+
<table class="table table-striped table-hover" id="crud-#{table_name}">
|
|
14
|
+
<thead class="table-dark"><tr>
|
|
15
|
+
HTML
|
|
16
|
+
|
|
17
|
+
columns.each do |col|
|
|
18
|
+
html += "<th>#{col}</th>"
|
|
19
|
+
end
|
|
20
|
+
html += "<th>Actions</th>" if editable
|
|
21
|
+
html += "</tr></thead><tbody>"
|
|
22
|
+
|
|
23
|
+
records.each do |row|
|
|
24
|
+
pk_value = row[primary_key.to_sym] || row[primary_key.to_s]
|
|
25
|
+
html += "<tr data-id=\"#{pk_value}\">"
|
|
26
|
+
columns.each do |col|
|
|
27
|
+
value = row[col]
|
|
28
|
+
if editable
|
|
29
|
+
html += "<td contenteditable=\"true\" data-field=\"#{col}\">#{value}</td>"
|
|
30
|
+
else
|
|
31
|
+
html += "<td>#{value}</td>"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
if editable
|
|
35
|
+
html += "<td>"
|
|
36
|
+
html += "<button class=\"btn btn-sm btn-primary me-1\" onclick=\"crudSave('#{table_name}', '#{pk_value}')\">Save</button>"
|
|
37
|
+
html += "<button class=\"btn btn-sm btn-danger\" onclick=\"crudDelete('#{table_name}', '#{pk_value}')\">Delete</button>"
|
|
38
|
+
html += "</td>"
|
|
39
|
+
end
|
|
40
|
+
html += "</tr>"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
html += "</tbody></table></div>"
|
|
44
|
+
|
|
45
|
+
if editable
|
|
46
|
+
html += crud_javascript(table_name)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
html
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def generate_form(fields, action: "/", method: "POST", table_name: "data")
|
|
53
|
+
html = "<form action=\"#{action}\" method=\"#{method}\" class=\"needs-validation\" novalidate>"
|
|
54
|
+
html += "<input type=\"hidden\" name=\"_method\" value=\"#{method}\">" if %w[PUT PATCH DELETE].include?(method.upcase)
|
|
55
|
+
|
|
56
|
+
fields.each do |field|
|
|
57
|
+
name = field[:name]
|
|
58
|
+
type = field[:type] || :string
|
|
59
|
+
label = field[:label] || name.to_s.capitalize
|
|
60
|
+
value = field[:value] || ""
|
|
61
|
+
required = field[:required] || false
|
|
62
|
+
|
|
63
|
+
html += "<div class=\"mb-3\">"
|
|
64
|
+
html += "<label for=\"#{name}\" class=\"form-label\">#{label}</label>"
|
|
65
|
+
|
|
66
|
+
case type.to_sym
|
|
67
|
+
when :text
|
|
68
|
+
html += "<textarea class=\"form-control\" id=\"#{name}\" name=\"#{name}\" #{'required' if required}>#{value}</textarea>"
|
|
69
|
+
when :boolean
|
|
70
|
+
checked = value ? "checked" : ""
|
|
71
|
+
html += "<div class=\"form-check\">"
|
|
72
|
+
html += "<input class=\"form-check-input\" type=\"checkbox\" id=\"#{name}\" name=\"#{name}\" #{checked}>"
|
|
73
|
+
html += "</div>"
|
|
74
|
+
when :select
|
|
75
|
+
html += "<select class=\"form-select\" id=\"#{name}\" name=\"#{name}\" #{'required' if required}>"
|
|
76
|
+
(field[:options] || []).each do |opt|
|
|
77
|
+
selected = opt[:value].to_s == value.to_s ? "selected" : ""
|
|
78
|
+
html += "<option value=\"#{opt[:value]}\" #{selected}>#{opt[:label]}</option>"
|
|
79
|
+
end
|
|
80
|
+
html += "</select>"
|
|
81
|
+
when :date
|
|
82
|
+
html += "<input type=\"date\" class=\"form-control\" id=\"#{name}\" name=\"#{name}\" value=\"#{value}\" #{'required' if required}>"
|
|
83
|
+
when :integer, :number
|
|
84
|
+
html += "<input type=\"number\" class=\"form-control\" id=\"#{name}\" name=\"#{name}\" value=\"#{value}\" #{'required' if required}>"
|
|
85
|
+
else
|
|
86
|
+
html += "<input type=\"text\" class=\"form-control\" id=\"#{name}\" name=\"#{name}\" value=\"#{value}\" #{'required' if required}>"
|
|
87
|
+
end
|
|
88
|
+
html += "</div>"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
html += "<button type=\"submit\" class=\"btn btn-primary\">Submit</button>"
|
|
92
|
+
html += "</form>"
|
|
93
|
+
html
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
def crud_javascript(table_name)
|
|
99
|
+
<<~JS
|
|
100
|
+
<script>
|
|
101
|
+
function crudSave(table, id) {
|
|
102
|
+
const row = document.querySelector(`tr[data-id="${id}"]`);
|
|
103
|
+
const cells = row.querySelectorAll('td[data-field]');
|
|
104
|
+
const data = {};
|
|
105
|
+
cells.forEach(cell => { data[cell.dataset.field] = cell.textContent; });
|
|
106
|
+
fetch(`/api/${table}/${id}`, {
|
|
107
|
+
method: 'PUT',
|
|
108
|
+
headers: { 'Content-Type': 'application/json' },
|
|
109
|
+
body: JSON.stringify(data)
|
|
110
|
+
}).then(r => r.json()).then(d => { alert('Saved!'); }).catch(e => alert('Error: ' + e));
|
|
111
|
+
}
|
|
112
|
+
function crudDelete(table, id) {
|
|
113
|
+
if (!confirm('Delete this record?')) return;
|
|
114
|
+
fetch(`/api/${table}/${id}`, { method: 'DELETE' })
|
|
115
|
+
.then(r => r.json())
|
|
116
|
+
.then(d => { document.querySelector(`tr[data-id="${id}"]`).remove(); })
|
|
117
|
+
.catch(e => alert('Error: ' + e));
|
|
118
|
+
}
|
|
119
|
+
</script>
|
|
120
|
+
JS
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require "json"
|
|
3
|
+
|
|
4
|
+
module Tina4
|
|
5
|
+
class Database
|
|
6
|
+
attr_reader :driver, :driver_name, :connected
|
|
7
|
+
|
|
8
|
+
DRIVERS = {
|
|
9
|
+
"sqlite" => "Tina4::Drivers::SqliteDriver",
|
|
10
|
+
"sqlite3" => "Tina4::Drivers::SqliteDriver",
|
|
11
|
+
"postgres" => "Tina4::Drivers::PostgresDriver",
|
|
12
|
+
"postgresql" => "Tina4::Drivers::PostgresDriver",
|
|
13
|
+
"mysql" => "Tina4::Drivers::MysqlDriver",
|
|
14
|
+
"mysql2" => "Tina4::Drivers::MysqlDriver",
|
|
15
|
+
"mssql" => "Tina4::Drivers::MssqlDriver",
|
|
16
|
+
"sqlserver" => "Tina4::Drivers::MssqlDriver",
|
|
17
|
+
"firebird" => "Tina4::Drivers::FirebirdDriver"
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
20
|
+
def initialize(connection_string, driver_name: nil)
|
|
21
|
+
@connection_string = connection_string
|
|
22
|
+
@driver_name = driver_name || detect_driver(connection_string)
|
|
23
|
+
@driver = create_driver
|
|
24
|
+
@connected = false
|
|
25
|
+
connect
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def connect
|
|
29
|
+
@driver.connect(@connection_string)
|
|
30
|
+
@connected = true
|
|
31
|
+
Tina4::Debug.info("Database connected: #{@driver_name}")
|
|
32
|
+
rescue => e
|
|
33
|
+
Tina4::Debug.error("Database connection failed: #{e.message}")
|
|
34
|
+
@connected = false
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def close
|
|
38
|
+
@driver.close if @connected
|
|
39
|
+
@connected = false
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def fetch(sql, params = [], limit: nil, skip: nil)
|
|
43
|
+
effective_sql = sql
|
|
44
|
+
if limit
|
|
45
|
+
effective_sql = @driver.apply_limit(effective_sql, limit, skip || 0)
|
|
46
|
+
end
|
|
47
|
+
rows = @driver.execute_query(effective_sql, params)
|
|
48
|
+
Tina4::DatabaseResult.new(rows, sql: effective_sql)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def fetch_one(sql, params = [])
|
|
52
|
+
result = fetch(sql, params, limit: 1)
|
|
53
|
+
result.first
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def insert(table, data)
|
|
57
|
+
columns = data.keys.map(&:to_s)
|
|
58
|
+
placeholders = @driver.placeholders(columns.length)
|
|
59
|
+
sql = "INSERT INTO #{table} (#{columns.join(', ')}) VALUES (#{placeholders})"
|
|
60
|
+
@driver.execute(sql, data.values)
|
|
61
|
+
{ success: true, last_id: @driver.last_insert_id }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def update(table, data, filter = {})
|
|
65
|
+
set_parts = data.keys.map { |k| "#{k} = #{@driver.placeholder}" }
|
|
66
|
+
where_parts = filter.keys.map { |k| "#{k} = #{@driver.placeholder}" }
|
|
67
|
+
sql = "UPDATE #{table} SET #{set_parts.join(', ')}"
|
|
68
|
+
sql += " WHERE #{where_parts.join(' AND ')}" unless filter.empty?
|
|
69
|
+
values = data.values + filter.values
|
|
70
|
+
@driver.execute(sql, values)
|
|
71
|
+
{ success: true }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def delete(table, filter = {})
|
|
75
|
+
where_parts = filter.keys.map { |k| "#{k} = #{@driver.placeholder}" }
|
|
76
|
+
sql = "DELETE FROM #{table}"
|
|
77
|
+
sql += " WHERE #{where_parts.join(' AND ')}" unless filter.empty?
|
|
78
|
+
@driver.execute(sql, filter.values)
|
|
79
|
+
{ success: true }
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def execute(sql, params = [])
|
|
83
|
+
@driver.execute(sql, params)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def transaction
|
|
87
|
+
@driver.begin_transaction
|
|
88
|
+
yield self
|
|
89
|
+
@driver.commit
|
|
90
|
+
rescue => e
|
|
91
|
+
@driver.rollback
|
|
92
|
+
raise e
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def tables
|
|
96
|
+
@driver.tables
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def columns(table_name)
|
|
100
|
+
@driver.columns(table_name)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def table_exists?(table_name)
|
|
104
|
+
tables.any? { |t| t.downcase == table_name.to_s.downcase }
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
private
|
|
108
|
+
|
|
109
|
+
def detect_driver(conn)
|
|
110
|
+
case conn.to_s.downcase
|
|
111
|
+
when /\.db$/, /\.sqlite/, /sqlite/
|
|
112
|
+
"sqlite"
|
|
113
|
+
when /postgres/, /^pg:/
|
|
114
|
+
"postgres"
|
|
115
|
+
when /mysql/
|
|
116
|
+
"mysql"
|
|
117
|
+
when /mssql/, /sqlserver/
|
|
118
|
+
"mssql"
|
|
119
|
+
when /firebird/, /\.fdb$/
|
|
120
|
+
"firebird"
|
|
121
|
+
else
|
|
122
|
+
"sqlite"
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def create_driver
|
|
127
|
+
klass_name = DRIVERS[@driver_name]
|
|
128
|
+
raise "Unknown database driver: #{@driver_name}" unless klass_name
|
|
129
|
+
klass = Object.const_get(klass_name)
|
|
130
|
+
klass.new
|
|
131
|
+
rescue NameError
|
|
132
|
+
raise "Driver #{klass_name} not loaded. Install the required gem."
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require "json"
|
|
3
|
+
|
|
4
|
+
module Tina4
|
|
5
|
+
class DatabaseResult
|
|
6
|
+
include Enumerable
|
|
7
|
+
|
|
8
|
+
attr_reader :records, :sql, :count
|
|
9
|
+
|
|
10
|
+
def initialize(records = [], sql: "")
|
|
11
|
+
@records = records || []
|
|
12
|
+
@sql = sql
|
|
13
|
+
@count = @records.length
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def each(&block)
|
|
17
|
+
@records.each(&block)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def first
|
|
21
|
+
@records.first
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def last
|
|
25
|
+
@records.last
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def empty?
|
|
29
|
+
@records.empty?
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def [](index)
|
|
33
|
+
@records[index]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def to_array
|
|
37
|
+
@records.map do |record|
|
|
38
|
+
record.is_a?(Hash) ? record : record.to_h
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def to_json(*_args)
|
|
43
|
+
JSON.generate(to_array)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def to_csv(separator: ",", headers: true)
|
|
47
|
+
return "" if @records.empty?
|
|
48
|
+
lines = []
|
|
49
|
+
cols = @records.first.keys
|
|
50
|
+
lines << cols.join(separator) if headers
|
|
51
|
+
@records.each do |row|
|
|
52
|
+
lines << cols.map { |c| escape_csv(row[c], separator) }.join(separator)
|
|
53
|
+
end
|
|
54
|
+
lines.join("\n")
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def to_paginate(page: 1, per_page: 10)
|
|
58
|
+
total = @records.length
|
|
59
|
+
total_pages = (total.to_f / per_page).ceil
|
|
60
|
+
offset = (page - 1) * per_page
|
|
61
|
+
page_records = @records[offset, per_page] || []
|
|
62
|
+
{
|
|
63
|
+
data: page_records,
|
|
64
|
+
page: page,
|
|
65
|
+
per_page: per_page,
|
|
66
|
+
total: total,
|
|
67
|
+
total_pages: total_pages,
|
|
68
|
+
has_next: page < total_pages,
|
|
69
|
+
has_prev: page > 1
|
|
70
|
+
}
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def to_crud(table_name: "data", primary_key: "id", editable: true)
|
|
74
|
+
Tina4::Crud.generate_table(@records, table_name: table_name,
|
|
75
|
+
primary_key: primary_key, editable: editable)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def escape_csv(value, separator)
|
|
81
|
+
str = value.to_s
|
|
82
|
+
if str.include?(separator) || str.include?('"') || str.include?("\n")
|
|
83
|
+
"\"#{str.gsub('"', '""')}\""
|
|
84
|
+
else
|
|
85
|
+
str
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
data/lib/tina4/debug.rb
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require "logger"
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module Tina4
|
|
6
|
+
module Debug
|
|
7
|
+
LEVELS = {
|
|
8
|
+
"[TINA4_LOG_ALL]" => Logger::DEBUG,
|
|
9
|
+
"[TINA4_LOG_DEBUG]" => Logger::DEBUG,
|
|
10
|
+
"[TINA4_LOG_INFO]" => Logger::INFO,
|
|
11
|
+
"[TINA4_LOG_WARNING]" => Logger::WARN,
|
|
12
|
+
"[TINA4_LOG_ERROR]" => Logger::ERROR,
|
|
13
|
+
"[TINA4_LOG_NONE]" => Logger::FATAL
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
16
|
+
COLORS = {
|
|
17
|
+
reset: "\e[0m", red: "\e[31m", green: "\e[32m",
|
|
18
|
+
yellow: "\e[33m", blue: "\e[34m", magenta: "\e[35m",
|
|
19
|
+
cyan: "\e[36m", gray: "\e[90m"
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
22
|
+
class << self
|
|
23
|
+
def setup(root_dir = Dir.pwd)
|
|
24
|
+
log_dir = File.join(root_dir, "logs")
|
|
25
|
+
FileUtils.mkdir_p(log_dir)
|
|
26
|
+
log_file = File.join(log_dir, "debug.log")
|
|
27
|
+
@file_logger = Logger.new(log_file, 10, 5 * 1024 * 1024)
|
|
28
|
+
@file_logger.level = Logger::DEBUG
|
|
29
|
+
@console_logger = Logger.new($stdout)
|
|
30
|
+
@console_logger.level = resolve_level
|
|
31
|
+
@console_logger.formatter = method(:color_formatter)
|
|
32
|
+
@file_logger.formatter = method(:plain_formatter)
|
|
33
|
+
@initialized = true
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def info(message, *args)
|
|
37
|
+
log(:info, message, *args)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def debug(message, *args)
|
|
41
|
+
log(:debug, message, *args)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def warning(message, *args)
|
|
45
|
+
log(:warn, message, *args)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def error(message, *args)
|
|
49
|
+
log(:error, message, *args)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def log(level, message, *args)
|
|
55
|
+
setup unless @initialized
|
|
56
|
+
full_message = args.empty? ? message.to_s : "#{message} #{args.map(&:to_s).join(' ')}"
|
|
57
|
+
@console_logger.send(level, full_message)
|
|
58
|
+
@file_logger.send(level, full_message)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def resolve_level
|
|
62
|
+
env_level = ENV["TINA4_DEBUG_LEVEL"] || "[TINA4_LOG_ALL]"
|
|
63
|
+
LEVELS[env_level] || Logger::DEBUG
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def color_formatter(severity, datetime, _progname, message)
|
|
67
|
+
color = case severity
|
|
68
|
+
when "DEBUG" then COLORS[:gray]
|
|
69
|
+
when "INFO" then COLORS[:green]
|
|
70
|
+
when "WARN" then COLORS[:yellow]
|
|
71
|
+
when "ERROR" then COLORS[:red]
|
|
72
|
+
else COLORS[:reset]
|
|
73
|
+
end
|
|
74
|
+
ts = datetime.strftime("%Y-%m-%d %H:%M:%S")
|
|
75
|
+
"#{COLORS[:gray]}[#{ts}]#{COLORS[:reset]} #{color}[#{severity}]#{COLORS[:reset]} #{message}\n"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def plain_formatter(severity, datetime, _progname, message)
|
|
79
|
+
"[#{datetime.strftime('%Y-%m-%d %H:%M:%S')}] [#{severity}] #{message}\n"
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
data/lib/tina4/dev.rb
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Convenience require for all development/optional tools.
|
|
4
|
+
# Usage: require "tina4/dev"
|
|
5
|
+
|
|
6
|
+
require_relative "dev_reload"
|
|
7
|
+
require_relative "scss_compiler"
|
|
8
|
+
require_relative "testing"
|
|
9
|
+
require_relative "graphql"
|
|
10
|
+
require_relative "websocket"
|
|
11
|
+
require_relative "wsdl"
|
|
12
|
+
require_relative "swagger"
|
|
13
|
+
require_relative "seeder"
|
|
14
|
+
require_relative "crud"
|
|
15
|
+
require_relative "api"
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tina4
|
|
4
|
+
module DevReload
|
|
5
|
+
WATCH_EXTENSIONS = %w[.rb .twig .html .erb .scss .css .js].freeze
|
|
6
|
+
WATCH_DIRS = %w[src routes lib templates public].freeze
|
|
7
|
+
IGNORE_DIRS = %w[.git node_modules vendor logs sessions .queue .keys].freeze
|
|
8
|
+
|
|
9
|
+
class << self
|
|
10
|
+
def start(root_dir: Dir.pwd, &on_change)
|
|
11
|
+
require "listen"
|
|
12
|
+
|
|
13
|
+
dirs = WATCH_DIRS
|
|
14
|
+
.map { |d| File.join(root_dir, d) }
|
|
15
|
+
.select { |d| Dir.exist?(d) }
|
|
16
|
+
|
|
17
|
+
# Also watch root for .rb files
|
|
18
|
+
dirs << root_dir
|
|
19
|
+
|
|
20
|
+
Tina4::Debug.info("Dev reload watching: #{dirs.join(', ')}")
|
|
21
|
+
|
|
22
|
+
@listener = Listen.to(*dirs, only: /\.(#{WATCH_EXTENSIONS.map { |e| e.delete('.') }.join('|')})$/, ignore: build_ignore_regex) do |modified, added, removed|
|
|
23
|
+
changes = { modified: modified, added: added, removed: removed }
|
|
24
|
+
all_files = modified + added + removed
|
|
25
|
+
next if all_files.empty?
|
|
26
|
+
|
|
27
|
+
Tina4::Debug.info("File changes detected:")
|
|
28
|
+
modified.each { |f| Tina4::Debug.debug(" Modified: #{f}") }
|
|
29
|
+
added.each { |f| Tina4::Debug.debug(" Added: #{f}") }
|
|
30
|
+
removed.each { |f| Tina4::Debug.debug(" Removed: #{f}") }
|
|
31
|
+
|
|
32
|
+
# Reload Ruby files
|
|
33
|
+
modified.select { |f| f.end_with?(".rb") }.each do |file|
|
|
34
|
+
begin
|
|
35
|
+
load file
|
|
36
|
+
Tina4::Debug.info("Reloaded: #{file}")
|
|
37
|
+
rescue => e
|
|
38
|
+
Tina4::Debug.error("Reload failed: #{file} - #{e.message}")
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Recompile SCSS
|
|
43
|
+
scss_changes = all_files.select { |f| f.end_with?(".scss") }
|
|
44
|
+
if scss_changes.any?
|
|
45
|
+
Tina4::ScssCompiler.compile_all(root_dir)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
on_change&.call(changes)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
@listener.start
|
|
52
|
+
Tina4::Debug.info("Dev reload started")
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def stop
|
|
56
|
+
@listener&.stop
|
|
57
|
+
Tina4::Debug.info("Dev reload stopped")
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def build_ignore_regex
|
|
63
|
+
pattern = IGNORE_DIRS.map { |d| Regexp.escape(d) }.join("|")
|
|
64
|
+
/#{pattern}/
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tina4
|
|
4
|
+
module Drivers
|
|
5
|
+
class FirebirdDriver
|
|
6
|
+
attr_reader :connection
|
|
7
|
+
|
|
8
|
+
def connect(connection_string)
|
|
9
|
+
require "fb"
|
|
10
|
+
db_path = connection_string.sub(/^firebird:\/\//, "")
|
|
11
|
+
@connection = Fb::Database.new(database: db_path).connect
|
|
12
|
+
rescue LoadError
|
|
13
|
+
raise "Firebird driver requires the 'fb' gem. Install it with: gem install fb"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def close
|
|
17
|
+
@connection&.close
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def execute_query(sql, params = [])
|
|
21
|
+
if params.empty?
|
|
22
|
+
@connection.query(:hash, sql)
|
|
23
|
+
else
|
|
24
|
+
@connection.query(:hash, sql, *params)
|
|
25
|
+
end.map { |row| stringify_keys(row) }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def execute(sql, params = [])
|
|
29
|
+
if params.empty?
|
|
30
|
+
@connection.execute(sql)
|
|
31
|
+
else
|
|
32
|
+
@connection.execute(sql, *params)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def last_insert_id
|
|
37
|
+
nil
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def placeholder
|
|
41
|
+
"?"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def placeholders(count)
|
|
45
|
+
(["?"] * count).join(", ")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def apply_limit(sql, limit, offset = 0)
|
|
49
|
+
"SELECT FIRST #{limit} SKIP #{offset} * FROM (#{sql})"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def begin_transaction
|
|
53
|
+
@transaction = @connection.transaction
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def commit
|
|
57
|
+
@transaction&.commit
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def rollback
|
|
61
|
+
@transaction&.rollback
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def tables
|
|
65
|
+
sql = "SELECT RDB\$RELATION_NAME FROM RDB\$RELATIONS WHERE RDB\$SYSTEM_FLAG = 0 AND RDB\$VIEW_BLR IS NULL"
|
|
66
|
+
rows = execute_query(sql)
|
|
67
|
+
rows.map { |r| (r["RDB\$RELATION_NAME"] || r["rdb\$relation_name"] || "").strip }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def columns(table_name)
|
|
71
|
+
sql = "SELECT RF.RDB\$FIELD_NAME, F.RDB\$FIELD_TYPE, RF.RDB\$NULL_FLAG, RF.RDB\$DEFAULT_SOURCE " \
|
|
72
|
+
"FROM RDB\$RELATION_FIELDS RF " \
|
|
73
|
+
"JOIN RDB\$FIELDS F ON RF.RDB\$FIELD_SOURCE = F.RDB\$FIELD_NAME " \
|
|
74
|
+
"WHERE RF.RDB\$RELATION_NAME = ?"
|
|
75
|
+
rows = execute_query(sql, [table_name.upcase])
|
|
76
|
+
rows.map do |r|
|
|
77
|
+
{
|
|
78
|
+
name: (r["RDB\$FIELD_NAME"] || r["rdb\$field_name"] || "").strip,
|
|
79
|
+
type: r["RDB\$FIELD_TYPE"] || r["rdb\$field_type"],
|
|
80
|
+
nullable: (r["RDB\$NULL_FLAG"] || r["rdb\$null_flag"]).nil?,
|
|
81
|
+
default: r["RDB\$DEFAULT_SOURCE"] || r["rdb\$default_source"],
|
|
82
|
+
primary_key: false
|
|
83
|
+
}
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
def stringify_keys(hash)
|
|
90
|
+
hash.each_with_object({}) { |(k, v), h| h[k.to_s] = v }
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|