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,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tina4
4
+ module Drivers
5
+ class MssqlDriver
6
+ attr_reader :connection
7
+
8
+ def connect(connection_string)
9
+ require "tiny_tds"
10
+ uri = parse_connection(connection_string)
11
+ @connection = TinyTds::Client.new(
12
+ host: uri[:host],
13
+ port: uri[:port] || 1433,
14
+ username: uri[:username],
15
+ password: uri[:password],
16
+ database: uri[:database]
17
+ )
18
+ end
19
+
20
+ def close
21
+ @connection&.close
22
+ end
23
+
24
+ def execute_query(sql, params = [])
25
+ effective_sql = interpolate_params(sql, params)
26
+ result = @connection.execute(effective_sql)
27
+ rows = result.each(symbolize_keys: true).to_a
28
+ result.cancel if result.respond_to?(:cancel)
29
+ rows
30
+ end
31
+
32
+ def execute(sql, params = [])
33
+ effective_sql = interpolate_params(sql, params)
34
+ result = @connection.execute(effective_sql)
35
+ result.do
36
+ end
37
+
38
+ def last_insert_id
39
+ result = @connection.execute("SELECT SCOPE_IDENTITY() AS id")
40
+ row = result.first
41
+ result.cancel if result.respond_to?(:cancel)
42
+ row[:id]&.to_i
43
+ end
44
+
45
+ def placeholder
46
+ "?"
47
+ end
48
+
49
+ def placeholders(count)
50
+ (["?"] * count).join(", ")
51
+ end
52
+
53
+ def apply_limit(sql, limit, offset = 0)
54
+ "#{sql} OFFSET #{offset} ROWS FETCH NEXT #{limit} ROWS ONLY"
55
+ end
56
+
57
+ def begin_transaction
58
+ @connection.execute("BEGIN TRANSACTION").do
59
+ end
60
+
61
+ def commit
62
+ @connection.execute("COMMIT").do
63
+ end
64
+
65
+ def rollback
66
+ @connection.execute("ROLLBACK").do
67
+ end
68
+
69
+ def tables
70
+ rows = execute_query("SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE'")
71
+ rows.map { |r| r[:TABLE_NAME] || r[:table_name] }
72
+ end
73
+
74
+ def columns(table_name)
75
+ sql = "SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE, COLUMN_DEFAULT FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = ?"
76
+ rows = execute_query(sql, [table_name])
77
+ rows.map do |r|
78
+ {
79
+ name: r[:COLUMN_NAME] || r[:column_name],
80
+ type: r[:DATA_TYPE] || r[:data_type],
81
+ nullable: (r[:IS_NULLABLE] || r[:is_nullable]) == "YES",
82
+ default: r[:COLUMN_DEFAULT] || r[:column_default],
83
+ primary_key: false
84
+ }
85
+ end
86
+ end
87
+
88
+ private
89
+
90
+ def parse_connection(str)
91
+ # Format: mssql://user:pass@host:port/database
92
+ match = str.match(%r{(?:mssql|sqlserver)://(?:(\w+):([^@]+)@)?([^:/]+)(?::(\d+))?/(.+)})
93
+ if match
94
+ { username: match[1], password: match[2], host: match[3],
95
+ port: match[4]&.to_i, database: match[5] }
96
+ else
97
+ { host: "localhost", database: str }
98
+ end
99
+ end
100
+
101
+ def interpolate_params(sql, params)
102
+ return sql if params.empty?
103
+ result = sql.dup
104
+ params.each do |param|
105
+ escaped = param.is_a?(String) ? "'#{param.gsub("'", "''")}'" : param.to_s
106
+ result = result.sub("?", escaped)
107
+ end
108
+ result
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tina4
4
+ module Drivers
5
+ class MysqlDriver
6
+ attr_reader :connection
7
+
8
+ def connect(connection_string)
9
+ require "mysql2"
10
+ uri = URI.parse(connection_string.sub(/^mysql:\/\//, "mysql2://"))
11
+ @connection = Mysql2::Client.new(
12
+ host: uri.host || "localhost",
13
+ port: uri.port || 3306,
14
+ username: uri.user,
15
+ password: uri.password,
16
+ database: uri.path&.sub("/", "")
17
+ )
18
+ end
19
+
20
+ def close
21
+ @connection&.close
22
+ end
23
+
24
+ def execute_query(sql, params = [])
25
+ if params.empty?
26
+ results = @connection.query(sql, symbolize_keys: true)
27
+ else
28
+ stmt = @connection.prepare(sql)
29
+ results = stmt.execute(*params, symbolize_keys: true)
30
+ end
31
+ results.to_a
32
+ end
33
+
34
+ def execute(sql, params = [])
35
+ if params.empty?
36
+ @connection.query(sql)
37
+ else
38
+ stmt = @connection.prepare(sql)
39
+ stmt.execute(*params)
40
+ end
41
+ end
42
+
43
+ def last_insert_id
44
+ @connection.last_id
45
+ end
46
+
47
+ def placeholder
48
+ "?"
49
+ end
50
+
51
+ def placeholders(count)
52
+ (["?"] * count).join(", ")
53
+ end
54
+
55
+ def apply_limit(sql, limit, offset = 0)
56
+ "#{sql} LIMIT #{limit} OFFSET #{offset}"
57
+ end
58
+
59
+ def begin_transaction
60
+ @connection.query("START TRANSACTION")
61
+ end
62
+
63
+ def commit
64
+ @connection.query("COMMIT")
65
+ end
66
+
67
+ def rollback
68
+ @connection.query("ROLLBACK")
69
+ end
70
+
71
+ def tables
72
+ rows = execute_query("SHOW TABLES")
73
+ rows.map { |r| r.values.first }
74
+ end
75
+
76
+ def columns(table_name)
77
+ rows = execute_query("DESCRIBE #{table_name}")
78
+ rows.map do |r|
79
+ {
80
+ name: r[:Field],
81
+ type: r[:Type],
82
+ nullable: r[:Null] == "YES",
83
+ default: r[:Default],
84
+ primary_key: r[:Key] == "PRI"
85
+ }
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tina4
4
+ module Drivers
5
+ class PostgresDriver
6
+ attr_reader :connection
7
+
8
+ def connect(connection_string)
9
+ require "pg"
10
+ @connection = PG.connect(connection_string)
11
+ end
12
+
13
+ def close
14
+ @connection&.close
15
+ end
16
+
17
+ def execute_query(sql, params = [])
18
+ converted_sql = convert_placeholders(sql)
19
+ result = if params.empty?
20
+ @connection.exec(converted_sql)
21
+ else
22
+ @connection.exec_params(converted_sql, params)
23
+ end
24
+ result.map { |row| symbolize_keys(row) }
25
+ end
26
+
27
+ def execute(sql, params = [])
28
+ converted_sql = convert_placeholders(sql)
29
+ if params.empty?
30
+ @connection.exec(converted_sql)
31
+ else
32
+ @connection.exec_params(converted_sql, params)
33
+ end
34
+ end
35
+
36
+ def last_insert_id
37
+ result = @connection.exec("SELECT lastval()")
38
+ result.first["lastval"].to_i
39
+ rescue PG::Error
40
+ nil
41
+ end
42
+
43
+ def placeholder
44
+ "?"
45
+ end
46
+
47
+ def placeholders(count)
48
+ (1..count).map { |i| "$#{i}" }.join(", ")
49
+ end
50
+
51
+ def apply_limit(sql, limit, offset = 0)
52
+ "#{sql} LIMIT #{limit} OFFSET #{offset}"
53
+ end
54
+
55
+ def begin_transaction
56
+ @connection.exec("BEGIN")
57
+ end
58
+
59
+ def commit
60
+ @connection.exec("COMMIT")
61
+ end
62
+
63
+ def rollback
64
+ @connection.exec("ROLLBACK")
65
+ end
66
+
67
+ def tables
68
+ sql = "SELECT tablename FROM pg_tables WHERE schemaname = 'public'"
69
+ rows = execute_query(sql)
70
+ rows.map { |r| r[:tablename] }
71
+ end
72
+
73
+ def columns(table_name)
74
+ sql = "SELECT column_name, data_type, is_nullable, column_default FROM information_schema.columns WHERE table_name = $1"
75
+ rows = execute_query(sql, [table_name])
76
+ rows.map do |r|
77
+ {
78
+ name: r[:column_name],
79
+ type: r[:data_type],
80
+ nullable: r[:is_nullable] == "YES",
81
+ default: r[:column_default],
82
+ primary_key: false
83
+ }
84
+ end
85
+ end
86
+
87
+ private
88
+
89
+ def convert_placeholders(sql)
90
+ counter = 0
91
+ sql.gsub("?") { counter += 1; "$#{counter}" }
92
+ end
93
+
94
+ def symbolize_keys(hash)
95
+ hash.each_with_object({}) { |(k, v), h| h[k.to_sym] = v }
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tina4
4
+ module Drivers
5
+ class SqliteDriver
6
+ attr_reader :connection
7
+
8
+ def connect(connection_string)
9
+ require "sqlite3"
10
+ db_path = connection_string.sub(/^sqlite:\/\//, "").sub(/^sqlite:/, "")
11
+ @connection = SQLite3::Database.new(db_path)
12
+ @connection.results_as_hash = true
13
+ @connection.execute("PRAGMA journal_mode=WAL")
14
+ @connection.execute("PRAGMA foreign_keys=ON")
15
+ end
16
+
17
+ def close
18
+ @connection&.close
19
+ end
20
+
21
+ def execute_query(sql, params = [])
22
+ results = @connection.execute(sql, params)
23
+ results.map { |row| symbolize_keys(row) }
24
+ end
25
+
26
+ def execute(sql, params = [])
27
+ @connection.execute(sql, params)
28
+ end
29
+
30
+ def last_insert_id
31
+ @connection.last_insert_row_id
32
+ end
33
+
34
+ def placeholder
35
+ "?"
36
+ end
37
+
38
+ def placeholders(count)
39
+ (["?"] * count).join(", ")
40
+ end
41
+
42
+ def apply_limit(sql, limit, offset = 0)
43
+ "#{sql} LIMIT #{limit} OFFSET #{offset}"
44
+ end
45
+
46
+ def begin_transaction
47
+ @connection.execute("BEGIN TRANSACTION")
48
+ end
49
+
50
+ def commit
51
+ @connection.execute("COMMIT")
52
+ end
53
+
54
+ def rollback
55
+ @connection.execute("ROLLBACK")
56
+ end
57
+
58
+ def tables
59
+ rows = execute_query("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'")
60
+ rows.map { |r| r[:name] }
61
+ end
62
+
63
+ def columns(table_name)
64
+ rows = execute_query("PRAGMA table_info(#{table_name})")
65
+ rows.map do |r|
66
+ {
67
+ name: r[:name],
68
+ type: r[:type],
69
+ nullable: r[:notnull] == 0,
70
+ default: r[:dflt_value],
71
+ primary_key: r[:pk] == 1
72
+ }
73
+ end
74
+ end
75
+
76
+ private
77
+
78
+ def symbolize_keys(hash)
79
+ hash.each_with_object({}) do |(k, v), h|
80
+ h[k.to_s.to_sym] = v if k.is_a?(String) || k.is_a?(Symbol)
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
data/lib/tina4/env.rb ADDED
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+ require "digest"
3
+
4
+ module Tina4
5
+ module Env
6
+ DEFAULT_ENV = {
7
+ "PROJECT_NAME" => "Tina4 Ruby Project",
8
+ "VERSION" => "1.0.0",
9
+ "TINA4_LANGUAGE" => "en",
10
+ "TINA4_DEBUG_LEVEL" => "[TINA4_LOG_ALL]",
11
+ "SECRET" => "tina4-secret-change-me"
12
+ }.freeze
13
+
14
+ class << self
15
+ def load(root_dir = Dir.pwd)
16
+ env_file = resolve_env_file(root_dir)
17
+ unless File.exist?(env_file)
18
+ create_default_env(env_file)
19
+ end
20
+ parse_env_file(env_file)
21
+ end
22
+
23
+ private
24
+
25
+ def resolve_env_file(root_dir)
26
+ environment = ENV["ENVIRONMENT"]
27
+ if environment && !environment.empty?
28
+ candidate = File.join(root_dir, ".env.#{environment}")
29
+ return candidate if File.exist?(candidate)
30
+ end
31
+ File.join(root_dir, ".env")
32
+ end
33
+
34
+ def create_default_env(path)
35
+ api_key = Digest::MD5.hexdigest(Time.now.to_s)
36
+ content = DEFAULT_ENV.map { |k, v| "#{k}=\"#{v}\"" }.join("\n")
37
+ content += "\nAPI_KEY=\"#{api_key}\"\n"
38
+ File.write(path, content)
39
+ end
40
+
41
+ def parse_env_file(path)
42
+ return unless File.exist?(path)
43
+ File.readlines(path).each do |line|
44
+ line = line.strip
45
+ next if line.empty? || line.start_with?("#")
46
+ if (match = line.match(/\A([A-Za-z_][A-Za-z0-9_]*)=["']?(.*)["']?\z/))
47
+ key = match[1]
48
+ value = match[2].gsub(/["']\z/, "")
49
+ ENV[key] ||= value
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tina4
4
+ module FieldTypes
5
+ def self.included(base)
6
+ base.extend(ClassMethods)
7
+ end
8
+
9
+ module ClassMethods
10
+ def field_definitions
11
+ @field_definitions ||= {}
12
+ end
13
+
14
+ def primary_key_field
15
+ @primary_key_field
16
+ end
17
+
18
+ def table_name(name = nil)
19
+ if name
20
+ @table_name = name
21
+ else
22
+ @table_name || self.name.split("::").last.downcase + "s"
23
+ end
24
+ end
25
+
26
+ def integer_field(name, primary_key: false, auto_increment: false, nullable: true, default: nil)
27
+ register_field(name, :integer, primary_key: primary_key, auto_increment: auto_increment,
28
+ nullable: nullable, default: default)
29
+ end
30
+
31
+ def string_field(name, length: 255, primary_key: false, nullable: true, default: nil)
32
+ register_field(name, :string, length: length, primary_key: primary_key,
33
+ nullable: nullable, default: default)
34
+ end
35
+
36
+ def text_field(name, nullable: true, default: nil)
37
+ register_field(name, :text, nullable: nullable, default: default)
38
+ end
39
+
40
+ def float_field(name, nullable: true, default: nil)
41
+ register_field(name, :float, nullable: nullable, default: default)
42
+ end
43
+
44
+ def decimal_field(name, precision: 10, scale: 2, nullable: true, default: nil)
45
+ register_field(name, :decimal, precision: precision, scale: scale,
46
+ nullable: nullable, default: default)
47
+ end
48
+
49
+ def boolean_field(name, nullable: true, default: nil)
50
+ register_field(name, :boolean, nullable: nullable, default: default)
51
+ end
52
+
53
+ def date_field(name, nullable: true, default: nil)
54
+ register_field(name, :date, nullable: nullable, default: default)
55
+ end
56
+
57
+ def datetime_field(name, nullable: true, default: nil)
58
+ register_field(name, :datetime, nullable: nullable, default: default)
59
+ end
60
+
61
+ def timestamp_field(name, nullable: true, default: nil)
62
+ register_field(name, :timestamp, nullable: nullable, default: default)
63
+ end
64
+
65
+ def blob_field(name, nullable: true, default: nil)
66
+ register_field(name, :blob, nullable: nullable, default: default)
67
+ end
68
+
69
+ def json_field(name, nullable: true, default: nil)
70
+ register_field(name, :json, nullable: nullable, default: default)
71
+ end
72
+
73
+ private
74
+
75
+ def register_field(name, type, **options)
76
+ field_definitions[name] = { type: type }.merge(options)
77
+ @primary_key_field = name if options[:primary_key]
78
+
79
+ # Define getter/setter
80
+ attr_accessor name
81
+ end
82
+ end
83
+ end
84
+ end