tina4ruby 3.13.0 → 3.13.1

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: bf3bb8bf3275f76f25951fdb75de731c304323b64cb2a495e4060ad7fcafdde1
4
- data.tar.gz: a616d714a2a9beecfeb8d42a5b4124540d2abe07be8d9d2cd83ec5996596eb44
3
+ metadata.gz: eb13055e35412e98279f173f9cda3b6dc3e5a2c6766db1c2439b74e9de3354e2
4
+ data.tar.gz: 907a9ba2b3a732927dfd965cb41a212fcb4e245688fa6fab2ae805728e71489f
5
5
  SHA512:
6
- metadata.gz: be8986d4743a886d442d267add5953bff00a24fe85388b1947e57b1c9fa68447db08d63811b1f13d25cacb64eeea80b8e06fe366ec53961290c3125dbea69a1a
7
- data.tar.gz: 535158f269ed6964d764e010efb319ac343991d9737a41bad8acbefe6eb8e8ff191694fb662a2df4cec684499c4b708ca924b6d80a7b1cfea306323eb697706b
6
+ metadata.gz: b261fd27af0fed8f591599c2acf376c0333286910d302773e228fe92cae1e24201260a240081f26bb9abba86f731e9c7790d8c1f26b86dff857d1a87d0b938b0
7
+ data.tar.gz: 821cb969b8b9822b39d9b6b8d0e2b0ca4a74c7a35febcb277b6e8b119df38dfb09af5acaf88fbcda1b3793a44f4477c636bc5b7223a14214029dbbd8eb9f4cce
data/lib/tina4/api.rb CHANGED
@@ -8,13 +8,33 @@ module Tina4
8
8
  class API
9
9
  attr_reader :base_url, :headers
10
10
 
11
- def initialize(base_url, headers: {}, timeout: 30)
11
+ # 3.13.1: added ergonomic kwargs (bearer_token, username, password,
12
+ # verify_ssl) so callers no longer need three follow-up setter calls.
13
+ # Cross-framework parity with the Python tina4_python.api.Api kwargs.
14
+ #
15
+ # api = Tina4::API.new("https://api.example.com", bearer_token: "sk-abc")
16
+ # api = Tina4::API.new("https://api.example.com", username: "u", password: "p")
17
+ # api = Tina4::API.new("https://api.example.com", headers: {"X-Tenant" => "acme"})
18
+ # api = Tina4::API.new("https://self-signed.local", verify_ssl: false)
19
+ #
20
+ # Bearer wins over basic-auth when both are passed.
21
+ def initialize(base_url, headers: {}, timeout: 30,
22
+ bearer_token: nil, username: nil, password: nil,
23
+ verify_ssl: nil)
12
24
  @base_url = base_url.chomp("/")
13
25
  @headers = {
14
26
  "Content-Type" => "application/json",
15
27
  "Accept" => "application/json"
16
28
  }.merge(headers)
17
29
  @timeout = timeout
30
+ @verify_ssl = verify_ssl
31
+
32
+ # Bearer wins over basic-auth when both passed
33
+ if bearer_token
34
+ set_bearer_token(bearer_token)
35
+ elsif username && password
36
+ set_basic_auth(username, password)
37
+ end
18
38
  end
19
39
 
20
40
  def get(path, params: {})
@@ -101,6 +101,29 @@ module Tina4
101
101
  pool: pool)
102
102
  end
103
103
 
104
+ # Open a database connection — convention name matching SQLAlchemy
105
+ # engine.connect() and the cross-framework Database.get_connection()
106
+ # surface shipped in 3.13.x.
107
+ #
108
+ # The first argument may be either a URL (containing `://` or `sqlite:`)
109
+ # or an env-var name. Falls back to in-memory SQLite when no URL
110
+ # resolves — matches Python tina4_python's default behaviour.
111
+ #
112
+ # db = Tina4::Database.get_connection # from TINA4_DATABASE_URL
113
+ # db = Tina4::Database.get_connection("sqlite::memory:") # explicit URL
114
+ # db = Tina4::Database.get_connection("postgres://...", username: "u", password: "p")
115
+ def self.get_connection(url_or_env_key = "TINA4_DATABASE_URL", username: nil, password: nil, pool: nil)
116
+ if url_or_env_key.include?("://") || url_or_env_key.start_with?("sqlite:")
117
+ return new(url_or_env_key, username: username, password: password, pool: pool)
118
+ end
119
+
120
+ db = from_env(env_key: url_or_env_key, pool: pool)
121
+ return db if db
122
+
123
+ # Fallback: in-memory SQLite — matches Python parity.
124
+ new("sqlite::memory:", username: username, password: password, pool: pool)
125
+ end
126
+
104
127
  def initialize(connection_string = nil, username: nil, password: nil, driver_name: nil, pool: nil)
105
128
  @connection_string = connection_string || ENV["TINA4_DATABASE_URL"]
106
129
  @username = username || ENV["TINA4_DATABASE_USERNAME"]
