tina4ruby 0.4.0
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 +7 -0
- data/CHANGELOG.md +80 -0
- data/LICENSE.txt +21 -0
- data/README.md +768 -0
- data/exe/tina4 +4 -0
- data/lib/tina4/api.rb +152 -0
- data/lib/tina4/auth.rb +139 -0
- data/lib/tina4/cli.rb +349 -0
- data/lib/tina4/crud.rb +124 -0
- data/lib/tina4/database.rb +135 -0
- data/lib/tina4/database_result.rb +89 -0
- data/lib/tina4/debug.rb +83 -0
- data/lib/tina4/dev.rb +15 -0
- data/lib/tina4/dev_reload.rb +68 -0
- data/lib/tina4/drivers/firebird_driver.rb +94 -0
- data/lib/tina4/drivers/mssql_driver.rb +112 -0
- data/lib/tina4/drivers/mysql_driver.rb +90 -0
- data/lib/tina4/drivers/postgres_driver.rb +99 -0
- data/lib/tina4/drivers/sqlite_driver.rb +85 -0
- data/lib/tina4/env.rb +55 -0
- data/lib/tina4/field_types.rb +84 -0
- data/lib/tina4/graphql.rb +837 -0
- data/lib/tina4/localization.rb +100 -0
- data/lib/tina4/middleware.rb +59 -0
- data/lib/tina4/migration.rb +124 -0
- data/lib/tina4/orm.rb +168 -0
- data/lib/tina4/public/css/tina4.css +2286 -0
- data/lib/tina4/public/css/tina4.min.css +2 -0
- data/lib/tina4/public/js/tina4.js +134 -0
- data/lib/tina4/public/js/tina4helper.js +387 -0
- data/lib/tina4/queue.rb +117 -0
- data/lib/tina4/queue_backends/kafka_backend.rb +80 -0
- data/lib/tina4/queue_backends/lite_backend.rb +79 -0
- data/lib/tina4/queue_backends/rabbitmq_backend.rb +73 -0
- data/lib/tina4/rack_app.rb +150 -0
- data/lib/tina4/request.rb +158 -0
- data/lib/tina4/response.rb +172 -0
- data/lib/tina4/router.rb +148 -0
- data/lib/tina4/scss/tina4css/_alerts.scss +34 -0
- data/lib/tina4/scss/tina4css/_badges.scss +22 -0
- data/lib/tina4/scss/tina4css/_buttons.scss +69 -0
- data/lib/tina4/scss/tina4css/_cards.scss +49 -0
- data/lib/tina4/scss/tina4css/_forms.scss +156 -0
- data/lib/tina4/scss/tina4css/_grid.scss +81 -0
- data/lib/tina4/scss/tina4css/_modals.scss +84 -0
- data/lib/tina4/scss/tina4css/_nav.scss +149 -0
- data/lib/tina4/scss/tina4css/_reset.scss +94 -0
- data/lib/tina4/scss/tina4css/_tables.scss +54 -0
- data/lib/tina4/scss/tina4css/_typography.scss +55 -0
- data/lib/tina4/scss/tina4css/_utilities.scss +197 -0
- data/lib/tina4/scss/tina4css/_variables.scss +117 -0
- data/lib/tina4/scss/tina4css/base.scss +1 -0
- data/lib/tina4/scss/tina4css/colors.scss +48 -0
- data/lib/tina4/scss/tina4css/tina4.scss +17 -0
- data/lib/tina4/scss_compiler.rb +131 -0
- data/lib/tina4/seeder.rb +529 -0
- data/lib/tina4/session.rb +145 -0
- data/lib/tina4/session_handlers/file_handler.rb +55 -0
- data/lib/tina4/session_handlers/mongo_handler.rb +49 -0
- data/lib/tina4/session_handlers/redis_handler.rb +43 -0
- data/lib/tina4/swagger.rb +123 -0
- data/lib/tina4/template.rb +478 -0
- data/lib/tina4/templates/base.twig +26 -0
- data/lib/tina4/templates/errors/403.twig +22 -0
- data/lib/tina4/templates/errors/404.twig +22 -0
- data/lib/tina4/templates/errors/500.twig +22 -0
- data/lib/tina4/testing.rb +213 -0
- data/lib/tina4/version.rb +5 -0
- data/lib/tina4/webserver.rb +101 -0
- data/lib/tina4/websocket.rb +167 -0
- data/lib/tina4/wsdl.rb +164 -0
- data/lib/tina4.rb +259 -0
- data/lib/tina4ruby.rb +4 -0
- metadata +324 -0
data/lib/tina4/seeder.rb
ADDED
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module Tina4
|
|
7
|
+
# Zero-dependency fake data generator with deterministic seeding.
|
|
8
|
+
# Uses Ruby's built-in Random for reproducible data generation.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# fake = Tina4::FakeData.new(seed: 42)
|
|
12
|
+
# fake.name # => "Sarah Johnson"
|
|
13
|
+
# fake.email # => "sarah.johnson123@example.com"
|
|
14
|
+
# fake.integer(1, 100)
|
|
15
|
+
class FakeData
|
|
16
|
+
FIRST_NAMES = %w[
|
|
17
|
+
James Mary Robert Patricia John Jennifer Michael Linda David Elizabeth
|
|
18
|
+
William Barbara Richard Susan Joseph Jessica Thomas Sarah Charles Karen
|
|
19
|
+
Christopher Lisa Daniel Nancy Matthew Betty Anthony Margaret Mark Sandra
|
|
20
|
+
Donald Ashley Steven Dorothy Paul Kimberly Andrew Emily Joshua Donna
|
|
21
|
+
Kenneth Michelle Kevin Carol Brian Amanda George Melissa Timothy Deborah
|
|
22
|
+
Ronald Stephanie Edward Rebecca Jason Sharon Jeffrey Laura Ryan Cynthia
|
|
23
|
+
Jacob Kathleen Gary Amy Nicholas Angela Eric Shirley Jonathan Anna
|
|
24
|
+
Stephen Brenda Larry Pamela Justin Emma Scott Nicole Brandon Helen
|
|
25
|
+
Benjamin Samantha Samuel Katherine Raymond Christine Gregory Debra
|
|
26
|
+
Frank Rachel Alexander Carolyn Patrick Janet Jack Catherine Andre Aisha
|
|
27
|
+
Wei Yuki Carlos Fatima Raj Priya Mohammed Sophia Liam Olivia Noah Ava
|
|
28
|
+
Ethan Mia Lucas Isabella Mason Charlotte Logan Amelia Aiden Harper
|
|
29
|
+
].freeze
|
|
30
|
+
|
|
31
|
+
LAST_NAMES = %w[
|
|
32
|
+
Smith Johnson Williams Brown Jones Garcia Miller Davis Rodriguez Martinez
|
|
33
|
+
Hernandez Lopez Gonzalez Wilson Anderson Thomas Taylor Moore Jackson Martin
|
|
34
|
+
Lee Perez Thompson White Harris Sanchez Clark Ramirez Lewis Robinson Walker
|
|
35
|
+
Young Allen King Wright Scott Torres Nguyen Hill Flores Green Adams Nelson
|
|
36
|
+
Baker Hall Rivera Campbell Mitchell Carter Roberts Gomez Phillips Evans
|
|
37
|
+
Turner Diaz Parker Cruz Edwards Collins Reyes Stewart Morris Morales
|
|
38
|
+
Murphy Cook Rogers Gutierrez Ortiz Morgan Cooper Peterson Bailey Reed
|
|
39
|
+
Kelly Howard Ramos Kim Cox Ward Richardson Watson Brooks Chavez Wood
|
|
40
|
+
James Bennett Gray Mendoza Ruiz Hughes Price Alvarez Castillo Sanders
|
|
41
|
+
Patel Müller Nakamura Singh Chen Silva Ali Okafor
|
|
42
|
+
].freeze
|
|
43
|
+
|
|
44
|
+
WORDS = %w[
|
|
45
|
+
the be to of and a in that have it for not on with he as you do at
|
|
46
|
+
this but his by from they we say her she or an will my one all would
|
|
47
|
+
there their what so up out if about who get which go me when make can
|
|
48
|
+
like time no just him know take people into year your good some could
|
|
49
|
+
them see other than then now look only come its over think also back
|
|
50
|
+
after use two how our work first well way even new want because any
|
|
51
|
+
these give day most us great small large every found still between name
|
|
52
|
+
should home big end along each much both help line turn move thing right
|
|
53
|
+
same old better point long real system data report order product service
|
|
54
|
+
customer account payment record total status market world company project
|
|
55
|
+
team value process business group result information development management
|
|
56
|
+
quality performance technology support research design program network
|
|
57
|
+
].freeze
|
|
58
|
+
|
|
59
|
+
CITIES = [
|
|
60
|
+
"New York", "London", "Tokyo", "Paris", "Berlin", "Sydney", "Toronto",
|
|
61
|
+
"Mumbai", "São Paulo", "Cairo", "Lagos", "Dubai", "Singapore",
|
|
62
|
+
"Hong Kong", "Seoul", "Mexico City", "Bangkok", "Istanbul", "Moscow",
|
|
63
|
+
"Rome", "Barcelona", "Amsterdam", "Nairobi", "Cape Town", "Johannesburg",
|
|
64
|
+
"Buenos Aires", "Lima", "Santiago", "Jakarta", "Manila", "Kuala Lumpur",
|
|
65
|
+
"Auckland", "Vancouver", "Chicago", "San Francisco", "Los Angeles",
|
|
66
|
+
"Miami", "Boston", "Seattle", "Denver"
|
|
67
|
+
].freeze
|
|
68
|
+
|
|
69
|
+
COUNTRIES = [
|
|
70
|
+
"United States", "United Kingdom", "Canada", "Australia", "Germany",
|
|
71
|
+
"France", "Japan", "Brazil", "India", "South Africa", "Nigeria",
|
|
72
|
+
"Egypt", "Kenya", "Mexico", "Argentina", "Chile", "Colombia", "Spain",
|
|
73
|
+
"Italy", "Netherlands", "Sweden", "Norway", "Denmark", "Finland",
|
|
74
|
+
"Switzerland", "Belgium", "Austria", "New Zealand", "Singapore",
|
|
75
|
+
"South Korea", "Thailand", "Indonesia", "Philippines", "Vietnam",
|
|
76
|
+
"Malaysia", "United Arab Emirates", "Saudi Arabia", "Turkey", "Poland"
|
|
77
|
+
].freeze
|
|
78
|
+
|
|
79
|
+
DOMAINS = %w[
|
|
80
|
+
example.com test.org sample.net demo.io mail.com
|
|
81
|
+
inbox.org webmail.net company.com corp.io biz.net
|
|
82
|
+
].freeze
|
|
83
|
+
|
|
84
|
+
STREETS = %w[Main Oak Pine Maple Cedar Elm Park Lake Hill River Church Market King Queen High].freeze
|
|
85
|
+
STREET_TYPES = %w[Street Avenue Road Drive Lane Boulevard Way Place].freeze
|
|
86
|
+
COMPANY_WORDS = %w[Tech Global Apex Nova Core Prime Next Blue Bright Smart Swift Peak Fusion Pulse Vertex].freeze
|
|
87
|
+
COMPANY_SUFFIXES = %w[Inc Corp Ltd LLC Group Solutions Systems Labs].freeze
|
|
88
|
+
|
|
89
|
+
def initialize(seed: nil)
|
|
90
|
+
@rng = seed ? Random.new(seed) : Random.new
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def first_name
|
|
94
|
+
FIRST_NAMES[@rng.rand(FIRST_NAMES.length)]
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def last_name
|
|
98
|
+
LAST_NAMES[@rng.rand(LAST_NAMES.length)]
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def name
|
|
102
|
+
"#{first_name} #{last_name}"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def email(from_name: nil)
|
|
106
|
+
if from_name
|
|
107
|
+
local = from_name.downcase.split.join(".")
|
|
108
|
+
else
|
|
109
|
+
local = "#{first_name.downcase}.#{last_name.downcase}"
|
|
110
|
+
end
|
|
111
|
+
local += @rng.rand(1..999).to_s
|
|
112
|
+
"#{local}@#{DOMAINS[@rng.rand(DOMAINS.length)]}"
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def phone
|
|
116
|
+
area = @rng.rand(200..999)
|
|
117
|
+
mid = @rng.rand(100..999)
|
|
118
|
+
tail = @rng.rand(1000..9999)
|
|
119
|
+
"+1 (#{area}) #{mid}-#{tail}"
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def sentence(words: 6)
|
|
123
|
+
w = Array.new(words) { WORDS[@rng.rand(WORDS.length)] }
|
|
124
|
+
w[0] = w[0].capitalize
|
|
125
|
+
"#{w.join(' ')}."
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def paragraph(sentences: 3)
|
|
129
|
+
Array.new(sentences) { sentence(words: @rng.rand(5..12)) }.join(" ")
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def text(max_length: 200)
|
|
133
|
+
t = paragraph(sentences: 2)
|
|
134
|
+
t.length > max_length ? t[0...max_length] : t
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def word
|
|
138
|
+
WORDS[@rng.rand(WORDS.length)]
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def slug(words: 3)
|
|
142
|
+
Array.new(words) { WORDS[@rng.rand(WORDS.length)] }.join("-")
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def url
|
|
146
|
+
"https://#{DOMAINS[@rng.rand(DOMAINS.length)]}/#{slug}"
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def integer(min: 0, max: 10_000)
|
|
150
|
+
@rng.rand(min..max)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def numeric(min: 0.0, max: 1000.0, decimals: 2)
|
|
154
|
+
val = min + @rng.rand * (max - min)
|
|
155
|
+
val.round(decimals)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def boolean
|
|
159
|
+
@rng.rand(2)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def datetime(start_year: 2020, end_year: 2026)
|
|
163
|
+
start_time = Time.new(start_year, 1, 1)
|
|
164
|
+
end_time = Time.new(end_year, 12, 31, 23, 59, 59)
|
|
165
|
+
delta = (end_time - start_time).to_i
|
|
166
|
+
Time.at(start_time.to_i + @rng.rand(0..delta))
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def date(start_year: 2020, end_year: 2026)
|
|
170
|
+
datetime(start_year: start_year, end_year: end_year).strftime("%Y-%m-%d")
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def timestamp(start_year: 2020, end_year: 2026)
|
|
174
|
+
datetime(start_year: start_year, end_year: end_year).strftime("%Y-%m-%d %H:%M:%S")
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def blob(size: 64)
|
|
178
|
+
SecureRandom.random_bytes(size)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def json_data(keys: nil)
|
|
182
|
+
if keys
|
|
183
|
+
keys.each_with_object({}) { |k, h| h[k] = word }
|
|
184
|
+
else
|
|
185
|
+
n = @rng.rand(2..5)
|
|
186
|
+
n.times.each_with_object({}) { |_, h| h[word] = word }
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def choice(items)
|
|
191
|
+
items[@rng.rand(items.length)]
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def city
|
|
195
|
+
CITIES[@rng.rand(CITIES.length)]
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def country
|
|
199
|
+
COUNTRIES[@rng.rand(COUNTRIES.length)]
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def address
|
|
203
|
+
"#{@rng.rand(1..9999)} #{STREETS[@rng.rand(STREETS.length)]} #{STREET_TYPES[@rng.rand(STREET_TYPES.length)]}"
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def zip_code
|
|
207
|
+
@rng.rand(10_000..99_999).to_s
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def company
|
|
211
|
+
w1 = COMPANY_WORDS[@rng.rand(COMPANY_WORDS.length)]
|
|
212
|
+
w2 = COMPANY_WORDS[@rng.rand(COMPANY_WORDS.length)]
|
|
213
|
+
suffix = COMPANY_SUFFIXES[@rng.rand(COMPANY_SUFFIXES.length)]
|
|
214
|
+
"#{w1}#{w2} #{suffix}"
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def color_hex
|
|
218
|
+
"#%06x" % @rng.rand(0..0xFFFFFF)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def uuid
|
|
222
|
+
h = Array.new(32) { "0123456789abcdef"[@rng.rand(16)] }.join
|
|
223
|
+
"#{h[0..7]}-#{h[8..11]}-#{h[12..15]}-#{h[16..19]}-#{h[20..31]}"
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def password(length: 16)
|
|
227
|
+
chars = [*"a".."z", *"A".."Z", *"0".."9"]
|
|
228
|
+
Array.new(length) { chars[@rng.rand(chars.length)] }.join
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Generate appropriate data based on field definition and column name.
|
|
232
|
+
def for_field(field_def, column_name = nil)
|
|
233
|
+
col = (column_name || "").to_s.downcase
|
|
234
|
+
type = field_def[:type]
|
|
235
|
+
|
|
236
|
+
# Skip auto-increment primary keys
|
|
237
|
+
return nil if field_def[:primary_key] && field_def[:auto_increment]
|
|
238
|
+
|
|
239
|
+
case type
|
|
240
|
+
when :integer
|
|
241
|
+
return integer(min: 18, max: 85) if col.include?("age")
|
|
242
|
+
return integer(min: 1950, max: 2026) if col.include?("year")
|
|
243
|
+
return integer(min: 1, max: 100) if col =~ /quantity|qty|count/
|
|
244
|
+
return boolean if col =~ /active|enabled|visible|^is_/
|
|
245
|
+
return integer(min: 1, max: 10) if col =~ /rating|score/
|
|
246
|
+
integer(min: 1, max: 10_000)
|
|
247
|
+
|
|
248
|
+
when :float, :decimal
|
|
249
|
+
decimals = field_def[:scale] || 2
|
|
250
|
+
return numeric(min: 0.01, max: 9999.99, decimals: decimals) if col =~ /price|cost|amount|total|fee/
|
|
251
|
+
return numeric(min: 0.0, max: 100.0, decimals: decimals) if col =~ /rate|percent|ratio/
|
|
252
|
+
return numeric(min: -90.0, max: 90.0, decimals: 6) if col.include?("lat")
|
|
253
|
+
return numeric(min: -180.0, max: 180.0, decimals: 6) if col =~ /lon|lng/
|
|
254
|
+
numeric(min: 0.0, max: 10_000.0, decimals: decimals)
|
|
255
|
+
|
|
256
|
+
when :date
|
|
257
|
+
date
|
|
258
|
+
|
|
259
|
+
when :datetime, :timestamp
|
|
260
|
+
timestamp
|
|
261
|
+
|
|
262
|
+
when :boolean
|
|
263
|
+
boolean
|
|
264
|
+
|
|
265
|
+
when :blob
|
|
266
|
+
blob
|
|
267
|
+
|
|
268
|
+
when :json
|
|
269
|
+
json_data
|
|
270
|
+
|
|
271
|
+
when :string, :text
|
|
272
|
+
max_len = field_def[:length] || 255
|
|
273
|
+
val = generate_string_for(col, max_len)
|
|
274
|
+
val.length > max_len ? val[0...max_len] : val
|
|
275
|
+
|
|
276
|
+
else
|
|
277
|
+
word
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
private
|
|
282
|
+
|
|
283
|
+
def generate_string_for(col, max_len)
|
|
284
|
+
return email[0...max_len] if col.include?("email")
|
|
285
|
+
return name[0...max_len] if %w[name full_name fullname display_name].include?(col)
|
|
286
|
+
return first_name[0...max_len] if col.include?("first") && col.include?("name")
|
|
287
|
+
return last_name[0...max_len] if col.include?("last") && col.include?("name")
|
|
288
|
+
return last_name[0...max_len] if col =~ /surname|family_name/
|
|
289
|
+
return phone[0...max_len] if col =~ /phone|tel|mobile|cell/
|
|
290
|
+
return url[0...max_len] if col =~ /url|website|link|href/
|
|
291
|
+
return address[0...max_len] if col =~ /address|street/
|
|
292
|
+
return city[0...max_len] if col =~ /city|town/
|
|
293
|
+
return country[0...max_len] if col.include?("country")
|
|
294
|
+
return zip_code[0...max_len] if col =~ /zip|postal/
|
|
295
|
+
return company[0...max_len] if col =~ /company|organization|org/
|
|
296
|
+
return color_hex[0...max_len] if col =~ /color|colour/
|
|
297
|
+
return uuid[0...max_len] if col =~ /uuid|guid/
|
|
298
|
+
return slug[0...max_len] if col.include?("slug")
|
|
299
|
+
return sentence(words: @rng.rand(3..6)).chomp(".")[0...max_len] if col =~ /title|subject|heading/
|
|
300
|
+
return text(max_length: max_len) if col =~ /description|summary|bio|about/
|
|
301
|
+
return paragraph(sentences: 2)[0...max_len] if col =~ /content|body|text|note|comment/
|
|
302
|
+
return choice(%w[active inactive pending archived])[0...max_len] if col.include?("status")
|
|
303
|
+
return choice(%w[standard premium basic enterprise custom])[0...max_len] if col =~ /type|category|kind/
|
|
304
|
+
return word[0...max_len] if col =~ /tag|label/
|
|
305
|
+
return password(length: [16, max_len].min) if col =~ /password|pass|secret/
|
|
306
|
+
return password(length: [32, max_len].min) if col =~ /token|key|hash/
|
|
307
|
+
return "#{first_name.downcase}#{@rng.rand(1..99)}"[0...max_len] if col =~ /username|user_name|login/
|
|
308
|
+
|
|
309
|
+
sentence(words: @rng.rand(2..5)).chomp(".")[0...max_len]
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
# Seed an ORM class with auto-generated fake data.
|
|
314
|
+
#
|
|
315
|
+
# @param orm_class [Class] ORM subclass (e.g., User, Product)
|
|
316
|
+
# @param count [Integer] number of records to insert
|
|
317
|
+
# @param overrides [Hash] field overrides — static values or lambdas receiving FakeData
|
|
318
|
+
# @param clear [Boolean] delete existing records before seeding
|
|
319
|
+
# @param seed [Integer, nil] random seed for reproducible data
|
|
320
|
+
# @return [Integer] number of records inserted
|
|
321
|
+
#
|
|
322
|
+
# @example
|
|
323
|
+
# Tina4.seed_orm(User, count: 50)
|
|
324
|
+
# Tina4.seed_orm(Order, count: 200, overrides: { status: ->(f) { f.choice(%w[pending shipped]) } })
|
|
325
|
+
def self.seed_orm(orm_class, count: 10, overrides: {}, clear: false, seed: nil)
|
|
326
|
+
fake = FakeData.new(seed: seed)
|
|
327
|
+
fields = orm_class.field_definitions
|
|
328
|
+
table = orm_class.table_name
|
|
329
|
+
|
|
330
|
+
if fields.empty?
|
|
331
|
+
Tina4::Debug.error("Seeder: No fields found on #{orm_class.name}")
|
|
332
|
+
return 0
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
db = Tina4.database
|
|
336
|
+
unless db
|
|
337
|
+
Tina4::Debug.error("Seeder: No database connection. Set Tina4.database first.")
|
|
338
|
+
return 0
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
# Idempotency check
|
|
342
|
+
unless clear
|
|
343
|
+
begin
|
|
344
|
+
result = db.fetch_one("SELECT count(*) as cnt FROM #{table}")
|
|
345
|
+
if result && result[:cnt].to_i >= count
|
|
346
|
+
Tina4::Debug.info("Seeder: #{table} already has #{result[:cnt]} records, skipping")
|
|
347
|
+
return 0
|
|
348
|
+
end
|
|
349
|
+
rescue => e
|
|
350
|
+
# Table might not exist
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
# Clear if requested
|
|
355
|
+
if clear
|
|
356
|
+
begin
|
|
357
|
+
db.execute("DELETE FROM #{table}")
|
|
358
|
+
Tina4::Debug.info("Seeder: Cleared #{table}")
|
|
359
|
+
rescue => e
|
|
360
|
+
Tina4::Debug.warn("Seeder: Could not clear #{table}: #{e.message}")
|
|
361
|
+
end
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
# Identify fields to populate
|
|
365
|
+
pk_field = orm_class.primary_key_field
|
|
366
|
+
insert_fields = fields.reject { |name, opts| opts[:primary_key] && opts[:auto_increment] }
|
|
367
|
+
|
|
368
|
+
inserted = 0
|
|
369
|
+
count.times do |i|
|
|
370
|
+
attrs = {}
|
|
371
|
+
|
|
372
|
+
insert_fields.each do |name, field_def|
|
|
373
|
+
if overrides.key?(name)
|
|
374
|
+
val = overrides[name]
|
|
375
|
+
attrs[name] = val.respond_to?(:call) ? val.call(fake) : val
|
|
376
|
+
else
|
|
377
|
+
generated = fake.for_field(field_def, name)
|
|
378
|
+
attrs[name] = generated unless generated.nil?
|
|
379
|
+
end
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
begin
|
|
383
|
+
obj = orm_class.new(attrs)
|
|
384
|
+
if obj.save
|
|
385
|
+
inserted += 1
|
|
386
|
+
else
|
|
387
|
+
Tina4::Debug.warn("Seeder: Insert failed for #{table} row #{i + 1}: #{obj.errors.join(', ')}")
|
|
388
|
+
end
|
|
389
|
+
rescue => e
|
|
390
|
+
Tina4::Debug.warn("Seeder: Insert failed for #{table} row #{i + 1}: #{e.message}")
|
|
391
|
+
end
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
Tina4::Debug.info("Seeder: Inserted #{inserted}/#{count} records into #{table}")
|
|
395
|
+
inserted
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
# Seed a raw database table (no ORM class needed).
|
|
399
|
+
#
|
|
400
|
+
# @param table_name [String] name of the table
|
|
401
|
+
# @param columns [Hash] { column_name: type_string } — supports :integer, :string, :text, etc.
|
|
402
|
+
# @param count [Integer] number of records to insert
|
|
403
|
+
# @param overrides [Hash] field overrides
|
|
404
|
+
# @param clear [Boolean] delete before seeding
|
|
405
|
+
# @param seed [Integer, nil] random seed
|
|
406
|
+
# @return [Integer] records inserted
|
|
407
|
+
def self.seed_table(table_name, columns, count: 10, overrides: {}, clear: false, seed: nil)
|
|
408
|
+
fake = FakeData.new(seed: seed)
|
|
409
|
+
db = Tina4.database
|
|
410
|
+
|
|
411
|
+
unless db
|
|
412
|
+
Tina4::Debug.error("Seeder: No database connection.")
|
|
413
|
+
return 0
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
if clear
|
|
417
|
+
begin
|
|
418
|
+
db.execute("DELETE FROM #{table_name}")
|
|
419
|
+
rescue => e
|
|
420
|
+
Tina4::Debug.warn("Seeder: Could not clear #{table_name}: #{e.message}")
|
|
421
|
+
end
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
inserted = 0
|
|
425
|
+
count.times do |i|
|
|
426
|
+
row = {}
|
|
427
|
+
columns.each do |col_name, type_str|
|
|
428
|
+
if overrides.key?(col_name)
|
|
429
|
+
val = overrides[col_name]
|
|
430
|
+
row[col_name] = val.respond_to?(:call) ? val.call(fake) : val
|
|
431
|
+
else
|
|
432
|
+
field_def = { type: type_str.to_sym }
|
|
433
|
+
row[col_name] = fake.for_field(field_def, col_name)
|
|
434
|
+
end
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
begin
|
|
438
|
+
db.insert(table_name, row)
|
|
439
|
+
inserted += 1
|
|
440
|
+
rescue => e
|
|
441
|
+
Tina4::Debug.warn("Seeder: Insert failed for #{table_name} row #{i + 1}: #{e.message}")
|
|
442
|
+
end
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
Tina4::Debug.info("Seeder: Inserted #{inserted}/#{count} records into #{table_name}")
|
|
446
|
+
inserted
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
# Builder class for seeding multiple ORM classes with dependency resolution.
|
|
450
|
+
#
|
|
451
|
+
# @example
|
|
452
|
+
# seeder = Tina4::Seeder.new
|
|
453
|
+
# seeder.add(User, count: 20)
|
|
454
|
+
# seeder.add(Order, count: 100, overrides: { status: "pending" })
|
|
455
|
+
# seeder.run(clear: true)
|
|
456
|
+
class Seeder
|
|
457
|
+
def initialize
|
|
458
|
+
@tasks = []
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
def add(orm_class, count: 10, overrides: {}, seed: nil)
|
|
462
|
+
@tasks << {
|
|
463
|
+
orm_class: orm_class,
|
|
464
|
+
count: count,
|
|
465
|
+
overrides: overrides,
|
|
466
|
+
seed: seed
|
|
467
|
+
}
|
|
468
|
+
self
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
def run(clear: false)
|
|
472
|
+
results = {}
|
|
473
|
+
|
|
474
|
+
if clear
|
|
475
|
+
@tasks.reverse_each do |task|
|
|
476
|
+
begin
|
|
477
|
+
Tina4.database&.execute("DELETE FROM #{task[:orm_class].table_name}")
|
|
478
|
+
Tina4::Debug.info("Seeder: Cleared #{task[:orm_class].table_name}")
|
|
479
|
+
rescue => e
|
|
480
|
+
Tina4::Debug.warn("Seeder: Could not clear #{task[:orm_class].table_name}: #{e.message}")
|
|
481
|
+
end
|
|
482
|
+
end
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
@tasks.each do |task|
|
|
486
|
+
n = Tina4.seed_orm(
|
|
487
|
+
task[:orm_class],
|
|
488
|
+
count: task[:count],
|
|
489
|
+
overrides: task[:overrides],
|
|
490
|
+
clear: false,
|
|
491
|
+
seed: task[:seed]
|
|
492
|
+
)
|
|
493
|
+
results[task[:orm_class].name] = n
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
results
|
|
497
|
+
end
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
# Run all seed files in the given folder.
|
|
501
|
+
#
|
|
502
|
+
# @param seed_folder [String] path to seed files (default: "seeds")
|
|
503
|
+
def self.seed(seed_folder: "seeds", clear: false)
|
|
504
|
+
unless Dir.exist?(seed_folder)
|
|
505
|
+
Tina4::Debug.info("Seeder: No seeds folder found at #{seed_folder}")
|
|
506
|
+
return
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
files = Dir.glob(File.join(seed_folder, "*.rb")).sort
|
|
510
|
+
files.reject! { |f| File.basename(f).start_with?("_") }
|
|
511
|
+
|
|
512
|
+
if files.empty?
|
|
513
|
+
Tina4::Debug.info("Seeder: No seed files found in #{seed_folder}")
|
|
514
|
+
return
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
Tina4::Debug.info("Seeder: Found #{files.length} seed file(s) in #{seed_folder}")
|
|
518
|
+
|
|
519
|
+
files.each do |filepath|
|
|
520
|
+
begin
|
|
521
|
+
Tina4::Debug.info("Seeder: Running #{File.basename(filepath)}...")
|
|
522
|
+
load filepath
|
|
523
|
+
Tina4::Debug.info("Seeder: Completed #{File.basename(filepath)}")
|
|
524
|
+
rescue => e
|
|
525
|
+
Tina4::Debug.error("Seeder: Failed to run #{File.basename(filepath)}: #{e.message}")
|
|
526
|
+
end
|
|
527
|
+
end
|
|
528
|
+
end
|
|
529
|
+
end
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require "securerandom"
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Tina4
|
|
6
|
+
class Session
|
|
7
|
+
DEFAULT_OPTIONS = {
|
|
8
|
+
cookie_name: "tina4_session",
|
|
9
|
+
secret: nil,
|
|
10
|
+
max_age: 86400,
|
|
11
|
+
handler: :file,
|
|
12
|
+
handler_options: {}
|
|
13
|
+
}.freeze
|
|
14
|
+
|
|
15
|
+
attr_reader :id, :data
|
|
16
|
+
|
|
17
|
+
def initialize(env, options = {})
|
|
18
|
+
@options = DEFAULT_OPTIONS.merge(options)
|
|
19
|
+
@options[:secret] ||= ENV["SECRET"] || "tina4-default-secret"
|
|
20
|
+
@handler = create_handler
|
|
21
|
+
@id = extract_session_id(env) || SecureRandom.hex(32)
|
|
22
|
+
@data = load_session
|
|
23
|
+
@modified = false
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def [](key)
|
|
27
|
+
@data[key.to_s]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def []=(key, value)
|
|
31
|
+
@data[key.to_s] = value
|
|
32
|
+
@modified = true
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def delete(key)
|
|
36
|
+
@data.delete(key.to_s)
|
|
37
|
+
@modified = true
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def clear
|
|
41
|
+
@data = {}
|
|
42
|
+
@modified = true
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def to_hash
|
|
46
|
+
@data.dup
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def save
|
|
50
|
+
return unless @modified
|
|
51
|
+
@handler.write(@id, @data)
|
|
52
|
+
@modified = false
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def destroy
|
|
56
|
+
@handler.destroy(@id)
|
|
57
|
+
@data = {}
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def cookie_header
|
|
61
|
+
"#{@options[:cookie_name]}=#{@id}; Path=/; HttpOnly; SameSite=Lax; Max-Age=#{@options[:max_age]}"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def extract_session_id(env)
|
|
67
|
+
cookie_str = env["HTTP_COOKIE"] || ""
|
|
68
|
+
cookie_str.split(";").each do |pair|
|
|
69
|
+
key, value = pair.strip.split("=", 2)
|
|
70
|
+
return value if key == @options[:cookie_name]
|
|
71
|
+
end
|
|
72
|
+
nil
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def load_session
|
|
76
|
+
existing = @handler.read(@id)
|
|
77
|
+
existing || {}
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def create_handler
|
|
81
|
+
case @options[:handler].to_sym
|
|
82
|
+
when :file
|
|
83
|
+
Tina4::SessionHandlers::FileHandler.new(@options[:handler_options])
|
|
84
|
+
when :redis
|
|
85
|
+
Tina4::SessionHandlers::RedisHandler.new(@options[:handler_options])
|
|
86
|
+
when :mongo, :mongodb
|
|
87
|
+
Tina4::SessionHandlers::MongoHandler.new(@options[:handler_options])
|
|
88
|
+
else
|
|
89
|
+
Tina4::SessionHandlers::FileHandler.new(@options[:handler_options])
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
class LazySession
|
|
95
|
+
def initialize(env, options = {})
|
|
96
|
+
@env = env
|
|
97
|
+
@options = options
|
|
98
|
+
@session = nil
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def [](key)
|
|
102
|
+
ensure_loaded
|
|
103
|
+
@session[key]
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def []=(key, value)
|
|
107
|
+
ensure_loaded
|
|
108
|
+
@session[key] = value
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def delete(key)
|
|
112
|
+
ensure_loaded
|
|
113
|
+
@session.delete(key)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def clear
|
|
117
|
+
ensure_loaded
|
|
118
|
+
@session.clear
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def save
|
|
122
|
+
@session&.save
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def destroy
|
|
126
|
+
@session&.destroy
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def cookie_header
|
|
130
|
+
ensure_loaded
|
|
131
|
+
@session.cookie_header
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def to_hash
|
|
135
|
+
ensure_loaded
|
|
136
|
+
@session.to_hash
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
private
|
|
140
|
+
|
|
141
|
+
def ensure_loaded
|
|
142
|
+
@session ||= Session.new(@env, @options)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|