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 +4 -4
- data/lib/tina4/field_types.rb +40 -2
- data/lib/tina4/orm.rb +58 -9
- data/lib/tina4/response.rb +20 -0
- data/lib/tina4/seeder.rb +1 -1
- data/lib/tina4/version.rb +1 -1
- data/lib/tina4.rb +21 -2
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 93597b3b5afd4c56593d4c2d107b573790b68fbae4b0dd28f07d4f636a462381
|
|
4
|
+
data.tar.gz: c384790499ba27f16fc95efec1053a34c201d5444f742a9581167f6450418526
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fd30dce3ddb8de6ec35fa4416f105b52df238951d723df7887ad24787a9e657bd04c90e904427dae4365faf602ef273cfb1716dcd655b15b0ac0caf45ecfa4d0
|
|
7
|
+
data.tar.gz: 1febfd89b40487edc5c51314d6bd5878ecd22244720959e58f54e56d90f2751d0543be833f4f0ddb2da51bcdee5ba8aa5d3c06d6723fe28df644ae4f6f2ef3db
|
data/lib/tina4/field_types.rb
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
@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.
|
|
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.
|
|
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)
|
data/lib/tina4/response.rb
CHANGED
|
@@ -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.
|
|
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
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
|
|
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
|
-
|
|
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}")
|