@@ -213,6 +236,19 @@ module Tina4
213
236
  end
214
237
  end
215
238
 
239
+ # Fetch rows and return the records array directly.
240
+ #
241
+ # Symmetric with fetch_one. Cross-framework parity with Python
242
+ # db.fetch_all() / PHP $db->fetchAll() / Node db.fetchAll().
243
+ #
244
+ # rows = db.fetch_all("SELECT * FROM users WHERE active = ?", [1])
245
+ # rows.each { |row| puts row["name"] }
246
+ #
247
+ # Returns [] (not nil) when no rows match.
248
+ def fetch_all(sql, params = [], limit: 100, offset: nil)
249
+ fetch(sql, params, limit: limit, offset: offset).records
250
+ end
251
+
216
252
  def fetch(sql, params = [], limit: 100, offset: nil)
217
253
  offset ||= 0
218
254
  drv = current_driver
data/lib/tina4/graphql.rb CHANGED
@@ -853,9 +853,103 @@ module Tina4
853
853
  !%w[false 0 no off].include?(val)
854
854
  end
855
855
 
856
+ # ── Class-level resolver registry — 3.13.1 ────────────────────────
857
+ #
858
+ # Resolvers registered via Tina4::GraphQL.resolve("Type", "field")
859
+ # accumulate here BEFORE any GraphQL instance exists. When `new` runs,
860
+ # the instance drains the registry into its schema. This is what makes
861
+ # the documented decorator-style pattern work at app-startup time:
862
+ # modules `Tina4::GraphQL.resolve(...)` at load time and register
863
+ # before the singleton is even constructed. Cross-framework parity
864
+ # with Python @GraphQL.resolve and PHP GraphQL::resolve.
865
+ @class_resolvers = {}
866
+ @default_instance = nil
867
+
868
+ class << self
869
+ attr_accessor :default_instance
870
+
871
+ def class_resolvers
872
+ @class_resolvers ||= {}
873
+ end
874
+
875
+ # Decorator-style resolver registration.
876
+ #
877
+ # Tina4::GraphQL.resolve("Query", "products") do |root, args, ctx|
878
+ # Product.all
879
+ # end
880
+ #
881
+ # Tina4::GraphQL.resolve("Mutation", "createProduct") do |root, args, ctx|
882
+ # Product.create(args["input"])
883
+ # end
884
+ #
885
+ # Tina4::GraphQL.resolve("Product", "reviews") do |product, args, ctx|
886
+ # Review.where("product_id = ?", [product["id"]])
887
+ # end
888
+ #
889
+ # Resolvers registered before any GraphQL instance accumulate in a
890
+ # class-level registry. new GraphQL drains them into its schema.
891
+ # Resolvers registered after .default_instance is set wire into
892
+ # the live schema immediately.
893
+ def resolve(type_name, field_name, &block)
894
+ class_resolvers[type_name] ||= {}
895
+ class_resolvers[type_name][field_name] = block
896
+
897
+ # If a default instance is active, attach immediately so post-startup
898
+ # registrations take effect without re-instantiation.
899
+ if @default_instance
900
+ @default_instance.send(:attach_resolver, type_name, field_name, block)
901
+ end
902
+ block
903
+ end
904
+
905
+ # Test-only — clear the class-level registry. Used by parity tests
906
+ # to avoid bleed-over between cases.
907
+ def _clear_class_resolvers!
908
+ @class_resolvers = {}
909
+ @default_instance = nil
910
+ end
911
+ end
912
+
856
913
  def initialize(schema = nil)
857
914
  @schema = schema || GraphQLSchema.new
858
915
  @executor = GraphQLExecutor.new(@schema)
916
+ @field_resolvers = {}
917
+
918
+ # Drain any resolvers registered via the class-level GraphQL.resolve()
919
+ # BEFORE this instance was constructed.
920
+ self.class.class_resolvers.each do |type_name, fields|
921
+ fields.each do |field_name, resolver|
922
+ attach_resolver(type_name, field_name, resolver)
923
+ end
924
+ end
925
+ end
926
+
927
+ # Wire a single resolver into the live schema.
928
+ def attach_resolver(type_name, field_name, resolver)
929
+ if type_name == "Query"
930
+ existing = @schema.queries[field_name] || {}
931
+ existing[:resolve] = resolver
932
+ existing[:args] ||= {}
933
+ existing[:type] ||= "String"
934
+ @schema.queries[field_name] = existing
935
+ elsif type_name == "Mutation"
936
+ existing = @schema.mutations[field_name] || {}
937
+ existing[:resolve] = resolver
938
+ existing[:args] ||= {}
939
+ existing[:type] ||= "String"
940
+ @schema.mutations[field_name] = existing
941
+ else
942
+ # Object-type field resolver — stash for the executor.
943
+ @field_resolvers[type_name] ||= {}
944
+ @field_resolvers[type_name][field_name] = resolver
945
+ end
946
+ end
947
+ private :attach_resolver
948
+
949
+ # Get the field resolver registered for an object type, if any.
950
+ # Used by the executor during nested field resolution.
951
+ def field_resolver(type_name, field_name)
952
+ @field_resolvers.dig(type_name, field_name)
859
953
  end
