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 +4 -4
- data/lib/tina4/api.rb +21 -1
- data/lib/tina4/database.rb +36 -0
- data/lib/tina4/graphql.rb +94 -0
- data/lib/tina4/service.rb +60 -0
- data/lib/tina4/service_runner.rb +35 -0
- data/lib/tina4/version.rb +1 -1
- data/lib/tina4.rb +1 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: eb13055e35412e98279f173f9cda3b6dc3e5a2c6766db1c2439b74e9de3354e2
|
|
4
|
+
data.tar.gz: 907a9ba2b3a732927dfd965cb41a212fcb4e245688fa6fab2ae805728e71489f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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: {})
|
data/lib/tina4/database.rb
CHANGED
|
@@ -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
|
data/lib/tina4/service_runner.rb
CHANGED
|
@@ -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
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.
|
|
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-
|
|
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
|