tina4ruby 3.10.90 → 3.10.91

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: fddba220f8bcb8da6f84302fdba1814ec708faf394cb0735e5dbe657cc30f201
4
- data.tar.gz: 5d76ea85c26170f94d958ab6f415e14f59404ef9e4296f28a17bafa48d0334dd
3
+ metadata.gz: 5a737d8b571f74eceea00422b681722ba711019f59c1fae14a8cc618bce5aa51
4
+ data.tar.gz: 6834b22997597eeb49064e17f56a6fd00280f9dc57c699a413402ff4fcbcc1c4
5
5
  SHA512:
6
- metadata.gz: da823b77b2de40ffd6a50143a5563f17c4edf56476f78eb97f1fba086ddb985b980cdbab9d76f58abda7e40746f6fbe1351e196376d98dffde654b38ccd3b3e5
7
- data.tar.gz: 1a699a7954a9c75d5fdecef19654c2533f249491c711d209ef081fd951f1cd240407837834b3ddea45fdabbfee5b9f1436c7896dd413096626a1b7c0a643b72f
6
+ metadata.gz: d85adcf4bfc4ca50956912e2fe926fc9cbe2833c0cb2a134d376ad63d13066d6c533e5418ff9aad40065a5c2a6313145d2a3da574f0a494fb86f906ca85545f5
7
+ data.tar.gz: 93a399a93f6c98ce34e0c521c33221cf5ac6d69238c0103099e7ecaa0f9c3c9c1d6ae2db48f0a2e12585e7d7700545a045c1e2122152657bdb753f518c4efcb5
data/lib/tina4/api.rb CHANGED
@@ -2,6 +2,7 @@
2
2
  require "net/http"
3
3
  require "uri"
4
4
  require "json"
5
+ require "base64"
5
6
 
6
7
  module Tina4
7
8
  class API
@@ -16,41 +17,51 @@ module Tina4
16
17
  @timeout = timeout
17
18
  end
18
19
 
19
- def get(path, params: {}, headers: {})
20
+ def get(path, params: {})
20
21
  uri = build_uri(path, params)
21
22
  request = Net::HTTP::Get.new(uri)
22
- apply_headers(request, headers)
23
+ apply_headers(request, {})
23
24
  execute(uri, request)
24
25
  end
25
26
 
26
- def post(path, body: nil, headers: {})
27
+ def post(path, body: nil, content_type: "application/json")
27
28
  uri = build_uri(path)
28
29
  request = Net::HTTP::Post.new(uri)
29
- request.body = body.is_a?(String) ? body : JSON.generate(body) if body
30
- apply_headers(request, headers)
30
+ if body
31
+ request.body = body.is_a?(String) ? body : JSON.generate(body)
32
+ request["Content-Type"] = content_type
33
+ end
34
+ apply_headers(request, {})
31
35
  execute(uri, request)
32
36
  end
33
37
 
34
- def put(path, body: nil, headers: {})
38
+ def put(path, body: nil, content_type: "application/json")
35
39
  uri = build_uri(path)
36
40
  request = Net::HTTP::Put.new(uri)
37
- request.body = body.is_a?(String) ? body : JSON.generate(body) if body
38
- apply_headers(request, headers)
41
+ if body
42
+ request.body = body.is_a?(String) ? body : JSON.generate(body)
43
+ request["Content-Type"] = content_type
44
+ end
45
+ apply_headers(request, {})
39
46
  execute(uri, request)
40
47
  end
41
48
 
42
- def patch(path, body: nil, headers: {})
49
+ def patch(path, body: nil, content_type: "application/json")
43
50
  uri = build_uri(path)
44
51
  request = Net::HTTP::Patch.new(uri)
45
- request.body = body.is_a?(String) ? body : JSON.generate(body) if body
46
- apply_headers(request, headers)
52
+ if body
53
+ request.body = body.is_a?(String) ? body : JSON.generate(body)
54
+ request["Content-Type"] = content_type
55
+ end
56
+ apply_headers(request, {})
47
57
  execute(uri, request)
48
58
  end
49
59
 
50
- def delete(path, headers: {})
60
+ def delete(path, body: nil)
51
61
  uri = build_uri(path)
52
62
  request = Net::HTTP::Delete.new(uri)
53
- apply_headers(request, headers)
63
+ request.body = body.is_a?(String) ? body : JSON.generate(body) if body
64
+ apply_headers(request, {})
54
65
  execute(uri, request)
55
66
  end