860
954
 
861
955
  # Execute a query string directly
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Tina4 — The Intelligent Native Application 4ramework
4
+ # Copyright 2007 - current Tina4
5
+ # License: MIT https://opensource.org/licenses/MIT
6
+
7
+ module Tina4
8
+ # Base class for class-based background services managed by
9
+ # {Tina4::ServiceRunner}. Cross-framework parity with PHP `Tina4\Service`
10
+ # and the same shape the documentation has long taught.
11
+ #
12
+ # class EmailQueueWorker < Tina4::Service
13
+ # def run
14
+ # until should_stop?
15
+ # process_next_job
16
+ # sleep 1
17
+ # end
18
+ # end
19
+ # end
20
+ #
21
+ # Tina4::ServiceRunner.register_service("emails", EmailQueueWorker.new)
22
+ # Tina4::ServiceRunner.start
23
+ #
24
+ # Subclasses MUST override #run. Optionally override #stop for custom
25
+ # shutdown behaviour but always call `super` so the internal flag
26
+ # gets set — the default #should_stop? reads from it.
27
+ class Service
28
+ def initialize
29
+ @running = true
30
+ end
31
+
32
+ # Main work loop — subclasses MUST override.
33
+ def run
34
+ raise NotImplementedError, "#{self.class}#run must be implemented by the subclass"
35
+ end
36
+
37
+ # Signal this service to stop. The next `should_stop?` check returns true.
38
+ def stop
39
+ @running = false
40
+ end
41
+
42
+ # Returns true once #stop has been called. Use inside #run loops as
43
+ # the exit condition:
44
+ #
45
+ # def run
46
+ # until should_stop?
47
+ # # do work
48
+ # end
49
+ # end
50
+ def should_stop?
51
+ !@running
52
+ end
53
+
54
+ # Return a callable that ServiceRunner can register. Used by
55
+ # ServiceRunner.register_service under the hood.
56
+ def to_proc
57
+ method(:run).to_proc
58
+ end
59
+ end
60
+ end
@@ -48,6 +48,41 @@ module Tina4
48
48
  Tina4::Log.debug("Service registered: #{name}")
49
49
  end
50
50
 
51
+ # Register a class-based service (subclass of {Tina4::Service}) by name.
52
+ #
53
+ # Wraps the Service's #run method as the handler callable that
54
+ # ServiceRunner.start invokes. The service's #stop is also wired
55
+ # up so ServiceRunner.stop(name) shuts it down cleanly.
56
+ #
57
+ # class EmailQueueWorker < Tina4::Service
58
+ # def run
59
+ # until should_stop?
60
+ # # process work
61
+ # end
62
+ # end
63
+ # end
64
+ #
65
+ # Tina4::ServiceRunner.register_service("emails", EmailQueueWorker.new)
66
+ # Tina4::ServiceRunner.start
67
+ #
68
+ # Default options set daemon: true because Service subclasses manage
69
+ # their own loop inside #run. Override via `options`.
70
+ def register_service(name, service, options = {})
71
+ raise ArgumentError, "service must be a Tina4::Service instance" unless service.is_a?(Tina4::Service)
72
+
73
+ options = { daemon: true }.merge(options)
74
+ callable = service.method(:run)
75
+
76
+ @mutex.synchronize do
77
+ @registry[name.to_s] = {
78
+ handler: callable,
79
+ options: options,
80
+ instance: service,
81
+ }
82
+ end
83
+ Tina4::Log.debug("Service registered (class-based): #{name}")
84
+ end
85
+
51
86
  # Auto-discover service files from a directory.
52
87
  # Each file should call Tina4.service or Tina4::ServiceRunner.register.
53
88
  def discover(service_dir = nil)
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.0"
4
+ VERSION = "3.13.1"
5
5
  end
data/lib/tina4.rb CHANGED
@@ -39,6 +39,7 @@ require_relative "tina4/background"
39
39
  require_relative "tina4/localization"
40
40
  require_relative "tina4/container"
41
41
  require_relative "tina4/queue"
42
+ require_relative "tina4/service"
42
43
  require_relative "tina4/service_runner"
43
44
  require_relative "tina4/events"
44
45
  require_relative "tina4/plan"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tina4ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.13.0
4
+ version: 3.13.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tina4 Team
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-06-01 00:00:00.000000000 Z
11
+ date: 2026-06-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack
@@ -382,6 +382,7 @@ files:
382
382
  - lib/tina4/scss/tina4css/tina4.scss
383
383
  - lib/tina4/scss_compiler.rb
384
384
  - lib/tina4/seeder.rb
385
+ - lib/tina4/service.rb
385
386
  - lib/tina4/service_runner.rb
386
387
  - lib/tina4/session.rb
387
388
  - lib/tina4/session_handlers/database_handler.rb