tina4ruby 3.13.17 → 3.13.19

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 198d9905bf51e280f45f2bba90aac40fd64ea1bdf0c0f0b29e9a80d5e95020c9
4
- data.tar.gz: 74c01ef1ac870c47aebf57e686a9b3bd7efa46f5c56c1d78df4c378102ee39f2
3
+ metadata.gz: 93597b3b5afd4c56593d4c2d107b573790b68fbae4b0dd28f07d4f636a462381
4
+ data.tar.gz: c384790499ba27f16fc95efec1053a34c201d5444f742a9581167f6450418526
5
5
  SHA512:
6
- metadata.gz: 03a840227106a3e2c12a78bde6c5d62ff8e4eb3041bc443fe73cad5e5d6d1dbd366ffa5458d34faeffe8448ad8fb19ce9bd74c821a4d3ec436ab80aa984b58fc
7
- data.tar.gz: deefa98487ff855b8ad66beaa13b2e70f2b9f71677c0e317b52fc9153f43782a40723dc5519e814a2d65a29685fae18647f0bc84c124763aa8e5e7dcbddd07ca
6
+ metadata.gz: fd30dce3ddb8de6ec35fa4416f105b52df238951d723df7887ad24787a9e657bd04c90e904427dae4365faf602ef273cfb1716dcd655b15b0ac0caf45ecfa4d0
7
+ data.tar.gz: 1febfd89b40487edc5c51314d6bd5878ecd22244720959e58f54e56d90f2751d0543be833f4f0ddb2da51bcdee5ba8aa5d3c06d6723fe28df644ae4f6f2ef3db
@@ -84,7 +84,12 @@ module Tina4
84
84
  # Automatically:
85
85
  # - Registers an integer field for the column
86
86
  # - Calls belongs_to on this class (strip _id suffix for association name)
87
- # - Calls has_many on the referenced class (if already loaded)
87
+ # - Calls has_many on the referenced class. The accessor name is the
88
+ # declaring class name lowercased + "s" (e.g. Post → posts), matching
89
+ # Python; override with related_name:. Works whether the referenced
90
+ # class loaded before OR after this one, including the string form
91
+ # (references: "Author") for forward references — resolution is
92
+ # deferred via Tina4::ORM.inherited until the target class exists.
88
93
  #
89
94
  # @param name [Symbol] Column name (e.g. :user_id)
90
95
  # @param references [Class, String] Referenced model class or its name
@@ -122,12 +127,45 @@ module Tina4
122
127
  has_many_name: hm_key.to_sym,
123
128
  foreign_key: name.to_s
124
129
  }
130
+
131
+ # If the referenced model is ALREADY loaded (string form whose target
132
+ # is defined, or any forward reference that has since resolved), wire
133
+ # the deferred entry right now. The inherited hook on Tina4::ORM only
134
+ # fires when a NEW class is defined, so a string reference to a class
135
+ # that loaded BEFORE this one would otherwise never wire its has_many
136
+ # side. resolve_referenced_class returns the live class for either the
137
+ # Class or the string form.
138
+ resolved = resolve_referenced_class(references)
139
+ resolved.apply_fk_registry! if resolved && resolved.respond_to?(:apply_fk_registry!, true)
140
+ end
141
+
142
+ # Resolve a ForeignKeyField `references:` argument to a live Tina4::ORM
143
+ # subclass, or nil if it is not (yet) loaded. Accepts either a Class or a
144
+ # class-name String (with or without a namespace). The string form is the
145
+ # documented deferred-safe path: a bare constant used before its class is
146
+ # defined raises NameError (plain Ruby), so forward references must be
147
+ # written as strings.
148
+ def resolve_referenced_class(references)
149
+ return references if references.is_a?(Class)
150
+ return nil unless defined?(Tina4::ORM) && Tina4::ORM.respond_to?(:model_subclasses)
151
+
152
+ simple = references.to_s.split("::").last
153
+ Tina4::ORM.model_subclasses.find do |klass|
154
+ klass.name && klass.name.split("::").last == simple
155
+ end
156
+ rescue StandardError
157
+ nil
125
158
  end
126
159
 
127
160
  # Apply any deferred FK-registry has_many wiring for this class.
128
161
  # Called automatically when a class that is referenced by a ForeignKeyField is defined.
129
162
  def apply_fk_registry!
130
- class_simple_name = self.name.split("::").last
163
+ # Anonymous classes (Class.new(Tina4::ORM), common in specs) have a nil
164
+ # name and can never be a string-form ForeignKeyField target, so there
165
+ # is nothing to wire — bail out instead of raising on nil.split.
166
+ return if name.nil?
167
+
168
+ class_simple_name = name.split("::").last
131
169
  return unless defined?(@@_fk_registry) && @@_fk_registry.key?(class_simple_name)
