tina4ruby 3.13.137 → 3.13.139
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/CHANGELOG.md +40 -0
- data/COMMERCIAL-LICENSE.md +8 -0
- data/LICENSE +373 -0
- data/LICENSE.txt +373 -21
- data/NOTICE +11 -0
- data/README.md +11 -7
- data/exe/tina4ruby +6 -0
- data/lib/tina4/ai.rb +6 -0
- data/lib/tina4/ai_client.rb +10 -3
- data/lib/tina4/api.rb +74 -8
- data/lib/tina4/auth.rb +108 -26
- data/lib/tina4/auto_crud.rb +83 -27
- data/lib/tina4/background.rb +7 -1
- data/lib/tina4/base64.rb +59 -0
- data/lib/tina4/cache.rb +6 -0
- data/lib/tina4/cache_backends/base_backend.rb +14 -0
- data/lib/tina4/cache_backends/database_backend.rb +6 -0
- data/lib/tina4/cache_backends/file_backend.rb +6 -0
- data/lib/tina4/cache_backends/memcached_backend.rb +6 -0
- data/lib/tina4/cache_backends/memory_backend.rb +6 -0
- data/lib/tina4/cache_backends/mongo_backend.rb +22 -1
- data/lib/tina4/cache_backends/redis_backend.rb +6 -0
- data/lib/tina4/cache_backends/valkey_backend.rb +6 -0
- data/lib/tina4/cache_backends.rb +9 -1
- data/lib/tina4/cli.rb +27 -7
- data/lib/tina4/constants.rb +12 -8
- data/lib/tina4/container.rb +6 -0
- data/lib/tina4/context/chunker.rb +6 -0
- data/lib/tina4/context.rb +13 -5
- data/lib/tina4/cors.rb +66 -10
- data/lib/tina4/crud.rb +78 -9
- data/lib/tina4/database/sqlite3_adapter.rb +9 -1
- data/lib/tina4/database.rb +260 -44
- data/lib/tina4/database_adapter.rb +6 -0
- data/lib/tina4/database_result.rb +6 -0
- data/lib/tina4/database_url.rb +6 -0
- data/lib/tina4/debug.rb +6 -0
- data/lib/tina4/dev.rb +6 -0
- data/lib/tina4/dev_admin.rb +222 -89
- data/lib/tina4/dev_mailbox.rb +6 -1
- data/lib/tina4/dispatch_pipeline.rb +86 -1
- data/lib/tina4/docs.rb +6 -0
- data/lib/tina4/docstore.rb +23 -4
- data/lib/tina4/drivers/arango_graph_driver.rb +8 -1
- data/lib/tina4/drivers/bolt_graph_driver.rb +6 -0
- data/lib/tina4/drivers/firebird_driver.rb +95 -6
- data/lib/tina4/drivers/mongodb_driver.rb +6 -0
- data/lib/tina4/drivers/mssql_driver.rb +54 -39
- data/lib/tina4/drivers/mysql_driver.rb +6 -0
- data/lib/tina4/drivers/odbc_driver.rb +6 -0
- data/lib/tina4/drivers/postgres_driver.rb +35 -8
- data/lib/tina4/drivers/schema_split.rb +6 -0
- data/lib/tina4/drivers/sqlite_driver.rb +16 -1
- data/lib/tina4/drivers/ultipa_graph_driver.rb +6 -0
- data/lib/tina4/env.rb +14 -6
- data/lib/tina4/error_overlay.rb +6 -0
- data/lib/tina4/events.rb +6 -0
- data/lib/tina4/feedback.rb +9 -2
- data/lib/tina4/field_types.rb +6 -0
- data/lib/tina4/form_parser.rb +241 -0
- data/lib/tina4/frond.rb +200 -58
- data/lib/tina4/gallery/auth/src/routes/api/gallery_auth.rb +6 -0
- data/lib/tina4/gallery/database/src/routes/api/gallery_db.rb +6 -0
- data/lib/tina4/gallery/error-overlay/src/routes/api/gallery_crash.rb +6 -0
- data/lib/tina4/gallery/orm/src/routes/api/gallery_products.rb +6 -0
- data/lib/tina4/gallery/queue/src/routes/api/gallery_queue.rb +6 -0
- data/lib/tina4/gallery/rest-api/src/routes/api/gallery_hello.rb +6 -0
- data/lib/tina4/gallery/templates/src/routes/gallery_page.rb +6 -0
- data/lib/tina4/graph.rb +6 -0
- data/lib/tina4/graphql.rb +134 -35
- data/lib/tina4/health.rb +6 -0
- data/lib/tina4/html_element.rb +6 -0
- data/lib/tina4/http_server.rb +690 -0
- data/lib/tina4/imap_client.rb +352 -0
- data/lib/tina4/import_helper.rb +6 -0
- data/lib/tina4/job.rb +21 -2
- data/lib/tina4/localization.rb +6 -0
- data/lib/tina4/log.rb +6 -0
- data/lib/tina4/mail_socket.rb +124 -0
- data/lib/tina4/mcp.rb +18 -9
- data/lib/tina4/messenger.rb +86 -55
- data/lib/tina4/metrics.rb +6 -0
- data/lib/tina4/middleware.rb +27 -10
- data/lib/tina4/migration.rb +6 -0
- data/lib/tina4/model_collection.rb +6 -0
- data/lib/tina4/mqtt.rb +9 -2
- data/lib/tina4/mqtt_message.rb +6 -0
- data/lib/tina4/orm.rb +72 -16
- data/lib/tina4/parse_json.rb +25 -0
- data/lib/tina4/plan.rb +9 -2
- data/lib/tina4/point.rb +6 -0
- data/lib/tina4/port_takeover.rb +6 -0
- data/lib/tina4/project_index.rb +6 -0
- data/lib/tina4/public/THIRD-PARTY-NOTICES.md +55 -0
- data/lib/tina4/public/bundled-component-provenance.json +48 -0
- data/lib/tina4/public/js/tina4js.min.js +13 -3
- data/lib/tina4/public/licenses/swagger-ui-3.10.0-original-notice.txt +11 -0
- data/lib/tina4/public/licenses/swagger-ui-5.17.14-NOTICE.txt +2 -0
- data/lib/tina4/public/licenses/swagger-ui-Apache-2.0.txt +202 -0
- data/lib/tina4/public/licenses/tina4-css-MIT.txt +21 -0
- data/lib/tina4/public/licenses/tina4-dev-admin-components.json +240 -0
- data/lib/tina4/public/licenses/tina4-dev-admin-third-party.txt +919 -0
- data/lib/tina4/public/licenses/tina4-js-MIT.txt +20 -0
- data/lib/tina4/push.rb +22 -4
- data/lib/tina4/query_builder.rb +6 -0
- data/lib/tina4/queue.rb +6 -0
- data/lib/tina4/queue_backends/kafka_backend.rb +18 -2
- data/lib/tina4/queue_backends/lite_backend.rb +18 -0
- data/lib/tina4/queue_backends/mongo_backend.rb +18 -0
- data/lib/tina4/queue_backends/rabbitmq_backend.rb +18 -2
- data/lib/tina4/rack_app.rb +62 -50
- data/lib/tina4/rate_limiter.rb +6 -0
- data/lib/tina4/realtime/attachment.rb +6 -0
- data/lib/tina4/realtime/channel.rb +6 -0
- data/lib/tina4/realtime/channel_member.rb +6 -0
- data/lib/tina4/realtime/local_storage.rb +6 -0
- data/lib/tina4/realtime/message.rb +6 -0
- data/lib/tina4/realtime/s3_storage.rb +15 -1
- data/lib/tina4/realtime/storage.rb +6 -0
- data/lib/tina4/realtime/storage_backend.rb +6 -0
- data/lib/tina4/realtime/workspace.rb +6 -0
- data/lib/tina4/realtime.rb +10 -3
- data/lib/tina4/request.rb +90 -127
- data/lib/tina4/response.rb +71 -9
- data/lib/tina4/response_cache.rb +6 -0
- data/lib/tina4/router.rb +6 -0
- data/lib/tina4/secret_file.rb +34 -0
- data/lib/tina4/seeder.rb +9 -3
- data/lib/tina4/service.rb +8 -2
- data/lib/tina4/service_runner.rb +6 -0
- data/lib/tina4/session.rb +6 -0
- data/lib/tina4/session_handlers/database_handler.rb +42 -12
- data/lib/tina4/session_handlers/file_handler.rb +6 -0
- data/lib/tina4/session_handlers/memcached_handler.rb +6 -0
- data/lib/tina4/session_handlers/mongo_handler.rb +108 -39
- data/lib/tina4/session_handlers/mongo_wire_client.rb +23 -9
- data/lib/tina4/session_handlers/redis_handler.rb +6 -0
- data/lib/tina4/session_handlers/resp_client.rb +6 -0
- data/lib/tina4/session_handlers/valkey_handler.rb +6 -0
- data/lib/tina4/shutdown.rb +17 -14
- data/lib/tina4/smtp_client.rb +222 -0
- data/lib/tina4/sql_translator.rb +93 -0
- data/lib/tina4/sqlite3_gem.rb +27 -0
- data/lib/tina4/sso.rb +52 -12
- data/lib/tina4/ssrf.rb +148 -0
- data/lib/tina4/swagger.rb +6 -0
- data/lib/tina4/template.rb +6 -0
- data/lib/tina4/test.rb +8 -2
- data/lib/tina4/test_client.rb +8 -1
- data/lib/tina4/testing.rb +8 -1
- data/lib/tina4/validator.rb +6 -0
- data/lib/tina4/version.rb +7 -1
- data/lib/tina4/webserver.rb +25 -143
- data/lib/tina4/websocket.rb +170 -20
- data/lib/tina4/websocket_backplane.rb +25 -2
- data/lib/tina4/wsdl.rb +54 -18
- data/lib/tina4/xml_parser.rb +268 -0
- data/lib/tina4.rb +98 -33
- data/lib/tina4ruby.rb +6 -0
- metadata +44 -140
data/lib/tina4/cache.rb
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
# Copyright (c) 2026 Code Infinity
|
|
2
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
3
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
4
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
5
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
6
|
+
|
|
1
7
|
module Tina4
|
|
2
8
|
# In-memory TTL cache with tag-based invalidation.
|
|
3
9
|
#
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# Copyright (c) 2026 Code Infinity
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
|
|
2
8
|
|
|
3
9
|
module Tina4
|
|
4
10
|
module CacheBackends
|
|
@@ -72,6 +78,14 @@ module Tina4
|
|
|
72
78
|
def available?
|
|
73
79
|
true
|
|
74
80
|
end
|
|
81
|
+
|
|
82
|
+
# When available? is false because a required gem is not installed, the
|
|
83
|
+
# sentence that names the gem and the install command; nil otherwise.
|
|
84
|
+
#
|
|
85
|
+
# @return [String, nil]
|
|
86
|
+
def missing_driver_message
|
|
87
|
+
nil
|
|
88
|
+
end
|
|
75
89
|
end
|
|
76
90
|
end
|
|
77
91
|
end
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# Copyright (c) 2026 Code Infinity
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
|
|
2
8
|
|
|
3
9
|
require "json"
|
|
4
10
|
require_relative "base_backend"
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# Copyright (c) 2026 Code Infinity
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
|
|
2
8
|
|
|
3
9
|
require "json"
|
|
4
10
|
require "fileutils"
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# Copyright (c) 2026 Code Infinity
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
|
|
2
8
|
|
|
3
9
|
require "json"
|
|
4
10
|
require "socket"
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# Copyright (c) 2026 Code Infinity
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
|
|
2
8
|
|
|
3
9
|
require_relative "base_backend"
|
|
4
10
|
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# Copyright (c) 2026 Code Infinity
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
|
|
2
8
|
|
|
3
9
|
require "json"
|
|
4
10
|
require "uri"
|
|
@@ -17,6 +23,7 @@ module Tina4
|
|
|
17
23
|
@hits = 0
|
|
18
24
|
@misses = 0
|
|
19
25
|
@coll = nil
|
|
26
|
+
@driver_missing = false
|
|
20
27
|
begin
|
|
21
28
|
require "mongo"
|
|
22
29
|
Mongo::Logger.logger.level = Logger::FATAL if defined?(Mongo::Logger)
|
|
@@ -33,7 +40,12 @@ module Tina4
|
|
|
33
40
|
@coll = client[:tina4_cache]
|
|
34
41
|
@coll.indexes.create_one({ expires_at: 1 }, expire_after: 0)
|
|
35
42
|
client.database.command(ping: 1)
|
|
36
|
-
rescue LoadError
|
|
43
|
+
rescue LoadError
|
|
44
|
+
# Recorded apart from a service failure so the factory's fallback
|
|
45
|
+
# warning names the gem only when the gem is really what is missing.
|
|
46
|
+
@driver_missing = true
|
|
47
|
+
@coll = nil
|
|
48
|
+
rescue StandardError
|
|
37
49
|
@coll = nil
|
|
38
50
|
end
|
|
39
51
|
end
|
|
@@ -42,6 +54,15 @@ module Tina4
|
|
|
42
54
|
!@coll.nil?
|
|
43
55
|
end
|
|
44
56
|
|
|
57
|
+
# The install hint for the factory's fallback warning, or nil when the
|
|
58
|
+
# gem loaded and the service is what failed.
|
|
59
|
+
def missing_driver_message
|
|
60
|
+
return nil unless @driver_missing
|
|
61
|
+
|
|
62
|
+
"The 'mongo' gem is required for the mongodb cache backend. Install it with: " \
|
|
63
|
+
"bundle add mongo (or add gem \"mongo\" to your Gemfile)"
|
|
64
|
+
end
|
|
65
|
+
|
|
45
66
|
def get(key)
|
|
46
67
|
if @coll.nil?
|
|
47
68
|
@misses += 1
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# Copyright (c) 2026 Code Infinity
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
|
|
2
8
|
|
|
3
9
|
require "json"
|
|
4
10
|
require "socket"
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# Copyright (c) 2026 Code Infinity
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
|
|
2
8
|
|
|
3
9
|
require_relative "redis_backend"
|
|
4
10
|
|
data/lib/tina4/cache_backends.rb
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# Copyright (c) 2026 Code Infinity
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
|
|
2
8
|
|
|
3
9
|
require_relative "cache_backends/base_backend"
|
|
4
10
|
require_relative "cache_backends/memory_backend"
|
|
@@ -86,9 +92,11 @@ module Tina4
|
|
|
86
92
|
|
|
87
93
|
# Configured backend unusable — degrade to the file backend.
|
|
88
94
|
begin
|
|
95
|
+
install_hint = be.missing_driver_message
|
|
89
96
|
Tina4::Log.warning(
|
|
90
97
|
"Cache backend '#{backend}' is unavailable " \
|
|
91
|
-
"(driver missing or service unreachable) — falling back to 'file'."
|
|
98
|
+
"(driver missing or service unreachable) — falling back to 'file'." \
|
|
99
|
+
"#{install_hint ? " #{install_hint}" : ""}"
|
|
92
100
|
)
|
|
93
101
|
rescue StandardError
|
|
94
102
|
# Logging must never break cache construction.
|
data/lib/tina4/cli.rb
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# Copyright (c) 2026 Code Infinity
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
|
|
2
8
|
|
|
3
9
|
require "optparse"
|
|
4
10
|
require "fileutils"
|
|
@@ -8,6 +14,7 @@ require "tmpdir" # Dir.mktmpdir — used by the ADR-0063 sandbox edit-hint sc
|
|
|
8
14
|
require "stringio" # StringIO — captures generator stdout during the sandbox run
|
|
9
15
|
require "open3" # Open3.capture2e — captures `ruby -c` output+status for `lint`
|
|
10
16
|
require_relative "port_takeover"
|
|
17
|
+
require_relative "env" # Tina4::Env.is_truthy -- resolve_config runs before lib/tina4 loads
|
|
11
18
|
|
|
12
19
|
module Tina4
|
|
13
20
|
class CLI
|
|
@@ -676,7 +683,7 @@ module Tina4
|
|
|
676
683
|
# invokes PHP. That mismatch is why Ruby could not be launched through
|
|
677
684
|
# the shared launcher the other frameworks use.
|
|
678
685
|
opts.on("--managed", "Running under the tina4 CLI supervisor") { options[:managed] = true }
|
|
679
|
-
opts.on("--production", "
|
|
686
|
+
opts.on("--production", "Production mode: Puma if the app bundles it, else the built-in server") { options[:production] = true }
|
|
680
687
|
opts.on("--no-browser", "Do not open browser on start") { options[:no_browser] = true }
|
|
681
688
|
opts.on("--no-reload", "Disable file watcher / live-reload") { options[:no_reload] = true }
|
|
682
689
|
opts.on("--no-kill", "Never take over the port from a stale dev server") { options[:no_kill] = true }
|
|
@@ -708,6 +715,11 @@ module Tina4
|
|
|
708
715
|
|
|
709
716
|
require_relative "../tina4"
|
|
710
717
|
|
|
718
|
+
# --production turns debug off: the explicit flag beats .env and the
|
|
719
|
+
# environment (ADR-0041, ADR-0079 s3). Set before the env files load,
|
|
720
|
+
# which are first-wins. It used to only choose Puma.
|
|
721
|
+
ENV["TINA4_DEBUG"] = "false" if options[:production]
|
|
722
|
+
|
|
711
723
|
root_dir = Dir.pwd
|
|
712
724
|
Tina4.initialize!(root_dir)
|
|
713
725
|
|
|
@@ -727,9 +739,9 @@ module Tina4
|
|
|
727
739
|
|
|
728
740
|
is_debug = Tina4::Env.is_truthy(ENV["TINA4_DEBUG"])
|
|
729
741
|
|
|
730
|
-
#
|
|
731
|
-
#
|
|
732
|
-
# Same production server, same shutdown contract as Tina4.run! - one
|
|
742
|
+
# Puma only with --production AND only when the app bundles it
|
|
743
|
+
# (ADR-0067); otherwise Tina4's built-in server, in development and
|
|
744
|
+
# production alike. Same production server, same shutdown contract as Tina4.run! - one
|
|
733
745
|
# implementation, so the two entry points cannot drift again (the old
|
|
734
746
|
# copy here claimed "we handle DB cleanup" and did no cleanup at all).
|
|
735
747
|
# TINA4_DEFAULT_WEBSERVER=TRUE pins the built-in server even with
|
|
@@ -739,7 +751,6 @@ module Tina4
|
|
|
739
751
|
return
|
|
740
752
|
end
|
|
741
753
|
|
|
742
|
-
Tina4::Log.info("Development server: WEBrick")
|
|
743
754
|
server = Tina4::WebServer.new(app, host: options[:host], port: options[:port])
|
|
744
755
|
server.start
|
|
745
756
|
end
|
|
@@ -3806,7 +3817,7 @@ module Tina4
|
|
|
3806
3817
|
# loopback, not 0.0.0.0. Only the DEFAULT changes: production
|
|
3807
3818
|
# (TINA4_DEBUG off) keeps 0.0.0.0, and a developer who WANTS network
|
|
3808
3819
|
# exposure sets TINA4_HOST=0.0.0.0 to override deliberately.
|
|
3809
|
-
Tina4.
|
|
3820
|
+
Tina4::Env.is_truthy(ENV["TINA4_DEBUG"]) ? "127.0.0.1" : DEFAULT_HOST
|
|
3810
3821
|
end
|
|
3811
3822
|
end
|
|
3812
3823
|
|
|
@@ -3875,9 +3886,18 @@ module Tina4
|
|
|
3875
3886
|
|
|
3876
3887
|
# Gemfile
|
|
3877
3888
|
unless File.exist?(File.join(dir, "Gemfile"))
|
|
3889
|
+
# Same Gemfile `tina4 init ruby` writes. The gem is published as
|
|
3890
|
+
# "tina4ruby" ("tina4-ruby" does not exist, so bundle install failed),
|
|
3891
|
+
# and sqlite3 is an app dependency (ADR-0067): tina4ruby loads it lazily
|
|
3892
|
+
# for the default SQLite database but never declares it.
|
|
3878
3893
|
File.write(File.join(dir, "Gemfile"), <<~RUBY)
|
|
3879
3894
|
source "https://rubygems.org"
|
|
3880
|
-
|
|
3895
|
+
|
|
3896
|
+
gem "tina4ruby", "~> 3.0"
|
|
3897
|
+
|
|
3898
|
+
# SQLite driver for the default database (TINA4_DATABASE_URL=sqlite:...).
|
|
3899
|
+
# tina4ruby loads it lazily; it is an app dependency, like pg or mysql2.
|
|
3900
|
+
gem "sqlite3"
|
|
3881
3901
|
RUBY
|
|
3882
3902
|
end
|
|
3883
3903
|
|
data/lib/tina4/constants.rb
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
# Copyright (c) 2026 Code Infinity
|
|
2
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
3
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
4
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
5
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
6
|
+
|
|
1
7
|
# Tina4 Constants — HTTP status codes and content types.
|
|
2
8
|
#
|
|
3
9
|
# Standard constants for use in route handlers across all Tina4 frameworks.
|
|
@@ -59,25 +65,23 @@ module Tina4
|
|
|
59
65
|
304 => "Not Modified", 307 => "Temporary Redirect", 308 => "Permanent Redirect",
|
|
60
66
|
400 => "Bad Request", 401 => "Unauthorized", 403 => "Forbidden",
|
|
61
67
|
404 => "Not Found", 405 => "Method Not Allowed", 406 => "Not Acceptable",
|
|
68
|
+
408 => "Request Timeout",
|
|
62
69
|
409 => "Conflict", 410 => "Gone", 413 => "Content Too Large",
|
|
63
70
|
415 => "Unsupported Media Type", 422 => "Unprocessable Content",
|
|
64
|
-
429 => "Too Many Requests",
|
|
71
|
+
429 => "Too Many Requests", 431 => "Request Header Fields Too Large",
|
|
65
72
|
500 => "Internal Server Error", 501 => "Not Implemented",
|
|
66
|
-
502 => "Bad Gateway", 503 => "Service Unavailable", 504 => "Gateway Timeout"
|
|
73
|
+
502 => "Bad Gateway", 503 => "Service Unavailable", 504 => "Gateway Timeout",
|
|
74
|
+
505 => "HTTP Version Not Supported"
|
|
67
75
|
}.freeze
|
|
68
76
|
|
|
69
77
|
# Return the canonical HTTP reason phrase for ``status``.
|
|
70
78
|
#
|
|
71
79
|
# Falls back to a sensible label when an exotic status is used. Never
|
|
72
80
|
# returns an empty string — the HTTP/1.1 status line requires a phrase.
|
|
73
|
-
#
|
|
74
|
-
#
|
|
81
|
+
# The table is Tina4's own (the phrases match Python's and PHP's); it no
|
|
82
|
+
# longer defers to Rack::Utils, which is not a dependency.
|
|
75
83
|
def self.http_reason(status)
|
|
76
84
|
code = status.to_i
|
|
77
|
-
if defined?(Rack::Utils::HTTP_STATUS_CODES)
|
|
78
|
-
phrase = Rack::Utils::HTTP_STATUS_CODES[code]
|
|
79
|
-
return phrase if phrase && !phrase.empty?
|
|
80
|
-
end
|
|
81
85
|
phrase = HTTP_REASON_PHRASES[code]
|
|
82
86
|
return phrase if phrase && !phrase.empty?
|
|
83
87
|
return "OK" if code >= 200 && code < 300
|
data/lib/tina4/container.rb
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# Copyright (c) 2026 Code Infinity
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
|
|
2
8
|
|
|
3
9
|
module Tina4
|
|
4
10
|
# Lightweight dependency injection container.
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# Copyright (c) 2026 Code Infinity
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
|
|
2
8
|
|
|
3
9
|
# Tina4 Ruby
|
|
4
10
|
#
|
data/lib/tina4/context.rb
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# Copyright (c) 2026 Code Infinity
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
|
|
2
8
|
|
|
3
9
|
# Tina4 Ruby
|
|
4
10
|
#
|
|
@@ -6,9 +12,10 @@
|
|
|
6
12
|
#
|
|
7
13
|
# Lets a Tina4 app ground its own AI assistant on its own source, offline: it
|
|
8
14
|
# walks the project, chunks code on def/class/module boundaries and docs as
|
|
9
|
-
# prose, and answers keyword/fuzzy queries over a SQLite FTS5 index.
|
|
10
|
-
# sqlite3 gem
|
|
11
|
-
# FTS5 + bm25() built in, so
|
|
15
|
+
# prose, and answers keyword/fuzzy queries over a SQLite FTS5 index. It uses the
|
|
16
|
+
# sqlite3 gem the APP declares (the `tina4 init ruby` Gemfile carries it; tina4ruby
|
|
17
|
+
# itself does not, ADR-0067) and modern SQLite ships FTS5 + bm25() built in, so
|
|
18
|
+
# this adds NO dependency. Without the gem the index reports itself unavailable.
|
|
12
19
|
#
|
|
13
20
|
# The retrieval core is a thin port of tina4-python's tina4_python/context
|
|
14
21
|
# (itself the proven slice of neemee: SqliteFTS + the stable
|
|
@@ -29,6 +36,7 @@ require "pathname"
|
|
|
29
36
|
require "fileutils"
|
|
30
37
|
require "set"
|
|
31
38
|
require_relative "context/chunker"
|
|
39
|
+
require_relative "sqlite3_gem"
|
|
32
40
|
|
|
33
41
|
module Tina4
|
|
34
42
|
class Context
|
|
@@ -70,7 +78,7 @@ module Tina4
|
|
|
70
78
|
@available = !!check.call
|
|
71
79
|
return unless @available
|
|
72
80
|
|
|
73
|
-
|
|
81
|
+
Tina4.require_sqlite3!
|
|
74
82
|
parent = File.dirname(@path)
|
|
75
83
|
FileUtils.mkdir_p(parent) unless parent.empty? || parent == "." || File.directory?(parent)
|
|
76
84
|
# One connection guarded by a Mutex — the dev-MCP reload hook may run on a
|
|
@@ -82,7 +90,7 @@ module Tina4
|
|
|
82
90
|
# ── FTS5 availability ───────────────────────────────────────
|
|
83
91
|
# Whether this Ruby's sqlite3 build supports FTS5 (a real in-memory probe).
|
|
84
92
|
def self.fts5_available?
|
|
85
|
-
|
|
93
|
+
Tina4.require_sqlite3!
|
|
86
94
|
db = SQLite3::Database.new(":memory:")
|
|
87
95
|
begin
|
|
88
96
|
db.execute("CREATE VIRTUAL TABLE _probe USING fts5(x)")
|
data/lib/tina4/cors.rb
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# Copyright (c) 2026 Code Infinity
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
|
|
2
8
|
|
|
3
9
|
module Tina4
|
|
4
10
|
# CORS policy — reads config from env, computes the response headers.
|
|
@@ -37,6 +43,13 @@ module Tina4
|
|
|
37
43
|
@warned = nil
|
|
38
44
|
end
|
|
39
45
|
|
|
46
|
+
# The reasons warned about so far in this process - bounded by
|
|
47
|
+
# construction, never an origin (ADR-0048). Node's corsWarningReasons(),
|
|
48
|
+
# PHP's CorsMiddleware::warnedReasons().
|
|
49
|
+
def warned_reasons
|
|
50
|
+
(@warned || {}).keys.map(&:to_s)
|
|
51
|
+
end
|
|
52
|
+
|
|
40
53
|
# The configured origins, split and emptied of blanks.
|
|
41
54
|
def allowed_origins
|
|
42
55
|
config[:origins].split(",").map(&:strip).reject(&:empty?)
|
|
@@ -56,8 +69,13 @@ module Tina4
|
|
|
56
69
|
allowed = allowed_origins
|
|
57
70
|
request_origin = env["HTTP_ORIGIN"]
|
|
58
71
|
|
|
72
|
+
# A same-origin request (browsers send Origin on every same-origin
|
|
73
|
+
# POST/PUT/PATCH/DELETE) is not a CORS request: it is never warned
|
|
74
|
+
# about. That ONLY silences the warning - it grants no CORS access.
|
|
75
|
+
cross_origin = present?(request_origin) && !same_origin?(env)
|
|
76
|
+
|
|
59
77
|
if allowed.empty?
|
|
60
|
-
warn_once(:unconfigured, request_origin) if
|
|
78
|
+
warn_once(:unconfigured, request_origin) if cross_origin
|
|
61
79
|
return {}
|
|
62
80
|
end
|
|
63
81
|
|
|
@@ -70,7 +88,7 @@ module Tina4
|
|
|
70
88
|
|
|
71
89
|
origin = resolve_origin(env)
|
|
72
90
|
if origin.nil?
|
|
73
|
-
warn_once(:denied, request_origin) if
|
|
91
|
+
warn_once(:denied, request_origin) if cross_origin
|
|
74
92
|
return headers
|
|
75
93
|
end
|
|
76
94
|
|
|
@@ -150,6 +168,22 @@ module Tina4
|
|
|
150
168
|
%w[true 1 yes].include?(config[:credentials].to_s.downcase)
|
|
151
169
|
end
|
|
152
170
|
|
|
171
|
+
# Whether the request's Origin is the request's OWN origin
|
|
172
|
+
# (tina4-python #139). Own origin = scheme://host[:port], the scheme
|
|
173
|
+
# proxy-aware (Request.secure_scheme?, X-Forwarded-Proto first hop) and
|
|
174
|
+
# the host from X-Forwarded-Host, else Host. Both sides are normalised:
|
|
175
|
+
# lower-cased, default ports (http:80, https:443) dropped.
|
|
176
|
+
def same_origin?(env)
|
|
177
|
+
origin = normalize_origin(env["HTTP_ORIGIN"])
|
|
178
|
+
return false if origin.nil?
|
|
179
|
+
|
|
180
|
+
scheme = Tina4::Request.secure_scheme?(env) ? "https" : "http"
|
|
181
|
+
host = (env["HTTP_X_FORWARDED_HOST"] || env["HTTP_HOST"]).to_s.split(",").first.to_s.strip
|
|
182
|
+
return false if host.empty?
|
|
183
|
+
|
|
184
|
+
origin == normalize_origin("#{scheme}://#{host}")
|
|
185
|
+
end
|
|
186
|
+
|
|
153
187
|
private
|
|
154
188
|
|
|
155
189
|
def load_config
|
|
@@ -186,6 +220,20 @@ module Tina4
|
|
|
186
220
|
!value.nil? && !value.to_s.empty?
|
|
187
221
|
end
|
|
188
222
|
|
|
223
|
+
DEFAULT_PORTS = { "http" => 80, "https" => 443 }.freeze
|
|
224
|
+
|
|
225
|
+
# "scheme://host[:port]" lower-cased with a default port dropped, or nil
|
|
226
|
+
# for anything that is not an http(s) origin (e.g. "null").
|
|
227
|
+
def normalize_origin(value)
|
|
228
|
+
match = value.to_s.strip.chomp("/").match(%r{\A(https?)://([^/:\s]+|\[[^\]]+\])(?::(\d+))?\z}i)
|
|
229
|
+
return nil unless match
|
|
230
|
+
|
|
231
|
+
scheme = match[1].downcase
|
|
232
|
+
host = match[2].downcase
|
|
233
|
+
port = match[3]&.to_i
|
|
234
|
+
port.nil? || port == DEFAULT_PORTS[scheme] ? "#{scheme}://#{host}" : "#{scheme}://#{host}:#{port}"
|
|
235
|
+
end
|
|
236
|
+
|
|
189
237
|
def merge_vary(current, field_name)
|
|
190
238
|
parts = current.to_s.split(",").map(&:strip).reject(&:empty?)
|
|
191
239
|
return parts.join(", ") if parts.any? { |p| p.casecmp?(field_name) }
|
|
@@ -193,17 +241,22 @@ module Tina4
|
|
|
193
241
|
(parts + [field_name]).join(", ")
|
|
194
242
|
end
|
|
195
243
|
|
|
196
|
-
# Log an actionable warning at most once per
|
|
244
|
+
# Log an actionable warning at most once per REASON per process.
|
|
197
245
|
#
|
|
198
246
|
# A rejected cross-origin request is otherwise invisible: the browser
|
|
199
247
|
# reports a generic CORS failure and the server log says nothing, so the
|
|
200
248
|
# operator has to read the framework source to find the env var to set.
|
|
249
|
+
#
|
|
250
|
+
# BOUNDED (ADR-0048): keyed by reason only, never by origin. Keying
|
|
251
|
+
# :denied by "denied:<origin>" gave every attacker-chosen Origin header its
|
|
252
|
+
# own @warned entry (never freed) and its own log line - an unbounded
|
|
253
|
+
# ledger and an unbounded log. The message still names the origin that
|
|
254
|
+
# triggered it, the first time per reason.
|
|
201
255
|
def warn_once(reason, request_origin)
|
|
202
256
|
@warned ||= {}
|
|
203
|
-
|
|
204
|
-
return if @warned[key]
|
|
257
|
+
return if @warned[reason]
|
|
205
258
|
|
|
206
|
-
@warned[
|
|
259
|
+
@warned[reason] = true
|
|
207
260
|
Tina4::Log.warning(warning_message(reason, request_origin))
|
|
208
261
|
rescue StandardError
|
|
209
262
|
nil # logging must never break a request
|
|
@@ -211,13 +264,16 @@ module Tina4
|
|
|
211
264
|
|
|
212
265
|
def warning_message(reason, request_origin)
|
|
213
266
|
case reason
|
|
267
|
+
# Neither message ever suggests the wildcard: allowing every website to
|
|
268
|
+
# silence one warning is the wrong fix. Each names the ONE origin to add.
|
|
214
269
|
when :unconfigured
|
|
215
|
-
"CORS:
|
|
216
|
-
"
|
|
217
|
-
"
|
|
270
|
+
"CORS: cross-origin request from #{request_origin}, but no CORS policy is configured, " \
|
|
271
|
+
"so the browser will block this response. If that origin should be allowed, " \
|
|
272
|
+
"add it: TINA4_CORS_ORIGINS=#{request_origin}"
|
|
218
273
|
when :denied
|
|
219
274
|
"CORS: origin #{request_origin} is not in TINA4_CORS_ORIGINS (#{config[:origins]}) " \
|
|
220
|
-
"- the browser will block this response."
|
|
275
|
+
"- the browser will block this response. If it should be allowed, add it: " \
|
|
276
|
+
"TINA4_CORS_ORIGINS=#{config[:origins]},#{request_origin}"
|
|
221
277
|
else
|
|
222
278
|
"CORS: TINA4_CORS_CREDENTIALS is true but TINA4_CORS_ORIGINS is '*'. The Fetch Standard " \
|
|
223
279
|
"forbids Access-Control-Allow-Origin: * with credentials, so credentials are NOT being " \
|
data/lib/tina4/crud.rb
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# Copyright (c) 2026 Code Infinity
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
|
|
2
8
|
require "json"
|
|
3
9
|
|
|
4
10
|
module Tina4
|
|
@@ -59,7 +65,7 @@ module Tina4
|
|
|
59
65
|
query_params = request.respond_to?(:query) ? request.query : {}
|
|
60
66
|
page = [(query_params["page"] || 1).to_i, 1].max
|
|
61
67
|
search = query_params["search"].to_s.strip
|
|
62
|
-
sort_col = query_params["sort"]
|
|
68
|
+
sort_col = crud_sort_column(model, sql, query_params["sort"], pk)
|
|
63
69
|
sort_dir = query_params["sort_dir"] == "desc" ? "desc" : "asc"
|
|
64
70
|
offset = (page - 1) * limit
|
|
65
71
|
|
|
@@ -213,8 +219,8 @@ module Tina4
|
|
|
213
219
|
# POST create
|
|
214
220
|
Tina4::Router.add("POST", api_path, proc { |req, res|
|
|
215
221
|
begin
|
|
216
|
-
data = req.body_parsed
|
|
217
|
-
|
|
222
|
+
data = table_column_attributes(db, table_name, req.body_parsed)
|
|
223
|
+
db.insert(table_name, data)
|
|
218
224
|
res.json({ data: data, message: "Created" }, status: 201)
|
|
219
225
|
rescue => e
|
|
220
226
|
res.json({ error: e.message }, status: 500)
|
|
@@ -225,7 +231,8 @@ module Tina4
|
|
|
225
231
|
Tina4::Router.add("PUT", "#{api_path}/{id}", proc { |req, res|
|
|
226
232
|
begin
|
|
227
233
|
id = req.params["id"]
|
|
228
|
-
|
|
234
|
+
# The row is addressed by the URL id only, never by a body pk.
|
|
235
|
+
data = table_column_attributes(db, table_name, req.body_parsed, strip: pk)
|
|
229
236
|
db.update(table_name, data, { pk => id })
|
|
230
237
|
res.json({ data: data, message: "Updated" })
|
|
231
238
|
rescue => e
|
|
@@ -245,6 +252,68 @@ module Tina4
|
|
|
245
252
|
})
|
|
246
253
|
end
|
|
247
254
|
|
|
255
|
+
# tina4: ADR-0069 - a SQL-mode write body is allow-listed against the
|
|
256
|
+
# table's REAL columns (matched case-insensitively, written in the
|
|
257
|
+
# introspected spelling); unknown keys are dropped, is_deleted is never
|
|
258
|
+
# client-writable, and +strip+ (the pk on update) is removed. Same write
|
|
259
|
+
# rule as AutoCrud (CRUD-DEC-02).
|
|
260
|
+
def table_column_attributes(db, table_name, data, strip: nil)
|
|
261
|
+
return {} unless data.is_a?(Hash)
|
|
262
|
+
|
|
263
|
+
columns = db.columns(table_name).to_h { |column| [column[:name].to_s.downcase, column[:name].to_s] }
|
|
264
|
+
blocked = ["is_deleted", strip.to_s.downcase]
|
|
265
|
+
data.each_with_object({}) do |(key, value), allowed|
|
|
266
|
+
column = columns[key.to_s.downcase]
|
|
267
|
+
next if column.nil? || blocked.include?(column.downcase)
|
|
268
|
+
|
|
269
|
+
allowed[column] = value
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# tina4: ADR-0069 - ?sort reaches ORDER BY only as a column the source
|
|
274
|
+
# itself declares: a model's declared field (resolved to its DB column) or
|
|
275
|
+
# a column of the SQL query's own result set. Anything else falls back to
|
|
276
|
+
# the primary key - this is a rendered page, not an API, so no error.
|
|
277
|
+
def crud_sort_column(model, sql, requested, pk)
|
|
278
|
+
return pk if requested.nil? || requested.empty?
|
|
279
|
+
return model.resolve_field_column(requested) || pk if model
|
|
280
|
+
|
|
281
|
+
sql_result_columns(sql).include?(requested) ? requested : pk
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# The query with any ORDER BY / LIMIT clause removed, then stripped.
|
|
285
|
+
#
|
|
286
|
+
# Line by line with plain string operations - a case-insensitive search
|
|
287
|
+
# for the keyword, then a cut to the end of that line - so it is linear on
|
|
288
|
+
# any input (the regex it replaces was flagged as polynomial by CodeQL).
|
|
289
|
+
# Same result: a keyword is removed with the rest of its line only when at
|
|
290
|
+
# least one character follows it, and the line break is kept.
|
|
291
|
+
def strip_order_and_limit(sql)
|
|
292
|
+
sql.to_s.each_line.map do |line|
|
|
293
|
+
cut_from_keyword(cut_from_keyword(line, /ORDER BY /i), /LIMIT /i)
|
|
294
|
+
end.join.strip
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
# +keyword+ is a fixed literal with no quantifier, so the search is linear.
|
|
298
|
+
def cut_from_keyword(line, keyword)
|
|
299
|
+
ending = line.end_with?("\n") ? "\n" : ""
|
|
300
|
+
content = ending.empty? ? line : line[0...-1]
|
|
301
|
+
at = content.index(keyword)
|
|
302
|
+
return line if at.nil? || content.length <= at + keyword.source.length
|
|
303
|
+
|
|
304
|
+
content[0...at] + ending
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# Column names of the SQL query's result set, read from one probe row.
|
|
308
|
+
def sql_result_columns(sql)
|
|
309
|
+
db = Tina4.database
|
|
310
|
+
return [] unless db
|
|
311
|
+
|
|
312
|
+
base = strip_order_and_limit(sql)
|
|
313
|
+
row = db.fetch("SELECT * FROM (#{base}) AS _crud_sub", [], limit: 1).first
|
|
314
|
+
row ? row.keys.map(&:to_s) : []
|
|
315
|
+
end
|
|
316
|
+
|
|
248
317
|
# Fetch data using an ORM model class
|
|
249
318
|
def fetch_model_data(model, search: "", sort: "id", sort_dir: "asc", limit: 10, offset: 0)
|
|
250
319
|
order_by = "#{sort} #{sort_dir.upcase}"
|
|
@@ -280,12 +349,12 @@ module Tina4
|
|
|
280
349
|
return [[], 0] unless db
|
|
281
350
|
|
|
282
351
|
# Wrap the original SQL for sorting
|
|
283
|
-
|
|
284
|
-
query
|
|
352
|
+
base = strip_order_and_limit(sql)
|
|
353
|
+
query = "#{base} ORDER BY #{sort} #{sort_dir.upcase}"
|
|
285
354
|
|
|
286
355
|
if !search.empty?
|
|
287
356
|
# Wrap in a subquery to add search filtering
|
|
288
|
-
wrapped = "SELECT * FROM (#{
|
|
357
|
+
wrapped = "SELECT * FROM (#{base}) AS _crud_sub WHERE "
|
|
289
358
|
columns = extract_columns(sql)
|
|
290
359
|
search_parts = columns.map { |col| "CAST(#{col} AS TEXT) LIKE ?" }
|
|
291
360
|
wrapped += search_parts.join(" OR ")
|
|
@@ -293,14 +362,14 @@ module Tina4
|
|
|
293
362
|
params = columns.map { "%#{search}%" }
|
|
294
363
|
|
|
295
364
|
# Get total count
|
|
296
|
-
count_sql = "SELECT COUNT(*) as cnt FROM (#{
|
|
365
|
+
count_sql = "SELECT COUNT(*) as cnt FROM (#{base}) AS _crud_cnt WHERE #{search_parts.join(' OR ')}"
|
|
297
366
|
count_result = db.fetch_one(count_sql, params)
|
|
298
367
|
total = count_result ? (count_result[:cnt] || count_result["cnt"] || 0).to_i : 0
|
|
299
368
|
|
|
300
369
|
results = db.fetch(wrapped, params, limit: limit, offset: offset)
|
|
301
370
|
else
|
|
302
371
|
# Get total count
|
|
303
|
-
count_sql = "SELECT COUNT(*) as cnt FROM (#{
|
|
372
|
+
count_sql = "SELECT COUNT(*) as cnt FROM (#{base}) AS _crud_cnt"
|
|
304
373
|
count_result = db.fetch_one(count_sql)
|
|
305
374
|
total = count_result ? (count_result[:cnt] || count_result["cnt"] || 0).to_i : 0
|
|
306
375
|
|