tina4ruby 3.11.15 → 3.11.16
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 +80 -80
- data/LICENSE.txt +21 -21
- data/README.md +137 -137
- data/exe/tina4ruby +5 -5
- data/lib/tina4/ai.rb +696 -696
- data/lib/tina4/api.rb +189 -189
- data/lib/tina4/auth.rb +305 -305
- data/lib/tina4/auto_crud.rb +244 -244
- data/lib/tina4/cache.rb +154 -154
- data/lib/tina4/cli.rb +1449 -1449
- data/lib/tina4/constants.rb +46 -46
- data/lib/tina4/container.rb +74 -74
- data/lib/tina4/cors.rb +74 -74
- data/lib/tina4/crud.rb +692 -692
- data/lib/tina4/database/sqlite3_adapter.rb +165 -165
- data/lib/tina4/database.rb +625 -625
- data/lib/tina4/database_result.rb +208 -208
- data/lib/tina4/debug.rb +8 -8
- data/lib/tina4/dev.rb +14 -14
- data/lib/tina4/dev_admin.rb +1289 -935
- data/lib/tina4/dev_mailbox.rb +191 -191
- data/lib/tina4/drivers/firebird_driver.rb +124 -124
- data/lib/tina4/drivers/mongodb_driver.rb +561 -561
- data/lib/tina4/drivers/mssql_driver.rb +112 -112
- data/lib/tina4/drivers/mysql_driver.rb +90 -90
- data/lib/tina4/drivers/odbc_driver.rb +191 -191
- data/lib/tina4/drivers/postgres_driver.rb +116 -116
- data/lib/tina4/drivers/sqlite_driver.rb +122 -122
- data/lib/tina4/env.rb +95 -95
- data/lib/tina4/error_overlay.rb +252 -252
- data/lib/tina4/events.rb +109 -109
- data/lib/tina4/field_types.rb +154 -154
- data/lib/tina4/frond.rb +2087 -2025
- data/lib/tina4/gallery/auth/meta.json +1 -1
- data/lib/tina4/gallery/auth/src/routes/api/gallery_auth.rb +114 -114
- data/lib/tina4/gallery/database/meta.json +1 -1
- data/lib/tina4/gallery/database/src/routes/api/gallery_db.rb +43 -43
- data/lib/tina4/gallery/error-overlay/meta.json +1 -1
- data/lib/tina4/gallery/error-overlay/src/routes/api/gallery_crash.rb +17 -17
- data/lib/tina4/gallery/orm/meta.json +1 -1
- data/lib/tina4/gallery/orm/src/routes/api/gallery_products.rb +16 -16
- data/lib/tina4/gallery/queue/meta.json +1 -1
- data/lib/tina4/gallery/queue/src/routes/api/gallery_queue.rb +325 -325
- data/lib/tina4/gallery/rest-api/meta.json +1 -1
- data/lib/tina4/gallery/rest-api/src/routes/api/gallery_hello.rb +14 -14
- data/lib/tina4/gallery/templates/meta.json +1 -1
- data/lib/tina4/gallery/templates/src/routes/gallery_page.rb +12 -12
- data/lib/tina4/gallery/templates/src/templates/gallery_page.twig +257 -257
- data/lib/tina4/graphql.rb +966 -966
- data/lib/tina4/health.rb +39 -39
- data/lib/tina4/html_element.rb +170 -170
- data/lib/tina4/job.rb +80 -80
- data/lib/tina4/localization.rb +168 -168
- data/lib/tina4/log.rb +203 -203
- data/lib/tina4/mcp.rb +871 -696
- data/lib/tina4/messenger.rb +587 -587
- data/lib/tina4/metrics.rb +793 -793
- data/lib/tina4/middleware.rb +445 -445
- data/lib/tina4/migration.rb +451 -451
- data/lib/tina4/orm.rb +790 -790
- data/lib/tina4/plan.rb +471 -0
- data/lib/tina4/project_index.rb +366 -0
- data/lib/tina4/public/css/tina4.css +2463 -2463
- data/lib/tina4/public/css/tina4.min.css +1 -1
- data/lib/tina4/public/images/logo.svg +5 -5
- data/lib/tina4/public/js/frond.min.js +2 -2
- data/lib/tina4/public/js/tina4-dev-admin.js +1264 -565
- data/lib/tina4/public/js/tina4-dev-admin.min.js +1264 -480
- data/lib/tina4/public/js/tina4.min.js +92 -92
- data/lib/tina4/public/js/tina4js.min.js +48 -48
- data/lib/tina4/public/swagger/index.html +90 -90
- data/lib/tina4/public/swagger/oauth2-redirect.html +63 -63
- data/lib/tina4/query_builder.rb +380 -380
- data/lib/tina4/queue.rb +366 -366
- data/lib/tina4/queue_backends/kafka_backend.rb +80 -80
- data/lib/tina4/queue_backends/lite_backend.rb +298 -298
- data/lib/tina4/queue_backends/mongo_backend.rb +126 -126
- data/lib/tina4/queue_backends/rabbitmq_backend.rb +73 -73
- data/lib/tina4/rack_app.rb +817 -817
- data/lib/tina4/rate_limiter.rb +130 -130
- data/lib/tina4/request.rb +268 -268
- data/lib/tina4/response.rb +346 -346
- data/lib/tina4/response_cache.rb +551 -551
- data/lib/tina4/router.rb +406 -406
- data/lib/tina4/scss/tina4css/_alerts.scss +34 -34
- data/lib/tina4/scss/tina4css/_badges.scss +22 -22
- data/lib/tina4/scss/tina4css/_buttons.scss +69 -69
- data/lib/tina4/scss/tina4css/_cards.scss +49 -49
- data/lib/tina4/scss/tina4css/_forms.scss +156 -156
- data/lib/tina4/scss/tina4css/_grid.scss +81 -81
- data/lib/tina4/scss/tina4css/_modals.scss +84 -84
- data/lib/tina4/scss/tina4css/_nav.scss +149 -149
- data/lib/tina4/scss/tina4css/_reset.scss +94 -94
- data/lib/tina4/scss/tina4css/_tables.scss +54 -54
- data/lib/tina4/scss/tina4css/_typography.scss +55 -55
- data/lib/tina4/scss/tina4css/_utilities.scss +197 -197
- data/lib/tina4/scss/tina4css/_variables.scss +117 -117
- data/lib/tina4/scss/tina4css/base.scss +1 -1
- data/lib/tina4/scss/tina4css/colors.scss +48 -48
- data/lib/tina4/scss/tina4css/tina4.scss +17 -17
- data/lib/tina4/scss_compiler.rb +178 -178
- data/lib/tina4/seeder.rb +567 -567
- data/lib/tina4/service_runner.rb +303 -303
- data/lib/tina4/session.rb +297 -297
- data/lib/tina4/session_handlers/database_handler.rb +72 -72
- data/lib/tina4/session_handlers/file_handler.rb +67 -67
- data/lib/tina4/session_handlers/mongo_handler.rb +49 -49
- data/lib/tina4/session_handlers/redis_handler.rb +43 -43
- data/lib/tina4/session_handlers/valkey_handler.rb +43 -43
- data/lib/tina4/shutdown.rb +84 -84
- data/lib/tina4/sql_translation.rb +158 -158
- data/lib/tina4/swagger.rb +124 -124
- data/lib/tina4/template.rb +894 -894
- data/lib/tina4/templates/base.twig +26 -26
- data/lib/tina4/templates/errors/302.twig +14 -14
- data/lib/tina4/templates/errors/401.twig +9 -9
- data/lib/tina4/templates/errors/403.twig +29 -29
- data/lib/tina4/templates/errors/404.twig +29 -29
- data/lib/tina4/templates/errors/500.twig +38 -38
- data/lib/tina4/templates/errors/502.twig +9 -9
- data/lib/tina4/templates/errors/503.twig +12 -12
- data/lib/tina4/templates/errors/base.twig +37 -37
- data/lib/tina4/test_client.rb +159 -159
- data/lib/tina4/testing.rb +340 -340
- data/lib/tina4/validator.rb +174 -174
- data/lib/tina4/version.rb +1 -1
- data/lib/tina4/webserver.rb +312 -312
- data/lib/tina4/websocket.rb +343 -343
- data/lib/tina4/websocket_backplane.rb +190 -190
- data/lib/tina4/wsdl.rb +564 -564
- data/lib/tina4.rb +460 -458
- data/lib/tina4ruby.rb +4 -4
- metadata +5 -3
data/lib/tina4/auto_crud.rb
CHANGED
|
@@ -1,244 +1,244 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
require "json"
|
|
3
|
-
|
|
4
|
-
module Tina4
|
|
5
|
-
module AutoCrud
|
|
6
|
-
class << self
|
|
7
|
-
# Track registered model classes
|
|
8
|
-
def models
|
|
9
|
-
@models ||= []
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
# Register a model for auto-CRUD
|
|
13
|
-
def register(model_class)
|
|
14
|
-
models << model_class unless models.include?(model_class)
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
# Generate REST endpoints for all registered models
|
|
18
|
-
def generate_routes(prefix: "/api")
|
|
19
|
-
models.each do |model_class|
|
|
20
|
-
generate_routes_for(model_class, prefix: prefix)
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
# Build a sample request body from ORM field definitions.
|
|
25
|
-
def build_example(model_class)
|
|
26
|
-
example = {}
|
|
27
|
-
return example unless model_class.respond_to?(:field_definitions)
|
|
28
|
-
|
|
29
|
-
model_class.field_definitions.each do |name, opts|
|
|
30
|
-
next if opts[:primary_key] && opts[:auto_increment]
|
|
31
|
-
|
|
32
|
-
case opts[:type]
|
|
33
|
-
when :integer
|
|
34
|
-
example[name.to_s] = 0
|
|
35
|
-
when :numeric, :float, :decimal
|
|
36
|
-
example[name.to_s] = 0.0
|
|
37
|
-
when :boolean
|
|
38
|
-
example[name.to_s] = true
|
|
39
|
-
when :datetime
|
|
40
|
-
example[name.to_s] = "2024-01-01T00:00:00"
|
|
41
|
-
else
|
|
42
|
-
example[name.to_s] = "string"
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
example
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
# Generate REST endpoints for a single model class
|
|
49
|
-
def generate_routes_for(model_class, prefix: "/api")
|
|
50
|
-
table = model_class.table_name
|
|
51
|
-
pk = model_class.primary_key_field || :id
|
|
52
|
-
pretty_name = table.to_s.split("_").map(&:capitalize).join(" ")
|
|
53
|
-
example_body = build_example(model_class)
|
|
54
|
-
|
|
55
|
-
# GET /api/{table} -- list all with pagination, filtering, sorting
|
|
56
|
-
Tina4::Router.add("GET", "#{prefix}/#{table}", proc { |req, res|
|
|
57
|
-
begin
|
|
58
|
-
per_page = (req.query["per_page"] || req.query["limit"] || 10).to_i
|
|
59
|
-
page = (req.query["page"] || 1).to_i
|
|
60
|
-
limit = per_page
|
|
61
|
-
offset = req.query["offset"] ? req.query["offset"].to_i : (page - 1) * per_page
|
|
62
|
-
order_by = parse_sort(req.query["sort"])
|
|
63
|
-
|
|
64
|
-
# Filter support: ?filter[field]=value
|
|
65
|
-
filter_conditions = []
|
|
66
|
-
filter_values = []
|
|
67
|
-
req.query.each do |key, value|
|
|
68
|
-
if key =~ /\Afilter\[(\w+)\]\z/
|
|
69
|
-
filter_conditions << "#{$1} = ?"
|
|
70
|
-
filter_values << value
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
if filter_conditions.empty?
|
|
75
|
-
records = model_class.all(limit: limit, offset: offset, order_by: order_by)
|
|
76
|
-
total = model_class.count
|
|
77
|
-
else
|
|
78
|
-
where_clause = filter_conditions.join(" AND ")
|
|
79
|
-
records = model_class.where(where_clause, filter_values)
|
|
80
|
-
total = records.length
|
|
81
|
-
# Apply manual pagination for filtered results
|
|
82
|
-
records = records.slice(offset, limit) || []
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
res.json({
|
|
86
|
-
data: records.map { |r| r.to_h },
|
|
87
|
-
total: total,
|
|
88
|
-
limit: limit,
|
|
89
|
-
offset: offset
|
|
90
|
-
})
|
|
91
|
-
rescue => e
|
|
92
|
-
res.json({ error: e.message }, status: 500)
|
|
93
|
-
end
|
|
94
|
-
}, swagger_meta: { summary: "List all #{pretty_name}", tags: [table.to_s] })
|
|
95
|
-
|
|
96
|
-
# GET /api/{table}/{id} -- get single record
|
|
97
|
-
Tina4::Router.add("GET", "#{prefix}/#{table}/{id}", proc { |req, res|
|
|
98
|
-
begin
|
|
99
|
-
id = req.params["id"]
|
|
100
|
-
record = model_class.find_by_id(id.to_i)
|
|
101
|
-
if record
|
|
102
|
-
res.json({ data: record.to_h })
|
|
103
|
-
else
|
|
104
|
-
res.json({ error: "Not found" }, status: 404)
|
|
105
|
-
end
|
|
106
|
-
rescue => e
|
|
107
|
-
res.json({ error: e.message }, status: 500)
|
|
108
|
-
end
|
|
109
|
-
}, swagger_meta: { summary: "Get #{pretty_name} by ID", tags: [table.to_s] })
|
|
110
|
-
|
|
111
|
-
# POST /api/{table} -- create record
|
|
112
|
-
Tina4::Router.add("POST", "#{prefix}/#{table}", proc { |req, res|
|
|
113
|
-
begin
|
|
114
|
-
attributes = req.body_parsed
|
|
115
|
-
record = model_class.create(attributes)
|
|
116
|
-
if record.persisted?
|
|
117
|
-
res.json({ data: record.to_h }, status: 201)
|
|
118
|
-
else
|
|
119
|
-
res.json({ errors: record.errors }, status: 422)
|
|
120
|
-
end
|
|
121
|
-
rescue => e
|
|
122
|
-
res.json({ error: e.message }, status: 500)
|
|
123
|
-
end
|
|
124
|
-
}, swagger_meta: {
|
|
125
|
-
summary: "Create #{pretty_name}",
|
|
126
|
-
tags: [table.to_s],
|
|
127
|
-
request_body: {
|
|
128
|
-
"description" => "#{pretty_name} data",
|
|
129
|
-
"required" => true,
|
|
130
|
-
"content" => {
|
|
131
|
-
"application/json" => {
|
|
132
|
-
"schema" => { "type" => "object" },
|
|
133
|
-
"example" => example_body
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
})
|
|
138
|
-
|
|
139
|
-
# PUT /api/{table}/{id} -- update record
|
|
140
|
-
Tina4::Router.add("PUT", "#{prefix}/#{table}/{id}", proc { |req, res|
|
|
141
|
-
begin
|
|
142
|
-
id = req.params["id"]
|
|
143
|
-
record = model_class.find_by_id(id.to_i)
|
|
144
|
-
unless record
|
|
145
|
-
next res.json({ error: "Not found" }, status: 404)
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
attributes = req.body_parsed
|
|
149
|
-
attributes.each do |key, value|
|
|
150
|
-
setter = "#{key}="
|
|
151
|
-
record.__send__(setter, value) if record.respond_to?(setter)
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
if record.save
|
|
155
|
-
res.json({ data: record.to_h })
|
|
156
|
-
else
|
|
157
|
-
res.json({ errors: record.errors }, status: 422)
|
|
158
|
-
end
|
|
159
|
-
rescue => e
|
|
160
|
-
res.json({ error: e.message }, status: 500)
|
|
161
|
-
end
|
|
162
|
-
}, swagger_meta: {
|
|
163
|
-
summary: "Update #{pretty_name}",
|
|
164
|
-
tags: [table.to_s],
|
|
165
|
-
request_body: {
|
|
166
|
-
"description" => "#{pretty_name} data",
|
|
167
|
-
"required" => true,
|
|
168
|
-
"content" => {
|
|
169
|
-
"application/json" => {
|
|
170
|
-
"schema" => { "type" => "object" },
|
|
171
|
-
"example" => example_body
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
})
|
|
176
|
-
|
|
177
|
-
# DELETE /api/{table}/{id} -- delete record
|
|
178
|
-
Tina4::Router.add("DELETE", "#{prefix}/#{table}/{id}", proc { |req, res|
|
|
179
|
-
begin
|
|
180
|
-
id = req.params["id"]
|
|
181
|
-
record = model_class.find_by_id(id.to_i)
|
|
182
|
-
unless record
|
|
183
|
-
next res.json({ error: "Not found" }, status: 404)
|
|
184
|
-
end
|
|
185
|
-
|
|
186
|
-
if record.delete
|
|
187
|
-
res.json({ message: "Deleted" })
|
|
188
|
-
else
|
|
189
|
-
res.json({ error: "Delete failed" }, status: 500)
|
|
190
|
-
end
|
|
191
|
-
rescue => e
|
|
192
|
-
res.json({ error: e.message }, status: 500)
|
|
193
|
-
end
|
|
194
|
-
}, swagger_meta: { summary: "Delete #{pretty_name}", tags: [table.to_s] })
|
|
195
|
-
end
|
|
196
|
-
|
|
197
|
-
# Discover ORM model classes from a directory and register them.
|
|
198
|
-
#
|
|
199
|
-
# @param models_dir [String] directory to scan (default "src/orm")
|
|
200
|
-
# @param prefix [String] URL prefix for generated routes (default "/api")
|
|
201
|
-
# @return [Array<String>] list of discovered model class names
|
|
202
|
-
def discover(models_dir = "src/orm", prefix: "/api")
|
|
203
|
-
discovered = []
|
|
204
|
-
return discovered unless Dir.exist?(models_dir)
|
|
205
|
-
|
|
206
|
-
Dir.glob(File.join(models_dir, "*.rb")).each do |file|
|
|
207
|
-
require_relative File.expand_path(file)
|
|
208
|
-
end
|
|
209
|
-
|
|
210
|
-
# Find all ORM subclasses that have a table_name
|
|
211
|
-
ObjectSpace.each_object(Class).select { |c| c < Tina4::ORM rescue false }.each do |klass|
|
|
212
|
-
next unless klass.respond_to?(:table_name) && klass.table_name
|
|
213
|
-
register(klass)
|
|
214
|
-
discovered << klass.name
|
|
215
|
-
end
|
|
216
|
-
|
|
217
|
-
generate_routes(prefix: prefix) unless discovered.empty?
|
|
218
|
-
discovered
|
|
219
|
-
end
|
|
220
|
-
|
|
221
|
-
def clear!
|
|
222
|
-
@models = []
|
|
223
|
-
end
|
|
224
|
-
|
|
225
|
-
# Alias for parity with other frameworks
|
|
226
|
-
alias_method :clear, :clear!
|
|
227
|
-
|
|
228
|
-
private
|
|
229
|
-
|
|
230
|
-
# Parse sort parameter: "-name,created_at" => "name DESC, created_at ASC"
|
|
231
|
-
def parse_sort(sort_str)
|
|
232
|
-
return nil if sort_str.nil? || sort_str.empty?
|
|
233
|
-
sort_str.split(",").map do |field|
|
|
234
|
-
field = field.strip
|
|
235
|
-
if field.start_with?("-")
|
|
236
|
-
"#{field[1..-1]} DESC"
|
|
237
|
-
else
|
|
238
|
-
"#{field} ASC"
|
|
239
|
-
end
|
|
240
|
-
end.join(", ")
|
|
241
|
-
end
|
|
242
|
-
end
|
|
243
|
-
end
|
|
244
|
-
end
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require "json"
|
|
3
|
+
|
|
4
|
+
module Tina4
|
|
5
|
+
module AutoCrud
|
|
6
|
+
class << self
|
|
7
|
+
# Track registered model classes
|
|
8
|
+
def models
|
|
9
|
+
@models ||= []
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Register a model for auto-CRUD
|
|
13
|
+
def register(model_class)
|
|
14
|
+
models << model_class unless models.include?(model_class)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Generate REST endpoints for all registered models
|
|
18
|
+
def generate_routes(prefix: "/api")
|
|
19
|
+
models.each do |model_class|
|
|
20
|
+
generate_routes_for(model_class, prefix: prefix)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Build a sample request body from ORM field definitions.
|
|
25
|
+
def build_example(model_class)
|
|
26
|
+
example = {}
|
|
27
|
+
return example unless model_class.respond_to?(:field_definitions)
|
|
28
|
+
|
|
29
|
+
model_class.field_definitions.each do |name, opts|
|
|
30
|
+
next if opts[:primary_key] && opts[:auto_increment]
|
|
31
|
+
|
|
32
|
+
case opts[:type]
|
|
33
|
+
when :integer
|
|
34
|
+
example[name.to_s] = 0
|
|
35
|
+
when :numeric, :float, :decimal
|
|
36
|
+
example[name.to_s] = 0.0
|
|
37
|
+
when :boolean
|
|
38
|
+
example[name.to_s] = true
|
|
39
|
+
when :datetime
|
|
40
|
+
example[name.to_s] = "2024-01-01T00:00:00"
|
|
41
|
+
else
|
|
42
|
+
example[name.to_s] = "string"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
example
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Generate REST endpoints for a single model class
|
|
49
|
+
def generate_routes_for(model_class, prefix: "/api")
|
|
50
|
+
table = model_class.table_name
|
|
51
|
+
pk = model_class.primary_key_field || :id
|
|
52
|
+
pretty_name = table.to_s.split("_").map(&:capitalize).join(" ")
|
|
53
|
+
example_body = build_example(model_class)
|
|
54
|
+
|
|
55
|
+
# GET /api/{table} -- list all with pagination, filtering, sorting
|
|
56
|
+
Tina4::Router.add("GET", "#{prefix}/#{table}", proc { |req, res|
|
|
57
|
+
begin
|
|
58
|
+
per_page = (req.query["per_page"] || req.query["limit"] || 10).to_i
|
|
59
|
+
page = (req.query["page"] || 1).to_i
|
|
60
|
+
limit = per_page
|
|
61
|
+
offset = req.query["offset"] ? req.query["offset"].to_i : (page - 1) * per_page
|
|
62
|
+
order_by = parse_sort(req.query["sort"])
|
|
63
|
+
|
|
64
|
+
# Filter support: ?filter[field]=value
|
|
65
|
+
filter_conditions = []
|
|
66
|
+
filter_values = []
|
|
67
|
+
req.query.each do |key, value|
|
|
68
|
+
if key =~ /\Afilter\[(\w+)\]\z/
|
|
69
|
+
filter_conditions << "#{$1} = ?"
|
|
70
|
+
filter_values << value
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
if filter_conditions.empty?
|
|
75
|
+
records = model_class.all(limit: limit, offset: offset, order_by: order_by)
|
|
76
|
+
total = model_class.count
|
|
77
|
+
else
|
|
78
|
+
where_clause = filter_conditions.join(" AND ")
|
|
79
|
+
records = model_class.where(where_clause, filter_values)
|
|
80
|
+
total = records.length
|
|
81
|
+
# Apply manual pagination for filtered results
|
|
82
|
+
records = records.slice(offset, limit) || []
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
res.json({
|
|
86
|
+
data: records.map { |r| r.to_h },
|
|
87
|
+
total: total,
|
|
88
|
+
limit: limit,
|
|
89
|
+
offset: offset
|
|
90
|
+
})
|
|
91
|
+
rescue => e
|
|
92
|
+
res.json({ error: e.message }, status: 500)
|
|
93
|
+
end
|
|
94
|
+
}, swagger_meta: { summary: "List all #{pretty_name}", tags: [table.to_s] })
|
|
95
|
+
|
|
96
|
+
# GET /api/{table}/{id} -- get single record
|
|
97
|
+
Tina4::Router.add("GET", "#{prefix}/#{table}/{id}", proc { |req, res|
|
|
98
|
+
begin
|
|
99
|
+
id = req.params["id"]
|
|
100
|
+
record = model_class.find_by_id(id.to_i)
|
|
101
|
+
if record
|
|
102
|
+
res.json({ data: record.to_h })
|
|
103
|
+
else
|
|
104
|
+
res.json({ error: "Not found" }, status: 404)
|
|
105
|
+
end
|
|
106
|
+
rescue => e
|
|
107
|
+
res.json({ error: e.message }, status: 500)
|
|
108
|
+
end
|
|
109
|
+
}, swagger_meta: { summary: "Get #{pretty_name} by ID", tags: [table.to_s] })
|
|
110
|
+
|
|
111
|
+
# POST /api/{table} -- create record
|
|
112
|
+
Tina4::Router.add("POST", "#{prefix}/#{table}", proc { |req, res|
|
|
113
|
+
begin
|
|
114
|
+
attributes = req.body_parsed
|
|
115
|
+
record = model_class.create(attributes)
|
|
116
|
+
if record.persisted?
|
|
117
|
+
res.json({ data: record.to_h }, status: 201)
|
|
118
|
+
else
|
|
119
|
+
res.json({ errors: record.errors }, status: 422)
|
|
120
|
+
end
|
|
121
|
+
rescue => e
|
|
122
|
+
res.json({ error: e.message }, status: 500)
|
|
123
|
+
end
|
|
124
|
+
}, swagger_meta: {
|
|
125
|
+
summary: "Create #{pretty_name}",
|
|
126
|
+
tags: [table.to_s],
|
|
127
|
+
request_body: {
|
|
128
|
+
"description" => "#{pretty_name} data",
|
|
129
|
+
"required" => true,
|
|
130
|
+
"content" => {
|
|
131
|
+
"application/json" => {
|
|
132
|
+
"schema" => { "type" => "object" },
|
|
133
|
+
"example" => example_body
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
# PUT /api/{table}/{id} -- update record
|
|
140
|
+
Tina4::Router.add("PUT", "#{prefix}/#{table}/{id}", proc { |req, res|
|
|
141
|
+
begin
|
|
142
|
+
id = req.params["id"]
|
|
143
|
+
record = model_class.find_by_id(id.to_i)
|
|
144
|
+
unless record
|
|
145
|
+
next res.json({ error: "Not found" }, status: 404)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
attributes = req.body_parsed
|
|
149
|
+
attributes.each do |key, value|
|
|
150
|
+
setter = "#{key}="
|
|
151
|
+
record.__send__(setter, value) if record.respond_to?(setter)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
if record.save
|
|
155
|
+
res.json({ data: record.to_h })
|
|
156
|
+
else
|
|
157
|
+
res.json({ errors: record.errors }, status: 422)
|
|
158
|
+
end
|
|
159
|
+
rescue => e
|
|
160
|
+
res.json({ error: e.message }, status: 500)
|
|
161
|
+
end
|
|
162
|
+
}, swagger_meta: {
|
|
163
|
+
summary: "Update #{pretty_name}",
|
|
164
|
+
tags: [table.to_s],
|
|
165
|
+
request_body: {
|
|
166
|
+
"description" => "#{pretty_name} data",
|
|
167
|
+
"required" => true,
|
|
168
|
+
"content" => {
|
|
169
|
+
"application/json" => {
|
|
170
|
+
"schema" => { "type" => "object" },
|
|
171
|
+
"example" => example_body
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
# DELETE /api/{table}/{id} -- delete record
|
|
178
|
+
Tina4::Router.add("DELETE", "#{prefix}/#{table}/{id}", proc { |req, res|
|
|
179
|
+
begin
|
|
180
|
+
id = req.params["id"]
|
|
181
|
+
record = model_class.find_by_id(id.to_i)
|
|
182
|
+
unless record
|
|
183
|
+
next res.json({ error: "Not found" }, status: 404)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
if record.delete
|
|
187
|
+
res.json({ message: "Deleted" })
|
|
188
|
+
else
|
|
189
|
+
res.json({ error: "Delete failed" }, status: 500)
|
|
190
|
+
end
|
|
191
|
+
rescue => e
|
|
192
|
+
res.json({ error: e.message }, status: 500)
|
|
193
|
+
end
|
|
194
|
+
}, swagger_meta: { summary: "Delete #{pretty_name}", tags: [table.to_s] })
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Discover ORM model classes from a directory and register them.
|
|
198
|
+
#
|
|
199
|
+
# @param models_dir [String] directory to scan (default "src/orm")
|
|
200
|
+
# @param prefix [String] URL prefix for generated routes (default "/api")
|
|
201
|
+
# @return [Array<String>] list of discovered model class names
|
|
202
|
+
def discover(models_dir = "src/orm", prefix: "/api")
|
|
203
|
+
discovered = []
|
|
204
|
+
return discovered unless Dir.exist?(models_dir)
|
|
205
|
+
|
|
206
|
+
Dir.glob(File.join(models_dir, "*.rb")).each do |file|
|
|
207
|
+
require_relative File.expand_path(file)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Find all ORM subclasses that have a table_name
|
|
211
|
+
ObjectSpace.each_object(Class).select { |c| c < Tina4::ORM rescue false }.each do |klass|
|
|
212
|
+
next unless klass.respond_to?(:table_name) && klass.table_name
|
|
213
|
+
register(klass)
|
|
214
|
+
discovered << klass.name
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
generate_routes(prefix: prefix) unless discovered.empty?
|
|
218
|
+
discovered
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def clear!
|
|
222
|
+
@models = []
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Alias for parity with other frameworks
|
|
226
|
+
alias_method :clear, :clear!
|
|
227
|
+
|
|
228
|
+
private
|
|
229
|
+
|
|
230
|
+
# Parse sort parameter: "-name,created_at" => "name DESC, created_at ASC"
|
|
231
|
+
def parse_sort(sort_str)
|
|
232
|
+
return nil if sort_str.nil? || sort_str.empty?
|
|
233
|
+
sort_str.split(",").map do |field|
|
|
234
|
+
field = field.strip
|
|
235
|
+
if field.start_with?("-")
|
|
236
|
+
"#{field[1..-1]} DESC"
|
|
237
|
+
else
|
|
238
|
+
"#{field} ASC"
|
|
239
|
+
end
|
|
240
|
+
end.join(", ")
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
end
|