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.
Files changed (74) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +80 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +768 -0
  5. data/exe/tina4 +4 -0
  6. data/lib/tina4/api.rb +152 -0
  7. data/lib/tina4/auth.rb +139 -0
  8. data/lib/tina4/cli.rb +349 -0
  9. data/lib/tina4/crud.rb +124 -0
  10. data/lib/tina4/database.rb +135 -0
  11. data/lib/tina4/database_result.rb +89 -0
  12. data/lib/tina4/debug.rb +83 -0
  13. data/lib/tina4/dev.rb +15 -0
  14. data/lib/tina4/dev_reload.rb +68 -0
  15. data/lib/tina4/drivers/firebird_driver.rb +94 -0
  16. data/lib/tina4/drivers/mssql_driver.rb +112 -0
  17. data/lib/tina4/drivers/mysql_driver.rb +90 -0
  18. data/lib/tina4/drivers/postgres_driver.rb +99 -0
  19. data/lib/tina4/drivers/sqlite_driver.rb +85 -0
  20. data/lib/tina4/env.rb +55 -0
  21. data/lib/tina4/field_types.rb +84 -0
  22. data/lib/tina4/graphql.rb +837 -0
  23. data/lib/tina4/localization.rb +100 -0
  24. data/lib/tina4/middleware.rb +59 -0
  25. data/lib/tina4/migration.rb +124 -0
  26. data/lib/tina4/orm.rb +168 -0
  27. data/lib/tina4/public/css/tina4.css +2286 -0
  28. data/lib/tina4/public/css/tina4.min.css +2 -0
  29. data/lib/tina4/public/js/tina4.js +134 -0
  30. data/lib/tina4/public/js/tina4helper.js +387 -0
  31. data/lib/tina4/queue.rb +117 -0
  32. data/lib/tina4/queue_backends/kafka_backend.rb +80 -0
  33. data/lib/tina4/queue_backends/lite_backend.rb +79 -0
  34. data/lib/tina4/queue_backends/rabbitmq_backend.rb +73 -0
  35. data/lib/tina4/rack_app.rb +150 -0
  36. data/lib/tina4/request.rb +158 -0
  37. data/lib/tina4/response.rb +172 -0
  38. data/lib/tina4/router.rb +148 -0
  39. data/lib/tina4/scss/tina4css/_alerts.scss +34 -0
  40. data/lib/tina4/scss/tina4css/_badges.scss +22 -0
  41. data/lib/tina4/scss/tina4css/_buttons.scss +69 -0
  42. data/lib/tina4/scss/tina4css/_cards.scss +49 -0
  43. data/lib/tina4/scss/tina4css/_forms.scss +156 -0
  44. data/lib/tina4/scss/tina4css/_grid.scss +81 -0
  45. data/lib/tina4/scss/tina4css/_modals.scss +84 -0
  46. data/lib/tina4/scss/tina4css/_nav.scss +149 -0
  47. data/lib/tina4/scss/tina4css/_reset.scss +94 -0
  48. data/lib/tina4/scss/tina4css/_tables.scss +54 -0
  49. data/lib/tina4/scss/tina4css/_typography.scss +55 -0
  50. data/lib/tina4/scss/tina4css/_utilities.scss +197 -0
  51. data/lib/tina4/scss/tina4css/_variables.scss +117 -0
  52. data/lib/tina4/scss/tina4css/base.scss +1 -0
  53. data/lib/tina4/scss/tina4css/colors.scss +48 -0
  54. data/lib/tina4/scss/tina4css/tina4.scss +17 -0
  55. data/lib/tina4/scss_compiler.rb +131 -0
  56. data/lib/tina4/seeder.rb +529 -0
  57. data/lib/tina4/session.rb +145 -0
  58. data/lib/tina4/session_handlers/file_handler.rb +55 -0
  59. data/lib/tina4/session_handlers/mongo_handler.rb +49 -0
  60. data/lib/tina4/session_handlers/redis_handler.rb +43 -0
  61. data/lib/tina4/swagger.rb +123 -0
  62. data/lib/tina4/template.rb +478 -0
  63. data/lib/tina4/templates/base.twig +26 -0
  64. data/lib/tina4/templates/errors/403.twig +22 -0
  65. data/lib/tina4/templates/errors/404.twig +22 -0
  66. data/lib/tina4/templates/errors/500.twig +22 -0
  67. data/lib/tina4/testing.rb +213 -0
  68. data/lib/tina4/version.rb +5 -0
  69. data/lib/tina4/webserver.rb +101 -0
  70. data/lib/tina4/websocket.rb +167 -0
  71. data/lib/tina4/wsdl.rb +164 -0
  72. data/lib/tina4.rb +259 -0
  73. data/lib/tina4ruby.rb +4 -0
  74. metadata +324 -0
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+ require "json"
3
+
4
+ module Tina4
5
+ module Localization
6
+ LOCALE_DIRS = %w[locales translations i18n src/locales].freeze
7
+
8
+ class << self
9
+ def translations
10
+ @translations ||= {}
11
+ end
12
+
13
+ def current_locale
14
+ @current_locale || ENV["TINA4_LANGUAGE"] || "en"
15
+ end
16
+
17
+ def current_locale=(locale)
18
+ @current_locale = locale.to_s
19
+ end
20
+
21
+ def load(root_dir = Dir.pwd)
22
+ LOCALE_DIRS.each do |dir|
23
+ locale_dir = File.join(root_dir, dir)
24
+ next unless Dir.exist?(locale_dir)
25
+
26
+ Dir.glob(File.join(locale_dir, "*.json")).each do |file|
27
+ locale = File.basename(file, ".json")
28
+ data = JSON.parse(File.read(file))
29
+ translations[locale] ||= {}
30
+ translations[locale].merge!(data)
31
+ Tina4::Debug.debug("Loaded locale: #{locale} from #{file}")
32
+ end
33
+
34
+ # Also support YAML
35
+ Dir.glob(File.join(locale_dir, "*.{yml,yaml}")).each do |file|
36
+ begin
37
+ require "yaml"
38
+ locale = File.basename(file, File.extname(file))
39
+ data = YAML.safe_load(File.read(file))
40
+ translations[locale] ||= {}
41
+ translations[locale].merge!(data) if data.is_a?(Hash)
42
+ rescue LoadError
43
+ Tina4::Debug.warning("YAML support requires the 'yaml' gem")
44
+ end
45
+ end
46
+ end
47
+ end
48
+
49
+ def t(key, locale: nil, default: nil, **interpolations)
50
+ lang = locale || current_locale
51
+ value = lookup(lang, key)
52
+
53
+ if value.nil? && lang != "en"
54
+ value = lookup("en", key)
55
+ end
56
+
57
+ value = default || key if value.nil?
58
+
59
+ # Interpolation: "Hello %{name}" => "Hello World"
60
+ interpolations.each do |k, v|
61
+ value = value.gsub("%{#{k}}", v.to_s)
62
+ end
63
+
64
+ value
65
+ end
66
+
67
+ def add(locale, key, value)
68
+ translations[locale.to_s] ||= {}
69
+ keys = key.to_s.split(".")
70
+ hash = translations[locale.to_s]
71
+ keys[0..-2].each do |k|
72
+ hash[k] ||= {}
73
+ hash = hash[k]
74
+ end
75
+ hash[keys.last] = value
76
+ end
77
+
78
+ def available_locales
79
+ translations.keys
80
+ end
81
+
82
+ private
83
+
84
+ def lookup(locale, key)
85
+ keys = key.to_s.split(".")
86
+ result = translations[locale]
87
+ return nil unless result
88
+
89
+ keys.each do |k|
90
+ if result.is_a?(Hash)
91
+ result = result[k] || result[k.to_sym]
92
+ else
93
+ return nil
94
+ end
95
+ end
96
+ result.is_a?(String) ? result : nil
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tina4
4
+ class Middleware
5
+ class << self
6
+ def before_handlers
7
+ @before_handlers ||= []
8
+ end
9
+
10
+ def after_handlers
11
+ @after_handlers ||= []
12
+ end
13
+
14
+ def before(pattern = nil, &block)
15
+ before_handlers << { pattern: pattern, handler: block }
16
+ end
17
+
18
+ def after(pattern = nil, &block)
19
+ after_handlers << { pattern: pattern, handler: block }
20
+ end
21
+
22
+ def clear!
23
+ @before_handlers = []
24
+ @after_handlers = []
25
+ end
26
+
27
+ def run_before(request, response)
28
+ before_handlers.each do |entry|
29
+ next unless matches_pattern?(request.path, entry[:pattern])
30
+ result = entry[:handler].call(request, response)
31
+ # If handler returns false, halt the request
32
+ return false if result == false
33
+ end
34
+ true
35
+ end
36
+
37
+ def run_after(request, response)
38
+ after_handlers.each do |entry|
39
+ next unless matches_pattern?(request.path, entry[:pattern])
40
+ entry[:handler].call(request, response)
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ def matches_pattern?(path, pattern)
47
+ return true if pattern.nil?
48
+ case pattern
49
+ when String
50
+ path.start_with?(pattern)
51
+ when Regexp
52
+ pattern.match?(path)
53
+ else
54
+ true
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+ require "fileutils"
3
+
4
+ module Tina4
5
+ class Migration
6
+ TRACKING_TABLE = "tina4_migrations"
7
+
8
+ def initialize(db, migrations_dir: nil)
9
+ @db = db
10
+ @migrations_dir = migrations_dir || File.join(Dir.pwd, "migrations")
11
+ ensure_tracking_table
12
+ end
13
+
14
+ def run
15
+ pending = pending_migrations
16
+ if pending.empty?
17
+ Tina4::Debug.info("No pending migrations")
18
+ return []
19
+ end
20
+
21
+ results = []
22
+ pending.each do |file|
23
+ result = run_migration(file)
24
+ results << result
25
+ end
26
+ results
27
+ end
28
+
29
+ def rollback(steps = 1)
30
+ completed = completed_migrations.last(steps)
31
+ completed.reverse.each do |name|
32
+ down_file = File.join(@migrations_dir, name.sub(".sql", ".down.sql"))
33
+ if File.exist?(down_file)
34
+ execute_sql_file(down_file)
35
+ remove_migration_record(name)
36
+ Tina4::Debug.info("Rolled back: #{name}")
37
+ else
38
+ Tina4::Debug.warning("No rollback file for: #{name}")
39
+ end
40
+ end
41
+ end
42
+
43
+ def status
44
+ {
45
+ completed: completed_migrations,
46
+ pending: pending_migrations.map { |f| File.basename(f) }
47
+ }
48
+ end
49
+
50
+ def create(name)
51
+ FileUtils.mkdir_p(@migrations_dir)
52
+ timestamp = Time.now.strftime("%Y%m%d%H%M%S")
53
+ filename = "#{timestamp}_#{name.gsub(/\s+/, '_')}.sql"
54
+ filepath = File.join(@migrations_dir, filename)
55
+ File.write(filepath, "-- Migration: #{name}\n-- Created: #{Time.now}\n\n")
56
+
57
+ down_filepath = filepath.sub(".sql", ".down.sql")
58
+ File.write(down_filepath, "-- Rollback: #{name}\n-- Created: #{Time.now}\n\n")
59
+
60
+ Tina4::Debug.info("Created migration: #{filename}")
61
+ filepath
62
+ end
63
+
64
+ private
65
+
66
+ def ensure_tracking_table
67
+ unless @db.table_exists?(TRACKING_TABLE)
68
+ @db.execute(<<~SQL)
69
+ CREATE TABLE #{TRACKING_TABLE} (
70
+ id INTEGER PRIMARY KEY,
71
+ migration_name VARCHAR(255) NOT NULL,
72
+ executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
73
+ )
74
+ SQL
75
+ Tina4::Debug.info("Created migrations tracking table")
76
+ end
77
+ end
78
+
79
+ def completed_migrations
80
+ result = @db.fetch("SELECT migration_name FROM #{TRACKING_TABLE} ORDER BY id")
81
+ result.map { |r| r[:migration_name] }
82
+ end
83
+
84
+ def pending_migrations
85
+ return [] unless Dir.exist?(@migrations_dir)
86
+
87
+ completed = completed_migrations
88
+ Dir.glob(File.join(@migrations_dir, "*.sql"))
89
+ .reject { |f| f.end_with?(".down.sql") }
90
+ .sort
91
+ .reject { |f| completed.include?(File.basename(f)) }
92
+ end
93
+
94
+ def run_migration(file)
95
+ name = File.basename(file)
96
+ Tina4::Debug.info("Running migration: #{name}")
97
+ begin
98
+ execute_sql_file(file)
99
+ record_migration(name)
100
+ { name: name, status: "success" }
101
+ rescue => e
102
+ Tina4::Debug.error("Migration failed: #{name} - #{e.message}")
103
+ { name: name, status: "failed", error: e.message }
104
+ end
105
+ end
106
+
107
+ def execute_sql_file(file)
108
+ sql = File.read(file)
109
+ statements = sql.split(";").map(&:strip).reject(&:empty?)
110
+ statements.each do |stmt|
111
+ next if stmt.start_with?("--")
112
+ @db.execute(stmt)
113
+ end
114
+ end
115
+
116
+ def record_migration(name)
117
+ @db.insert(TRACKING_TABLE, { migration_name: name })
118
+ end
119
+
120
+ def remove_migration_record(name)
121
+ @db.delete(TRACKING_TABLE, { migration_name: name })
122
+ end
123
+ end
124
+ end
data/lib/tina4/orm.rb ADDED
@@ -0,0 +1,168 @@
1
+ # frozen_string_literal: true
2
+ require "json"
3
+
4
+ module Tina4
5
+ class ORM
6
+ include Tina4::FieldTypes
7
+
8
+ class << self
9
+ def db
10
+ Tina4.database
11
+ end
12
+
13
+ def find(id)
14
+ pk = primary_key_field || :id
15
+ result = db.fetch_one("SELECT * FROM #{table_name} WHERE #{pk} = ?", [id])
16
+ return nil unless result
17
+ from_hash(result)
18
+ end
19
+
20
+ def where(conditions, params = [])
21
+ sql = "SELECT * FROM #{table_name} WHERE #{conditions}"
22
+ results = db.fetch(sql, params)
23
+ results.map { |row| from_hash(row) }
24
+ end
25
+
26
+ def all(limit: nil, skip: nil, order_by: nil)
27
+ sql = "SELECT * FROM #{table_name}"
28
+ sql += " ORDER BY #{order_by}" if order_by
29
+ results = db.fetch(sql, [], limit: limit, skip: skip)
30
+ results.map { |row| from_hash(row) }
31
+ end
32
+
33
+ def count(conditions = nil, params = [])
34
+ sql = "SELECT COUNT(*) as cnt FROM #{table_name}"
35
+ sql += " WHERE #{conditions}" if conditions
36
+ result = db.fetch_one(sql, params)
37
+ result[:cnt].to_i
38
+ end
39
+
40
+ def create(attributes = {})
41
+ instance = new(attributes)
42
+ instance.save
43
+ instance
44
+ end
45
+
46
+ def from_hash(hash)
47
+ instance = new
48
+ hash.each do |key, value|
49
+ setter = "#{key}="
50
+ instance.send(setter, value) if instance.respond_to?(setter)
51
+ end
52
+ instance.instance_variable_set(:@persisted, true)
53
+ instance
54
+ end
55
+ end
56
+
57
+ def initialize(attributes = {})
58
+ @persisted = false
59
+ @errors = []
60
+ attributes.each do |key, value|
61
+ setter = "#{key}="
62
+ send(setter, value) if respond_to?(setter)
63
+ end
64
+ # Set defaults
65
+ self.class.field_definitions.each do |name, opts|
66
+ if send(name).nil? && opts[:default]
67
+ send("#{name}=", opts[:default])
68
+ end
69
+ end
70
+ end
71
+
72
+ def save
73
+ @errors = []
74
+ validate_fields
75
+ return false unless @errors.empty?
76
+
77
+ data = to_hash(exclude_nil: true)
78
+ pk = self.class.primary_key_field || :id
79
+ pk_value = send(pk)
80
+
81
+ if @persisted && pk_value
82
+ filter = { pk => pk_value }
83
+ data.delete(pk)
84
+ self.class.db.update(self.class.table_name, data, filter)
85
+ else
86
+ result = self.class.db.insert(self.class.table_name, data)
87
+ if result[:last_id] && respond_to?("#{pk}=")
88
+ send("#{pk}=", result[:last_id])
89
+ end
90
+ @persisted = true
91
+ end
92
+ true
93
+ rescue => e
94
+ @errors << e.message
95
+ false
96
+ end
97
+
98
+ def delete
99
+ pk = self.class.primary_key_field || :id
100
+ pk_value = send(pk)
101
+ return false unless pk_value
102
+
103
+ self.class.db.delete(self.class.table_name, { pk => pk_value })
104
+ @persisted = false
105
+ true
106
+ end
107
+
108
+ def load(id = nil)
109
+ pk = self.class.primary_key_field || :id
110
+ id ||= send(pk)
111
+ return false unless id
112
+
113
+ result = self.class.db.fetch_one("SELECT * FROM #{self.class.table_name} WHERE #{pk} = ?", [id])
114
+ return false unless result
115
+
116
+ result.each do |key, value|
117
+ setter = "#{key}="
118
+ send(setter, value) if respond_to?(setter)
119
+ end
120
+ @persisted = true
121
+ true
122
+ end
123
+
124
+ def persisted?
125
+ @persisted
126
+ end
127
+
128
+ def errors
129
+ @errors
130
+ end
131
+
132
+ def to_hash(exclude_nil: false)
133
+ hash = {}
134
+ self.class.field_definitions.each_key do |name|
135
+ value = send(name)
136
+ next if exclude_nil && value.nil?
137
+ hash[name] = value
138
+ end
139
+ hash
140
+ end
141
+
142
+ def to_json(*_args)
143
+ JSON.generate(to_hash)
144
+ end
145
+
146
+ def to_s
147
+ "#<#{self.class.name} #{to_hash}>"
148
+ end
149
+
150
+ def select(*fields)
151
+ fields_str = fields.map(&:to_s).join(", ")
152
+ pk = self.class.primary_key_field || :id
153
+ pk_value = send(pk)
154
+ self.class.db.fetch_one("SELECT #{fields_str} FROM #{self.class.table_name} WHERE #{pk} = ?", [pk_value])
155
+ end
156
+
157
+ private
158
+
159
+ def validate_fields
160
+ self.class.field_definitions.each do |name, opts|
161
+ value = send(name)
162
+ if !opts[:nullable] && value.nil? && !opts[:auto_increment] && !opts[:default]
163
+ @errors << "#{name} cannot be null"
164
+ end
165
+ end
166
+ end
167
+ end
168
+ end