132
170
 
133
171
  @@_fk_registry[class_simple_name].each do |entry|
data/lib/tina4/orm.rb CHANGED
@@ -16,16 +16,57 @@ module Tina4
16
16
  class ORM
17
17
  include Tina4::FieldTypes
18
18
 
19
+ # When a new model class is defined, resolve any deferred ForeignKeyField
20
+ # wiring that targets it. The string / forward-reference form of
21
+ # `foreign_key_field` (e.g. `references: "Author"`) records the has_many
22
+ # side in @@_fk_registry but cannot wire it until the referenced class
23
+ # actually loads — which is now. Without this hook apply_fk_registry! was
24
+ # never called, so the has_many side silently never wired. The class body
25
+ # (where the model's own foreign_key_field declarations run, populating the
26
+ # registry) executes AFTER inherited returns, so entries keyed on THIS
27
+ # class were already recorded by earlier-loaded models. Chain through super
28
+ # so we never clobber a future inherited hook.
29
+ def self.inherited(subclass)
30
+ super
31
+ (@_model_subclasses ||= []) << subclass
32
+ subclass.apply_fk_registry! if subclass.respond_to?(:apply_fk_registry!, true)
33
+ end
34
+
35
+ # Every Tina4::ORM subclass that has been loaded, in definition order.
36
+ # Mirrors Python's ORM.__subclasses__() — used to resolve string-form
37
+ # ForeignKeyField references to a live class.
38
+ def self.model_subclasses
39
+ @_model_subclasses ||= []
40
+ end
41
+
19
42
  class << self
20
43
  def db
21
- # v3.13.12: implicit binding from TINA4_DATABASE_URL.
22
- # Resolution: per-class @db → global Tina4.database → env-derived
23
- # auto-discovery. Pre-v3.13.12 this fell through to nil — the
24
- # helper auto_discover_db existed but was never called.
25
- @db || Tina4.database || auto_discover_db
44
+ # Resolution order:
45
+ # 1. @db is a Symbol/String named connection from Tina4.databases
46
+ # (bound via Tina4.bind_database(db, name:)). Raises a clear
47
+ # error if that named connection was never registered.
48
+ # 2. @db is a Database/driver instance → use it directly.
49
+ # 3. Otherwise → global Tina4.database, else env-derived
50
+ # auto-discovery (TINA4_DATABASE_URL). v3.13.12 wired this
51
+ # fallback; before that auto_discover_db was never called.
52
+ case @db
53
+ when Symbol, String
54
+ name = @db.to_sym
55
+ Tina4.databases[name] || raise(
56
+ "Tina4 named database connection '#{@db}' is not registered for #{name}. " \
57
+ "Call Tina4.bind_database(db, name: #{@db.inspect}) before using this model."
58
+ )
59
+ when nil
60
+ Tina4.database || auto_discover_db
61
+ else
62
+ @db
63
+ end
26
64
  end
27
65
 
28
- # Per-model database binding
66
+ # Per-model database binding.
67
+ # self.db = some_database_instance → use that connection
68
+ # self.db = :analytics → resolve a named connection
69
+ # from Tina4.databases at access time
29
70
  def db=(database)
30
71
  @db = database
31
72
  end
@@ -132,7 +173,7 @@ module Tina4
132
173
  #
133
174
  # @return [Tina4::QueryBuilder]
134
175
  def query
135
- QueryBuilder.from(table_name, db: db)
176
+ QueryBuilder.from_table(table_name, db: db)
136
177
  end
137
178
 
138
179
  def find(id_or_filter = nil, filter = nil, **kwargs)
@@ -453,8 +494,7 @@ module Tina4
453
494
  def auto_discover_db
454
495
  url = ENV["TINA4_DATABASE_URL"]
455
496
  return nil unless url
456
- Tina4.database = Tina4::Database.new(url, username: ENV.fetch("TINA4_DATABASE_USERNAME", ""), password: ENV.fetch("TINA4_DATABASE_PASSWORD", ""))
457
- Tina4.database
497
+ Tina4.bind_database(Tina4::Database.new(url, username: ENV.fetch("TINA4_DATABASE_USERNAME", ""), password: ENV.fetch("TINA4_DATABASE_PASSWORD", "")))
458
498
  end
459
499
 
460
500
  def find_by_filter(filter)
@@ -494,6 +534,15 @@ module Tina4
494
534
  @persisted = false
495
535
  @errors = []
496
536
  @relationship_cache = {}
537
+ # Accept a JSON object string (parity with Python/PHP/Node):
538
+ # Widget.new('{"id":1,"name":"alpha"}')
539
+ attributes = JSON.parse(attributes) if attributes.is_a?(String)
540
+ # A single model is one record — reject an Array with a clear message.
541
+ if attributes.is_a?(Array)
542
+ raise ArgumentError,
543
+ "#{self.class}.new expects a Hash, keyword args, or a JSON object string " \
544
+ "for one record — got an Array. Map over the list to build many records."
545
+ end
497
546
  attributes.each do |key, value|