56
67
 
@@ -67,6 +78,32 @@ module Tina4
67
78
  execute(uri, request)
68
79
  end
69
80
 
81
+ def set_basic_auth(username, password)
82
+ @headers["Authorization"] = "Basic #{Base64.strict_encode64("#{username}:#{password}")}"
83
+ self
84
+ end
85
+
86
+ def set_bearer_token(token)
87
+ @headers["Authorization"] = "Bearer #{token}"
88
+ self
89
+ end
90
+
91
+ def add_headers(headers)
92
+ @headers.merge!(headers)
93
+ self
94
+ end
95
+
96
+ def send_request(method = "GET", path = "", body: nil, content_type: "application/json")
97
+ case method.upcase
98
+ when "GET" then get(path)
99
+ when "POST" then post(path, body: body, content_type: content_type)
100
+ when "PUT" then put(path, body: body, content_type: content_type)
101
+ when "PATCH" then patch(path, body: body, content_type: content_type)
102
+ when "DELETE" then delete(path, body: body)
103
+ else get(path)
104
+ end
105
+ end
106
+
70
107
  private
71
108
 
72
109
  def build_uri(path, params = {})
data/lib/tina4/auth.rb CHANGED
@@ -89,7 +89,7 @@ module Tina4
89
89
 
90
90
  # ── Token API (auto-selects HS256 or RS256) ─────────────────
91
91
 
92
- def get_token(payload, expires_in: 60)
92
+ def get_token(payload, expires_in: 60, secret: nil)
93
93
  now = Time.now.to_i
94
94
  claims = payload.merge(
95
95
  "iat" => now,
@@ -97,7 +97,9 @@ module Tina4
97
97
  "nbf" => now
98
98
  )
99
99
 
100
- if use_hmac?
100
+ if secret
101
+ hmac_encode(claims, secret)
102
+ elsif use_hmac?
101
103
  hmac_encode(claims, hmac_secret)
102
104
  else
103
105
  ensure_keys
@@ -182,7 +184,7 @@ module Tina4
182
184
  get_token(payload, expires_in: expires_in)
183
185
  end
184
186
 
185
- def authenticate_request(headers)
187
+ def authenticate_request(headers, secret: nil, algorithm: "HS256")
186
188
  auth_header = headers["HTTP_AUTHORIZATION"] || headers["Authorization"] || ""
187
189
  return nil unless auth_header =~ /\ABearer\s+(.+)\z/i
188
190
 
@@ -194,6 +196,12 @@ module Tina4
194
196
  return { "api_key" => true }
195
197
  end
196
198
 
199
+ # If a custom secret is provided, validate against it directly
200
+ if secret
201
+ payload = hmac_decode(token, secret)
202
+ return payload ? payload : nil
203
+ end
204
+
197
205
  valid_token(token) ? get_payload(token) : nil
198
206
  end
199
207
 
@@ -53,7 +53,7 @@ module Tina4
53
53
  example_body = build_example(model_class)
54
54
 
55
55
  # GET /api/{table} -- list all with pagination, filtering, sorting
