tina4ruby 3.11.13 → 3.11.15

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 (132) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +80 -80
  3. data/LICENSE.txt +21 -21
  4. data/README.md +137 -137
  5. data/exe/tina4ruby +5 -5
  6. data/lib/tina4/ai.rb +696 -696
  7. data/lib/tina4/api.rb +189 -189
  8. data/lib/tina4/auth.rb +305 -305
  9. data/lib/tina4/auto_crud.rb +244 -244
  10. data/lib/tina4/cache.rb +154 -154
  11. data/lib/tina4/cli.rb +1449 -1449
  12. data/lib/tina4/constants.rb +46 -46
  13. data/lib/tina4/container.rb +74 -74
  14. data/lib/tina4/cors.rb +74 -74
  15. data/lib/tina4/crud.rb +692 -692
  16. data/lib/tina4/database/sqlite3_adapter.rb +165 -165
  17. data/lib/tina4/database.rb +625 -625
  18. data/lib/tina4/database_result.rb +208 -208
  19. data/lib/tina4/debug.rb +8 -8
  20. data/lib/tina4/dev.rb +14 -14
  21. data/lib/tina4/dev_admin.rb +935 -935
  22. data/lib/tina4/dev_mailbox.rb +191 -191
  23. data/lib/tina4/drivers/firebird_driver.rb +124 -110
  24. data/lib/tina4/drivers/mongodb_driver.rb +561 -561
  25. data/lib/tina4/drivers/mssql_driver.rb +112 -112
  26. data/lib/tina4/drivers/mysql_driver.rb +90 -90
  27. data/lib/tina4/drivers/odbc_driver.rb +191 -191
  28. data/lib/tina4/drivers/postgres_driver.rb +116 -106
  29. data/lib/tina4/drivers/sqlite_driver.rb +122 -122
  30. data/lib/tina4/env.rb +95 -95
  31. data/lib/tina4/error_overlay.rb +252 -252
  32. data/lib/tina4/events.rb +109 -109
  33. data/lib/tina4/field_types.rb +154 -154
  34. data/lib/tina4/frond.rb +2025 -2025
  35. data/lib/tina4/gallery/auth/meta.json +1 -1
  36. data/lib/tina4/gallery/auth/src/routes/api/gallery_auth.rb +114 -114
  37. data/lib/tina4/gallery/database/meta.json +1 -1
  38. data/lib/tina4/gallery/database/src/routes/api/gallery_db.rb +43 -43
  39. data/lib/tina4/gallery/error-overlay/meta.json +1 -1
  40. data/lib/tina4/gallery/error-overlay/src/routes/api/gallery_crash.rb +17 -17
  41. data/lib/tina4/gallery/orm/meta.json +1 -1
  42. data/lib/tina4/gallery/orm/src/routes/api/gallery_products.rb +16 -16
  43. data/lib/tina4/gallery/queue/meta.json +1 -1
  44. data/lib/tina4/gallery/queue/src/routes/api/gallery_queue.rb +325 -325
  45. data/lib/tina4/gallery/rest-api/meta.json +1 -1
  46. data/lib/tina4/gallery/rest-api/src/routes/api/gallery_hello.rb +14 -14
  47. data/lib/tina4/gallery/templates/meta.json +1 -1
  48. data/lib/tina4/gallery/templates/src/routes/gallery_page.rb +12 -12
  49. data/lib/tina4/gallery/templates/src/templates/gallery_page.twig +257 -257
  50. data/lib/tina4/graphql.rb +966 -966
  51. data/lib/tina4/health.rb +39 -39
  52. data/lib/tina4/html_element.rb +170 -170
  53. data/lib/tina4/job.rb +80 -80
  54. data/lib/tina4/localization.rb +168 -168
  55. data/lib/tina4/log.rb +203 -203
  56. data/lib/tina4/mcp.rb +696 -696
  57. data/lib/tina4/messenger.rb +587 -587
  58. data/lib/tina4/metrics.rb +793 -793
  59. data/lib/tina4/middleware.rb +445 -445
  60. data/lib/tina4/migration.rb +451 -451
  61. data/lib/tina4/orm.rb +790 -790
  62. data/lib/tina4/public/css/tina4.css +2463 -2463
  63. data/lib/tina4/public/css/tina4.min.css +1 -1
  64. data/lib/tina4/public/images/logo.svg +5 -5
  65. data/lib/tina4/public/js/frond.min.js +2 -2
  66. data/lib/tina4/public/js/tina4-dev-admin.js +565 -565
  67. data/lib/tina4/public/js/tina4-dev-admin.min.js +480 -480
  68. data/lib/tina4/public/js/tina4.min.js +92 -92
  69. data/lib/tina4/public/js/tina4js.min.js +48 -48
  70. data/lib/tina4/public/swagger/index.html +90 -90
  71. data/lib/tina4/public/swagger/oauth2-redirect.html +63 -63
  72. data/lib/tina4/query_builder.rb +380 -380
  73. data/lib/tina4/queue.rb +366 -366
  74. data/lib/tina4/queue_backends/kafka_backend.rb +80 -80
  75. data/lib/tina4/queue_backends/lite_backend.rb +298 -298
  76. data/lib/tina4/queue_backends/mongo_backend.rb +126 -126
  77. data/lib/tina4/queue_backends/rabbitmq_backend.rb +73 -73
  78. data/lib/tina4/rack_app.rb +817 -817
  79. data/lib/tina4/rate_limiter.rb +130 -130
  80. data/lib/tina4/request.rb +268 -255
  81. data/lib/tina4/response.rb +346 -346
  82. data/lib/tina4/response_cache.rb +551 -551
  83. data/lib/tina4/router.rb +406 -406
  84. data/lib/tina4/scss/tina4css/_alerts.scss +34 -34
  85. data/lib/tina4/scss/tina4css/_badges.scss +22 -22
  86. data/lib/tina4/scss/tina4css/_buttons.scss +69 -69
  87. data/lib/tina4/scss/tina4css/_cards.scss +49 -49
  88. data/lib/tina4/scss/tina4css/_forms.scss +156 -156
  89. data/lib/tina4/scss/tina4css/_grid.scss +81 -81
  90. data/lib/tina4/scss/tina4css/_modals.scss +84 -84
  91. data/lib/tina4/scss/tina4css/_nav.scss +149 -149
  92. data/lib/tina4/scss/tina4css/_reset.scss +94 -94
  93. data/lib/tina4/scss/tina4css/_tables.scss +54 -54
  94. data/lib/tina4/scss/tina4css/_typography.scss +55 -55
  95. data/lib/tina4/scss/tina4css/_utilities.scss +197 -197
  96. data/lib/tina4/scss/tina4css/_variables.scss +117 -117
  97. data/lib/tina4/scss/tina4css/base.scss +1 -1
  98. data/lib/tina4/scss/tina4css/colors.scss +48 -48
  99. data/lib/tina4/scss/tina4css/tina4.scss +17 -17
  100. data/lib/tina4/scss_compiler.rb +178 -178
  101. data/lib/tina4/seeder.rb +567 -567
  102. data/lib/tina4/service_runner.rb +303 -303
  103. data/lib/tina4/session.rb +297 -297
  104. data/lib/tina4/session_handlers/database_handler.rb +72 -72
  105. data/lib/tina4/session_handlers/file_handler.rb +67 -67
  106. data/lib/tina4/session_handlers/mongo_handler.rb +49 -49
  107. data/lib/tina4/session_handlers/redis_handler.rb +43 -43
  108. data/lib/tina4/session_handlers/valkey_handler.rb +43 -43
  109. data/lib/tina4/shutdown.rb +84 -84
  110. data/lib/tina4/sql_translation.rb +158 -158
  111. data/lib/tina4/swagger.rb +124 -124
  112. data/lib/tina4/template.rb +894 -894
  113. data/lib/tina4/templates/base.twig +26 -26
  114. data/lib/tina4/templates/errors/302.twig +14 -14
  115. data/lib/tina4/templates/errors/401.twig +9 -9
  116. data/lib/tina4/templates/errors/403.twig +29 -29
  117. data/lib/tina4/templates/errors/404.twig +29 -29
  118. data/lib/tina4/templates/errors/500.twig +38 -38
  119. data/lib/tina4/templates/errors/502.twig +9 -9
  120. data/lib/tina4/templates/errors/503.twig +12 -12
  121. data/lib/tina4/templates/errors/base.twig +37 -37
  122. data/lib/tina4/test_client.rb +159 -159
  123. data/lib/tina4/testing.rb +340 -340
  124. data/lib/tina4/validator.rb +174 -174
  125. data/lib/tina4/version.rb +1 -1
  126. data/lib/tina4/webserver.rb +312 -312
  127. data/lib/tina4/websocket.rb +343 -343
  128. data/lib/tina4/websocket_backplane.rb +190 -190
  129. data/lib/tina4/wsdl.rb +564 -564
  130. data/lib/tina4.rb +458 -458
  131. data/lib/tina4ruby.rb +4 -4
  132. metadata +3 -3