498
547
  setter = "#{key}="
499
548
  __send__(setter, value) if respond_to?(setter)
@@ -80,6 +80,7 @@ module Tina4
80
80
  # Matches Python __call__ / PHP __invoke / Node response() pattern.
81
81
  def call(data = nil, status_code = 200, content_type = nil)
82
82
  @status_code = status_code
83
+ data = jsonable(data)
83
84
  if content_type
84
85
  @headers["content-type"] = content_type
85
86
  @body = data.to_s
@@ -96,10 +97,29 @@ module Tina4
96
97
  def json(data, status_or_opts = nil, status: nil)
97
98
  @status_code = status || (status_or_opts.is_a?(Integer) ? status_or_opts : 200)
98
99
  @headers["content-type"] = JSON_CONTENT_TYPE
100
+ data = jsonable(data)
99
101
  @body = data.is_a?(String) ? data : JSON.generate(data)
100
102
  self
101
103
  end
102
104
 
105
+ # Normalise domain objects into JSON-serialisable structures so handlers can
106
+ # `response.(model)` / `response.json(model)` without calling .to_h by hand:
107
+ #
108
+ # response.json(user) # ORM model -> Hash
109
+ # response.json(User.all) # Array<ORM> -> Array<Hash>
110
+ # response.json(db.fetch(sql)) # DatabaseResult -> Array<Hash>
111
+ #
112
+ # Plain Hash / Array / String pass through unchanged (Array members that are
113
+ # models are still converted).
114
+ def jsonable(data)
115
+ return data.to_h if data.is_a?(Tina4::ORM)
116
+ return data.records if data.is_a?(Tina4::DatabaseResult)
117
+ return data.map { |item| item.is_a?(Tina4::ORM) ? item.to_h : item } if data.is_a?(Array)
118
+
119
+ data
120
+ end
121
+ private :jsonable
122
+
103
123
  def html(content, status_or_opts = nil, status: nil)
104
124
  @status_code = status || (status_or_opts.is_a?(Integer) ? status_or_opts : 200)
105
125
  @headers["content-type"] = HTML_CONTENT_TYPE
data/lib/tina4/seeder.rb CHANGED
@@ -373,7 +373,7 @@ module Tina4
373
373
 
374
374
  db = Tina4.database
375
375
  unless db
376
- Tina4::Log.error("Seeder: No database connection. Set Tina4.database first.")
376
+ Tina4::Log.error("Seeder: No database connection. Call Tina4.bind_database(db) first.")
377
377
  return 0
378
378
  end
379
379
 
data/lib/tina4/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Tina4
4
- VERSION = "3.13.17"
4
+ VERSION = "3.13.19"
5
5
  end
data/lib/tina4.rb CHANGED
@@ -120,7 +120,26 @@ module Tina4
120
120
  BANNER
121
121
 
122
122
  class << self
123
- attr_accessor :root_dir, :database
123
+ attr_accessor :root_dir
124
+ attr_reader :database
125
+
126
+ # Bind a database connection.
127
+ # bind_database(db) → sets the global default (Tina4.database)
128
+ # bind_database(db, name: :analytics) → registers a named connection
129
+ # A model with `self.db = :analytics` resolves from this named registry;
130
+ # otherwise models fall back to the global default / TINA4_DATABASE_URL.
131
+ def bind_database(db, name: nil)
132
+ if name.nil?
133
+ @database = db
134
+ else
135
+ (@databases ||= {})[name.to_sym] = db
136
+ end
137
+ db
138
+ end
139
+
140
+ # Named connection registry. bind_database(db, name:) populates it;
141
+ # models with a Symbol/String `self.db` resolve against it.
142
+ def databases = (@databases ||= {})
124
143
 
125
144
  def print_banner(host: "0.0.0.0", port: 7147, server_name: nil)
126
145
  # TINA4_SUPPRESS — short-circuit ALL banner output for headless / CI runs.
@@ -450,7 +469,7 @@ module Tina4
450
469
  db_url = ENV["TINA4_DATABASE_URL"]
451
470
  if db_url && !db_url.empty?
452
471
  begin
453
- @database = Tina4::Database.new(db_url)
472
+ bind_database(Tina4::Database.new(db_url))
454
473
  Tina4::Log.info("Database connected: #{db_url.sub(/:[^:@]+@/, ':***@')}")
455
474
  rescue => e
456
475
  Tina4::Log.error("Database connection failed: #{e.message}")
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tina4ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.13.17
4
+ version: 3.13.19
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tina4 Team