56
- Tina4::Router.add_route("GET", "#{prefix}/#{table}", proc { |req, res|
56
+ Tina4::Router.add("GET", "#{prefix}/#{table}", proc { |req, res|
57
57
  begin
58
58
  per_page = (req.query["per_page"] || req.query["limit"] || 10).to_i
59
59
  page = (req.query["page"] || 1).to_i
@@ -94,7 +94,7 @@ module Tina4
94
94
  }, swagger_meta: { summary: "List all #{pretty_name}", tags: [table.to_s] })
95
95
 
96
96
  # GET /api/{table}/{id} -- get single record
97
- Tina4::Router.add_route("GET", "#{prefix}/#{table}/{id}", proc { |req, res|
97
+ Tina4::Router.add("GET", "#{prefix}/#{table}/{id}", proc { |req, res|
98
98
  begin
99
99
  id = req.params["id"]
100
100
  record = model_class.find_by_id(id.to_i)
@@ -109,7 +109,7 @@ module Tina4
109
109
  }, swagger_meta: { summary: "Get #{pretty_name} by ID", tags: [table.to_s] })
110
110
 
111
111
  # POST /api/{table} -- create record
112
- Tina4::Router.add_route("POST", "#{prefix}/#{table}", proc { |req, res|
112
+ Tina4::Router.add("POST", "#{prefix}/#{table}", proc { |req, res|
113
113
  begin
114
114
  attributes = req.body_parsed
115
115
  record = model_class.create(attributes)
@@ -137,7 +137,7 @@ module Tina4
137
137
  })
138
138
 
139
139
  # PUT /api/{table}/{id} -- update record
140
- Tina4::Router.add_route("PUT", "#{prefix}/#{table}/{id}", proc { |req, res|
140
+ Tina4::Router.add("PUT", "#{prefix}/#{table}/{id}", proc { |req, res|
141
141
  begin
142
142
  id = req.params["id"]
143
143
  record = model_class.find_by_id(id.to_i)
@@ -175,7 +175,7 @@ module Tina4
175
175
  })
176
176
 
177
177
  # DELETE /api/{table}/{id} -- delete record
178
- Tina4::Router.add_route("DELETE", "#{prefix}/#{table}/{id}", proc { |req, res|
178
+ Tina4::Router.add("DELETE", "#{prefix}/#{table}/{id}", proc { |req, res|
179
179
  begin
180
180
  id = req.params["id"]
181
181
  record = model_class.find_by_id(id.to_i)
@@ -0,0 +1,154 @@
1
+ module Tina4
2
+ # In-memory TTL cache with tag-based invalidation.
3
+ #
4
+ # Matches the Python / PHP / Node.js QueryCache API for cross-framework
5
+ # parity. Thread-safe via an internal Mutex.
6
+ #
7
+ # Usage:
8
+ # cache = Tina4::QueryCache.new(default_ttl: 60, max_size: 1000)
9
+ # cache.set("key", "value", ttl: 30, tags: ["users"])
10
+ # cache.get("key") # => "value"
11
+ # cache.clear_tag("users")
12
+ #
13
+ class QueryCache
14
+ CacheEntry = Struct.new(:value, :expires_at, :tags)
15
+
16
+ # @param default_ttl [Integer] default TTL in seconds (default: 300)
17
+ # @param max_size [Integer] maximum number of cache entries (default: 1000)
18
+ def initialize(default_ttl: 300, max_size: 1000)
19
+ @default_ttl = default_ttl
20
+ @max_size = max_size
21
+ @store = {}
22
+ @mutex = Mutex.new
23
+ end
24
+
25
+ # Store a value with optional TTL and tags.
26
+ #
27
+ # @param key [String]
28
+ # @param value [Object]
29
+ # @param ttl [Integer, nil] TTL in seconds (nil uses default)
30
+ # @param tags [Array<String>] optional tags for grouped invalidation
31
+ def set(key, value, ttl: nil, tags: [])
32
+ ttl ||= @default_ttl
33
+ expires_at = Time.now.to_f + ttl
34
+
35
+ @mutex.synchronize do
36
+ # Evict oldest if at capacity
37
+ if @store.size >= @max_size && !@store.key?(key)
38
+ oldest_key = @store.keys.first
39
+ @store.delete(oldest_key)
40
+ end
41
+ @store[key] = CacheEntry.new(value, expires_at, tags)
42
+ end
43
+ end
44
+
45
+ # Retrieve a cached value. Returns nil if expired or missing.
46
+ #
47
+ # @param key [String]
48
+ # @param default [Object] value to return if key is missing
49
+ # @return [Object, nil]
50
+ def get(key, default = nil)
51
+ @mutex.synchronize do
52
+ entry = @store[key]
53
+ return default unless entry
54
+
55
+ if Time.now.to_f > entry.expires_at
56
+ @store.delete(key)
57
+ return default
58
+ end
59
+
60
+ entry.value
61
+ end
62
+ end
63
+
64
+ # Check if a key exists and is not expired.
65
+ #
66
+ # @param key [String]
67
+ # @return [Boolean]
68
+ def has?(key)
69
+ @mutex.synchronize do
70
+ entry = @store[key]
71
+ return false unless entry
72
+
73
+ if Time.now.to_f > entry.expires_at
74
+ @store.delete(key)
75
+ return false
76
+ end
77
+
78
+ true
79
+ end
80
+ end
81
+
82
+ # Delete a key from the cache.
83
+ #
84
+ # @param key [String]
85
+ # @return [Boolean] true if the key was present
86
+ def delete(key)
87
+ @mutex.synchronize do
88
+ !@store.delete(key).nil?
89
+ end
90
+ end
91
+
92
+ # Clear all entries from the cache.
93
+ def clear
94
+ @mutex.synchronize { @store.clear }
95
+ end
96
+
97
+ # Clear all entries with a given tag.
98
+ #
99
+ # @param tag [String]
100
+ # @return [Integer] number of entries removed
101
+ def clear_tag(tag)
102
+ @mutex.synchronize do
103
+ keys_to_remove = @store.select { |_k, v| v.tags.include?(tag) }.keys
104
+ keys_to_remove.each { |k| @store.delete(k) }
105
+ keys_to_remove.size
106
+ end
107
+ end
108
+
109
+ # Remove all expired entries.
110
+ #
111
+ # @return [Integer] number of entries removed
112
+ def sweep
113
+ @mutex.synchronize do
114
+ now = Time.now.to_f
115
+ keys_to_remove = @store.select { |_k, v| now > v.expires_at }.keys
116
+ keys_to_remove.each { |k| @store.delete(k) }
117
+ keys_to_remove.size
118
+ end
119
+ end
120
+
121
+ # Fetch from cache, or compute and store.
122
+ #
123
+ # @param key [String]
124
+ # @param ttl [Integer] TTL in seconds
125
+ # @param block [Proc] factory to compute the value if not cached
126
+ # @return [Object]
127
+ def remember(key, ttl, &block)
128
+ cached = get(key)
129
+ return cached unless cached.nil?
130
+
131
+ value = block.call
132
+ set(key, value, ttl: ttl)
133
+ value
134
+ end
135
+
136
+ # Current number of entries in the cache.
137
+ #
138
+ # @return [Integer]
139
+ def size
140
+ @mutex.synchronize { @store.size }
141
+ end
142
+
143
+ # Generate a stable cache key from a SQL query and params.
144
+ # Mirrors SQLTranslator.query_key for direct use on QueryCache.
145
+ #
146
+ # @param sql [String]
147
+ # @param params [Array, nil]
148
+ # @return [String]
149
+ def self.query_key(sql, params = nil)
150
+ raw = params ? "#{sql}|#{params.inspect}" : sql
151
+ "query:#{Digest::SHA256.hexdigest(raw)}"
152
+ end
153
+ end
154
+ end
data/lib/tina4/cli.rb CHANGED
@@ -368,7 +368,7 @@ module Tina4
368
368
  require_relative "../tina4"
369
369
  Tina4.initialize!(Dir.pwd)
370
370
  load_routes(Dir.pwd)
371
- Tina4.seed(seed_folder: "seeds", clear: options[:clear])
371
+ Tina4.seed_dir(seed_folder: "seeds", clear: options[:clear])
372
372
  end
373
373
 
374
374
  # ── seed:create ───────────────────────────────────────────────────────
@@ -3,10 +3,10 @@
3
3
  module Tina4
4
4
  # Lightweight dependency injection container.
5
5
  #
6
- # Tina4::Container.register(:mailer) { MailService.new } # transient — new instance each resolve
6
+ # Tina4::Container.register(:mailer) { MailService.new } # transient — new instance each get
7
7
  # Tina4::Container.singleton(:db) { Database.new(ENV["DB_URL"]) } # singleton — memoised
8
8
  # Tina4::Container.register(:cache, RedisCacheInstance) # concrete instance (always same)
9
- # Tina4::Container.resolve(:db) # => Database instance
9
+ # Tina4::Container.get(:db) # => Database instance
10
10
  #
11
11
  module Container
12
12
  class << self
@@ -16,7 +16,7 @@ module Tina4
16
16
 
17
17
  # Register a service by name.
18
18
  # Pass a concrete instance directly, or a block for transient instantiation.
19
- # Blocks are called on every resolve() — use singleton() for memoised factories.
19
+ # Blocks are called on every get() — use singleton() for memoised factories.
20
20
  def register(name, instance = nil, &factory)
21
21
  raise ArgumentError, "provide an instance or a block, not both" if instance && factory
22
22
  raise ArgumentError, "provide an instance or a block" unless instance || factory
@@ -39,7 +39,7 @@ module Tina4
39
39
  # Resolve a service by name.
40
40
  # Singletons and concrete instances return the same object each time.
41
41
  # Transient factories (register with block) return a new object each time.
42
- def resolve(name)
42
+ def get(name)
43
43
  entry = registry[name.to_sym]
44
44
  raise KeyError, "service not registered: #{name}" unless entry
45
45
 
@@ -61,12 +61,12 @@ module Tina4
61
61
  end
62
62
 
63
63
  # Check if a service is registered.
64
- def registered?(name)
64
+ def has(name)
65
65
  registry.key?(name.to_sym)
66
66
  end
67
67
 
68
68
  # Remove all registrations (useful in tests).
69
- def clear!
69
+ def reset
70
70
  @registry = {}
71
71
  end
72
72
  end
data/lib/tina4/crud.rb CHANGED
@@ -211,7 +211,7 @@ module Tina4
211
211
 
212
212
  # GET list (already handled by the page itself)
213
213
  # POST create
214
- Tina4::Router.add_route("POST", api_path, proc { |req, res|
214
+ Tina4::Router.add("POST", api_path, proc { |req, res|
215
215
  begin
216
216
  data = req.body_parsed
217
217
  result = db.insert(table_name, data)
@@ -222,7 +222,7 @@ module Tina4
222
222
  })
223
223
 
224
224
  # PUT update
225
- Tina4::Router.add_route("PUT", "#{api_path}/{id}", proc { |req, res|
225
+ Tina4::Router.add("PUT", "#{api_path}/{id}", proc { |req, res|
226
226
  begin
227
227
  id = req.params["id"]
228
228
  data = req.body_parsed
@@ -234,7 +234,7 @@ module Tina4
234
234
  })
235
235
 
236
236
  # DELETE
237
- Tina4::Router.add_route("DELETE", "#{api_path}/{id}", proc { |req, res|
237
+ Tina4::Router.add("DELETE", "#{api_path}/{id}", proc { |req, res|
238
238
  begin
239
239
  id = req.params["id"]
240
240
  db.delete(table_name, { pk => id })
@@ -82,6 +82,25 @@ module Tina4
82
82
  "odbc" => "Tina4::Drivers::OdbcDriver"
83
83
  }.freeze
84
84
 
85
+ # Static factory — cross-framework consistency: Database.create(url)
86
+ def self.create(url, username: "", password: "", pool: 0)
87
+ new(url, username: username.empty? ? nil : username,
88
+ password: password.empty? ? nil : password,
89
+ pool: pool)
90
+ end
91
+
92
+ # Construct a Database from environment variables.
93
+ # Returns nil if the named env var is not set.
94
+ def self.from_env(env_key: "DATABASE_URL", pool: 0)
95
+ url = ENV[env_key]
96
+ return nil if url.nil? || url.strip.empty?
97
+
98
+ new(url,
99
+ username: ENV["DATABASE_USERNAME"],
100
+ password: ENV["DATABASE_PASSWORD"],
101
+ pool: pool)
102
+ end
103
+
85
104
  def initialize(connection_string = nil, username: nil, password: nil, driver_name: nil, pool: 0)
86
105
  @connection_string = connection_string || ENV["DATABASE_URL"]
87
106
  @username = username || ENV["DATABASE_USERNAME"]
@@ -241,10 +260,19 @@ module Tina4
241
260
  { success: true, last_id: drv.last_insert_id }
242
261
  end
243
262
 
244
- def update(table, data, filter = {})
263
+ def update(table, data, filter = {}, params = nil)
245
264
  cache_invalidate if @cache_enabled
246
265
  drv = current_driver
247
266
 
267
+ # String filter with explicit params array
268
+ if filter.is_a?(String) && !params.nil?
269
+ set_parts = data.keys.map { |k| "#{k} = #{drv.placeholder}" }
270
+ sql = "UPDATE #{table} SET #{set_parts.join(', ')}"
271
+ sql += " WHERE #{filter}" unless filter.empty?
272
+ drv.execute(sql, data.values + Array(params))
273
+ return { success: true }
274
+ end
275
+
248
276
  set_parts = data.keys.map { |k| "#{k} = #{drv.placeholder}" }
249
277
  where_parts = filter.keys.map { |k| "#{k} = #{drv.placeholder}" }
250
278
  sql = "UPDATE #{table} SET #{set_parts.join(', ')}"
@@ -254,7 +282,7 @@ module Tina4
254
282
  { success: true }
255
283
  end
256
284
 
257
- def delete(table, filter = {})
285
+ def delete(table, filter = {}, params = nil)
258
286
  cache_invalidate if @cache_enabled
259
287
  drv = current_driver
260
288
 
@@ -264,11 +292,11 @@ module Tina4
264
292
  return { success: true }
265
293
  end
266
294
 
267
- # String filter — raw WHERE clause
295
+ # String filter — raw WHERE clause with optional params
268
296
  if filter.is_a?(String)
269
297
  sql = "DELETE FROM #{table}"
270
298
  sql += " WHERE #{filter}" unless filter.empty?
271
- drv.execute(sql)
299
+ drv.execute(sql, Array(params))
272
300
  return { success: true }
273
301
  end
274
302
 
@@ -335,18 +363,42 @@ module Tina4
335
363
  raise e
336
364
  end
337
365
 
366
+ # Begin a transaction without a block — matches PHP/Python/Node API.
367
+ def start_transaction
368
+ current_driver.begin_transaction
369
+ end
370
+
371
+ # Commit the current transaction — matches PHP/Python/Node API.
372
+ def commit
373
+ current_driver.commit
374
+ end
375
+
376
+ # Roll back the current transaction — matches PHP/Python/Node API.
377
+ def rollback
378
+ current_driver.rollback
379
+ end
380
+
338
381
  def tables
339
382
  current_driver.tables
340
383
  end
341
384
 
385
+ # Cross-framework alias for tables — matches PHP/Python/Node get_tables.
386
+ alias get_tables tables
387
+
342
388
  def columns(table_name)
343
389
  current_driver.columns(table_name)
344
390
  end
345
391
 
392
+ # Cross-framework alias for columns — matches PHP/Python/Node get_columns.
393
+ alias get_columns columns
394
+
346
395
  def table_exists?(table_name)
347
396
  tables.any? { |t| t.downcase == table_name.to_s.downcase }
348
397
  end
349
398
 
399
+ # Cross-framework alias for table_exists? — matches PHP/Python/Node table_exists.
400
+ alias table_exists table_exists?
401
+
350
402
  # Pre-generate the next available primary key ID using engine-aware strategies.
351
403
  #
352
404
  # Race-safe implementation using a `tina4_sequences` table for SQLite/MySQL/MSSQL
@@ -362,6 +414,40 @@ module Tina4
362
414
  # @param pk_column [String] Primary key column name (default: "id")
363
415
  # @param generator_name [String, nil] Override for sequence/generator name
364
416
  # @return [Integer] The next available ID
417
+ # Returns the underlying driver object (pool's current driver or single driver).
418
+ def get_adapter
419
+ current_driver
420
+ end
421
+
422
+ # Returns the configured pool size, or 1 for single-connection mode.
423
+ def pool_size
424
+ @pool_size > 0 ? @pool_size : 1
425
+ end
426
+
427
+ # Number of connections currently created (lazy pool connections counted).
428
+ def active_count
429
+ if @pool
430
+ @pool.active_count
431
+ else
432
+ @connected ? 1 : 0
433
+ end
434
+ end
435
+
436
+ # Check out a driver from the pool (or return the single driver).
437
+ def checkout
438
+ current_driver
439
+ end
440
+
441
+ # Return a driver to the pool. No-op for round-robin pool or single connection.
442
+ def checkin(_driver)
443
+ # no-op
444
+ end
445
+
446
+ # Close all pooled connections (or the single connection).
447
+ def close_all
448
+ close
449
+ end
450
+
365
451
  def get_next_id(table, pk_column: "id", generator_name: nil)
366
452
  drv = current_driver
367
453
 
@@ -8,6 +8,10 @@ module Tina4
8
8
  def connect(connection_string, username: nil, password: nil)
9
9
  require "sqlite3"
10
10
  db_path = connection_string.sub(/^sqlite:\/\//, "").sub(/^sqlite:/, "")
11
+ # Windows: sqlite:///C:/Users/app.db → /C:/Users/app.db after stripping scheme.
12
+ # The leading / before the drive letter must be removed.
13
+ db_path = db_path[1..] if db_path.match?(/^\/[A-Za-z]:/)
14
+
11
15
  @connection = SQLite3::Database.new(db_path)
12
16
  @connection.results_as_hash = true
13
17
  @connection.execute("PRAGMA journal_mode=WAL")
data/lib/tina4/frond.rb CHANGED
@@ -1931,6 +1931,14 @@ module Tina4
1931
1931
 
1932
1932
  class << self
1933
1933
  attr_accessor :form_token_session_id
1934
+
1935
+ # Set the session ID used for CSRF form token binding.
1936
+ # Parity with Python/PHP/Node: Frond.set_form_token_session_id(id)
1937
+ #
1938
+ # @param session_id [String] The session ID to bind form tokens to
1939
+ def set_form_token_session_id(session_id)
1940
+ self.form_token_session_id = session_id
1941
+ end
1934
1942
  end
1935
1943
 
1936
1944
  # Generate a raw JWT form token string.