data/lib/tina4/events.rb CHANGED
@@ -1,109 +1,109 @@
1
- # frozen_string_literal: true
2
-
3
- # Tina4 Events — Simple observer pattern for decoupled communication.
4
- #
5
- # Zero-dependency event system. Fire events, register listeners.
6
- #
7
- # Tina4::Events.on("user.created") { |user| puts "Welcome #{user[:name]}!" }
8
- # Tina4::Events.on("user.created") { |user| puts "New signup: #{user[:email]}" }
9
- # Tina4::Events.emit("user.created", { name: "Alice", email: "alice@example.com" })
10
- #
11
- # One-time listeners:
12
- #
13
- # Tina4::Events.once("app.ready") { puts "App started!" }
14
- #
15
- module Tina4
16
- class Events
17
- @listeners = Hash.new { |h, k| h[k] = [] }
18
-
19
- class << self
20
- # Register a listener for an event.
21
- #
22
- # Tina4::Events.on("user.created") { |user| ... }
23
- # Tina4::Events.on("user.created", priority: 10) { |user| ... }
24
- #
25
- # Higher priority runs first.
26
- def on(event, priority: 0, &block)
27
- raise ArgumentError, "block required" unless block_given?
28
-
29
- @listeners[event] << { priority: priority, callback: block, once: false }
30
- @listeners[event].sort_by! { |entry| -entry[:priority] }
31
- block
32
- end
33
-
34
- # Register a listener that fires only once then auto-removes.
35
- #
36
- # Tina4::Events.once("app.ready") { puts "App started!" }
37
- #
38
- def once(event, priority: 0, &block)
39
- raise ArgumentError, "block required" unless block_given?
40
-
41
- @listeners[event] << { priority: priority, callback: block, once: true }
42
- @listeners[event].sort_by! { |entry| -entry[:priority] }
43
- block
44
- end
45
-
46
- # Remove a specific listener, or all listeners for an event.
47
- #
48
- # Tina4::Events.off("user.created", handler) # remove specific
49
- # Tina4::Events.off("user.created") # remove all for event
50
- #
51
- def off(event, callback = nil)
52
- if callback.nil?
53
- @listeners.delete(event)
54
- else
55
- @listeners[event].reject! { |entry| entry[:callback] == callback }
56
- end
57
- end
58
-
59
- # Fire an event synchronously. Returns array of listener results.
60
- #
61
- # results = Tina4::Events.emit("user.created", user_data)
62
- #
63
- def emit(event, *args)
64
- entries = @listeners[event].dup
65
- results = []
66
- entries.each do |entry|
67
- # Remove one-time listeners before calling so re-entrant emits are safe
68
- @listeners[event].delete(entry) if entry[:once]
69
- results << entry[:callback].call(*args)
70
- end
71
- results
72
- end
73
-
74
- # Get all listener callbacks for an event (in priority order).
75
- def listeners(event)
76
- @listeners[event].map { |entry| entry[:callback] }
77
- end
78
-
79
- # List all registered event names.
80
- def events
81
- @listeners.keys
82
- end
83
-
84
- # Fire an event asynchronously. Each listener runs in its own thread.
85
- # Errors in listeners are silently caught.
86
- #
87
- # Tina4::Events.emit_async("user.created", user_data)
88
- #
89
- def emit_async(event, *args)
90
- return unless @listeners&.key?(event)
91
-
92
- @listeners[event].sort_by { |l| -(l[:priority] || 0) }.each do |listener|
93
- Thread.new do
94
- begin
95
- listener[:callback].call(*args)
96
- rescue => e
97
- # Async emit silently catches errors
98
- end
99
- end
100
- end
101
- end
102
-
103
- # Remove all listeners for all events.
104
- def clear
105
- @listeners.clear
106
- end
107
- end
108
- end
109
- end
1
+ # frozen_string_literal: true
2
+
3
+ # Tina4 Events — Simple observer pattern for decoupled communication.
4
+ #
5
+ # Zero-dependency event system. Fire events, register listeners.
6
+ #
7
+ # Tina4::Events.on("user.created") { |user| puts "Welcome #{user[:name]}!" }
8
+ # Tina4::Events.on("user.created") { |user| puts "New signup: #{user[:email]}" }
9
+ # Tina4::Events.emit("user.created", { name: "Alice", email: "alice@example.com" })
10
+ #
11
+ # One-time listeners:
12
+ #
13
+ # Tina4::Events.once("app.ready") { puts "App started!" }
14
+ #
15
+ module Tina4
16
+ class Events
17
+ @listeners = Hash.new { |h, k| h[k] = [] }
18
+
19
+ class << self
20
+ # Register a listener for an event.
21
+ #
22
+ # Tina4::Events.on("user.created") { |user| ... }
23
+ # Tina4::Events.on("user.created", priority: 10) { |user| ... }
24
+ #
25
+ # Higher priority runs first.
26
+ def on(event, priority: 0, &block)
27
+ raise ArgumentError, "block required" unless block_given?
28
+
29
+ @listeners[event] << { priority: priority, callback: block, once: false }
30
+ @listeners[event].sort_by! { |entry| -entry[:priority] }
31
+ block
32
+ end
33
+
34
+ # Register a listener that fires only once then auto-removes.
35
+ #
36
+ # Tina4::Events.once("app.ready") { puts "App started!" }
37
+ #
38
+ def once(event, priority: 0, &block)
39
+ raise ArgumentError, "block required" unless block_given?
40
+
41
+ @listeners[event] << { priority: priority, callback: block, once: true }
42
+ @listeners[event].sort_by! { |entry| -entry[:priority] }
43
+ block
44
+ end
45
+
46
+ # Remove a specific listener, or all listeners for an event.
47
+ #
48
+ # Tina4::Events.off("user.created", handler) # remove specific
49
+ # Tina4::Events.off("user.created") # remove all for event
50
+ #
51
+ def off(event, callback = nil)
52
+ if callback.nil?
53
+ @listeners.delete(event)
54
+ else
55
+ @listeners[event].reject! { |entry| entry[:callback] == callback }
56
+ end
57
+ end
58
+
59
+ # Fire an event synchronously. Returns array of listener results.
60
+ #
61
+ # results = Tina4::Events.emit("user.created", user_data)
62
+ #
63
+ def emit(event, *args)
64
+ entries = @listeners[event].dup
65
+ results = []
66
+ entries.each do |entry|
67
+ # Remove one-time listeners before calling so re-entrant emits are safe
68
+ @listeners[event].delete(entry) if entry[:once]
69
+ results << entry[:callback].call(*args)
70
+ end
71
+ results
72
+ end
73
+
74
+ # Get all listener callbacks for an event (in priority order).
75
+ def listeners(event)
76
+ @listeners[event].map { |entry| entry[:callback] }
77
+ end
78
+
79
+ # List all registered event names.
80
+ def events
81
+ @listeners.keys
82
+ end
83
+
84
+ # Fire an event asynchronously. Each listener runs in its own thread.
85
+ # Errors in listeners are silently caught.
86
+ #
87
+ # Tina4::Events.emit_async("user.created", user_data)
88
+ #
89
+ def emit_async(event, *args)
90
+ return unless @listeners&.key?(event)
91
+
92
+ @listeners[event].sort_by { |l| -(l[:priority] || 0) }.each do |listener|
93
+ Thread.new do
94
+ begin
95
+ listener[:callback].call(*args)
96
+ rescue => e
97
+ # Async emit silently catches errors
98
+ end
99
+ end
100
+ end
101
+ end
102
+
103
+ # Remove all listeners for all events.
104
+ def clear
105
+ @listeners.clear
106
+ end
107
+ end
108
+ end
109
+ end
@@ -1,154 +1,154 @@
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
- base = self.name.split("::").last.downcase
23
- # Pluralize by default (add "s") unless ORM_PLURAL_TABLE_NAMES is explicitly disabled
24
- unless ENV.fetch("ORM_PLURAL_TABLE_NAMES", "").match?(/\A(false|0|no)\z/i)
25
- base += "s" unless base.end_with?("s")
26
- end
27
- @table_name || base
28
- end
29
- end
30
-
31
- def integer_field(name, primary_key: false, auto_increment: false, nullable: true, default: nil)
32
- register_field(name, :integer, primary_key: primary_key, auto_increment: auto_increment,
33
- nullable: nullable, default: default)
34
- end
35
-
36
- def string_field(name, length: 255, primary_key: false, nullable: true, default: nil)
37
- register_field(name, :string, length: length, primary_key: primary_key,
38
- nullable: nullable, default: default)
39
- end
40
-
41
- def text_field(name, nullable: true, default: nil)
42
- register_field(name, :text, nullable: nullable, default: default)
43
- end
44
-
45
- def float_field(name, nullable: true, default: nil)
46
- register_field(name, :float, nullable: nullable, default: default)
47
- end
48
-
49
- def decimal_field(name, precision: 10, scale: 2, nullable: true, default: nil)
50
- register_field(name, :decimal, precision: precision, scale: scale,
51
- nullable: nullable, default: default)
52
- end
53
-
54
- def numeric_field(name, nullable: true, default: nil)
55
- register_field(name, :float, nullable: nullable, default: default)
56
- end
57
-
58
- def boolean_field(name, nullable: true, default: nil)
59
- register_field(name, :boolean, nullable: nullable, default: default)
60
- end
61
-
62
- def date_field(name, nullable: true, default: nil)
63
- register_field(name, :date, nullable: nullable, default: default)
64
- end
65
-
66
- def datetime_field(name, nullable: true, default: nil)
67
- register_field(name, :datetime, nullable: nullable, default: default)
68
- end
69
-
70
- def timestamp_field(name, nullable: true, default: nil)
71
- register_field(name, :timestamp, nullable: nullable, default: default)
72
- end
73
-
74
- def blob_field(name, nullable: true, default: nil)
75
- register_field(name, :blob, nullable: nullable, default: default)
76
- end
77
-
78
- def json_field(name, nullable: true, default: nil)
79
- register_field(name, :json, nullable: nullable, default: default)
80
- end
81
-
82
- # Declare a foreign key integer column and auto-wire relationships.
83
- #
84
- # Automatically:
85
- # - Registers an integer field for the column
86
- # - Calls belongs_to on this class (strip _id suffix for association name)
87
- # - Calls has_many on the referenced class (if already loaded)
88
- #
89
- # @param name [Symbol] Column name (e.g. :user_id)
90
- # @param references [Class, String] Referenced model class or its name
91
- # @param related_name [Symbol, String, nil] Override the has-many name on the referenced model
92
- #
93
- # Example:
94
- # class Post < Tina4::ORM
95
- # integer_field :id, primary_key: true
96
- # foreign_key_field :user_id, references: User
97
- # end
98
- # # post.user → belongs_to auto-wired
99
- # # user.posts → has_many auto-wired
100
- def foreign_key_field(name, references:, related_name: nil, **options)
101
- register_field(name, :integer, **options)
102
-
103
- # Derive association name: strip _id suffix
104
- belongs_name = name.to_s.end_with?("_id") ? name.to_s[0..-4].to_sym : name.to_sym
105
-
106
- # Wire belongs_to on this class
107
- belongs_to(belongs_name, class_name: references.to_s.split("::").last, foreign_key: name.to_s) if respond_to?(:belongs_to, true)
108
-
109
- # Wire has_many on referenced class (if already a loaded Class)
110
- if references.is_a?(Class) && references.respond_to?(:has_many, true)
111
- hm_name = (related_name || "#{self.name.split("::").last.downcase}s").to_sym
112
- references.has_many(hm_name, class_name: self.name.split("::").last, foreign_key: name.to_s)
113
- end
114
-
115
- # Register for deferred wiring (resolves when referenced class is later loaded)
116
- @@_fk_registry ||= {}
117
- ref_name = references.is_a?(Class) ? references.name.split("::").last : references.to_s.split("::").last
118
- @@_fk_registry[ref_name] ||= []
119
- hm_key = (related_name || "#{self.name.split("::").last.downcase}s").to_s
120
- @@_fk_registry[ref_name] << {
121
- declaring_class: self,
122
- has_many_name: hm_key.to_sym,
123
- foreign_key: name.to_s
124
- }
125
- end
126
-
127
- # Apply any deferred FK-registry has_many wiring for this class.
128
- # Called automatically when a class that is referenced by a ForeignKeyField is defined.
129
- def apply_fk_registry!
130
- class_simple_name = self.name.split("::").last
131
- return unless defined?(@@_fk_registry) && @@_fk_registry.key?(class_simple_name)
132
-
133
- @@_fk_registry[class_simple_name].each do |entry|
134
- next if entry[:applied]
135
-
136
- has_many(entry[:has_many_name],
137
- class_name: entry[:declaring_class].name.split("::").last,
138
- foreign_key: entry[:foreign_key]) if respond_to?(:has_many, true)
139
- entry[:applied] = true
140
- end
141
- end
142
-
143
- private
144
-
145
- def register_field(name, type, **options)
146
- field_definitions[name] = { type: type }.merge(options)
147
- @primary_key_field = name if options[:primary_key]
148
-
149
- # Define getter/setter
150
- attr_accessor name
151
- end
152
- end
153
- end
154
- end
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
+ base = self.name.split("::").last.downcase
23
+ # Pluralize by default (add "s") unless ORM_PLURAL_TABLE_NAMES is explicitly disabled
24
+ unless ENV.fetch("ORM_PLURAL_TABLE_NAMES", "").match?(/\A(false|0|no)\z/i)
25
+ base += "s" unless base.end_with?("s")
26
+ end
27
+ @table_name || base
28
+ end
29
+ end
30
+
31
+ def integer_field(name, primary_key: false, auto_increment: false, nullable: true, default: nil)
32
+ register_field(name, :integer, primary_key: primary_key, auto_increment: auto_increment,
33
+ nullable: nullable, default: default)
34
+ end
35
+
36
+ def string_field(name, length: 255, primary_key: false, nullable: true, default: nil)
37
+ register_field(name, :string, length: length, primary_key: primary_key,
38
+ nullable: nullable, default: default)
39
+ end
40
+
41
+ def text_field(name, nullable: true, default: nil)
42
+ register_field(name, :text, nullable: nullable, default: default)
43
+ end
44
+
45
+ def float_field(name, nullable: true, default: nil)
46
+ register_field(name, :float, nullable: nullable, default: default)
47
+ end
48
+
49
+ def decimal_field(name, precision: 10, scale: 2, nullable: true, default: nil)
50
+ register_field(name, :decimal, precision: precision, scale: scale,
51
+ nullable: nullable, default: default)
52
+ end
53
+
54
+ def numeric_field(name, nullable: true, default: nil)
55
+ register_field(name, :float, nullable: nullable, default: default)
56
+ end
57
+
58
+ def boolean_field(name, nullable: true, default: nil)
59
+ register_field(name, :boolean, nullable: nullable, default: default)
60
+ end
61
+
62
+ def date_field(name, nullable: true, default: nil)
63
+ register_field(name, :date, nullable: nullable, default: default)
64
+ end
65
+
66
+ def datetime_field(name, nullable: true, default: nil)
67
+ register_field(name, :datetime, nullable: nullable, default: default)
68
+ end
69
+
70
+ def timestamp_field(name, nullable: true, default: nil)
71
+ register_field(name, :timestamp, nullable: nullable, default: default)
72
+ end
73
+
74
+ def blob_field(name, nullable: true, default: nil)
75
+ register_field(name, :blob, nullable: nullable, default: default)
76
+ end
77
+
78
+ def json_field(name, nullable: true, default: nil)
79
+ register_field(name, :json, nullable: nullable, default: default)
80
+ end
81
+
82
+ # Declare a foreign key integer column and auto-wire relationships.
83
+ #
84
+ # Automatically:
85
+ # - Registers an integer field for the column
86
+ # - Calls belongs_to on this class (strip _id suffix for association name)
87
+ # - Calls has_many on the referenced class (if already loaded)
88
+ #
89
+ # @param name [Symbol] Column name (e.g. :user_id)
90
+ # @param references [Class, String] Referenced model class or its name
91
+ # @param related_name [Symbol, String, nil] Override the has-many name on the referenced model
92
+ #
93
+ # Example:
94
+ # class Post < Tina4::ORM
95
+ # integer_field :id, primary_key: true
96
+ # foreign_key_field :user_id, references: User
97
+ # end
98
+ # # post.user → belongs_to auto-wired
99
+ # # user.posts → has_many auto-wired
100
+ def foreign_key_field(name, references:, related_name: nil, **options)
101
+ register_field(name, :integer, **options)
102
+
103
+ # Derive association name: strip _id suffix
104
+ belongs_name = name.to_s.end_with?("_id") ? name.to_s[0..-4].to_sym : name.to_sym
105
+
106
+ # Wire belongs_to on this class
107
+ belongs_to(belongs_name, class_name: references.to_s.split("::").last, foreign_key: name.to_s) if respond_to?(:belongs_to, true)
108
+
109
+ # Wire has_many on referenced class (if already a loaded Class)
110
+ if references.is_a?(Class) && references.respond_to?(:has_many, true)
111
+ hm_name = (related_name || "#{self.name.split("::").last.downcase}s").to_sym
112
+ references.has_many(hm_name, class_name: self.name.split("::").last, foreign_key: name.to_s)
113
+ end
114
+
115
+ # Register for deferred wiring (resolves when referenced class is later loaded)
116
+ @@_fk_registry ||= {}
117
+ ref_name = references.is_a?(Class) ? references.name.split("::").last : references.to_s.split("::").last
118
+ @@_fk_registry[ref_name] ||= []
119
+ hm_key = (related_name || "#{self.name.split("::").last.downcase}s").to_s
120
+ @@_fk_registry[ref_name] << {
121
+ declaring_class: self,
122
+ has_many_name: hm_key.to_sym,
123
+ foreign_key: name.to_s
124
+ }
125
+ end
126
+
127
+ # Apply any deferred FK-registry has_many wiring for this class.
128
+ # Called automatically when a class that is referenced by a ForeignKeyField is defined.
129
+ def apply_fk_registry!
130
+ class_simple_name = self.name.split("::").last
131
+ return unless defined?(@@_fk_registry) && @@_fk_registry.key?(class_simple_name)
132
+
133
+ @@_fk_registry[class_simple_name].each do |entry|
134
+ next if entry[:applied]
135
+
136
+ has_many(entry[:has_many_name],
137
+ class_name: entry[:declaring_class].name.split("::").last,
138
+ foreign_key: entry[:foreign_key]) if respond_to?(:has_many, true)
139
+ entry[:applied] = true
140
+ end
141
+ end
142
+
143
+ private
144
+
145
+ def register_field(name, type, **options)
146
+ field_definitions[name] = { type: type }.merge(options)
147
+ @primary_key_field = name if options[:primary_key]
148
+
149
+ # Define getter/setter
150
+ attr_accessor name
151
+ end
152
+ end
153
+ end
154
+ end