yescode 1.0.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 +23 -0
- data/Dockerfile +46 -0
- data/Gemfile +7 -0
- data/LICENSE +21 -0
- data/README.md +73 -0
- data/exe/yescode +16 -0
- data/lib/yes_app.rb +114 -0
- data/lib/yes_controller.rb +85 -0
- data/lib/yes_logger.rb +8 -0
- data/lib/yes_mail.rb +88 -0
- data/lib/yes_rack_logger.rb +59 -0
- data/lib/yes_record.rb +283 -0
- data/lib/yes_routes.rb +64 -0
- data/lib/yes_static.rb +25 -0
- data/lib/yes_view.rb +99 -0
- data/lib/yescode/asset.rb +24 -0
- data/lib/yescode/asset_compiler.rb +33 -0
- data/lib/yescode/assets.rb +30 -0
- data/lib/yescode/database.rb +136 -0
- data/lib/yescode/emote.rb +85 -0
- data/lib/yescode/env.rb +23 -0
- data/lib/yescode/generator.rb +487 -0
- data/lib/yescode/logfmt_formatter.rb +30 -0
- data/lib/yescode/queries.rb +27 -0
- data/lib/yescode/refinements.rb +19 -0
- data/lib/yescode/request_cache/middleware.rb +29 -0
- data/lib/yescode/request_cache.rb +56 -0
- data/lib/yescode/router.rb +78 -0
- data/lib/yescode/strings.rb +23 -0
- data/lib/yescode.rb +48 -0
- data/yescode.gemspec +41 -0
- metadata +161 -0
data/lib/yes_record.rb
ADDED
@@ -0,0 +1,283 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class YesRecord
|
4
|
+
using Refinements
|
5
|
+
|
6
|
+
class QueryNotFound < StandardError; end
|
7
|
+
class RecordNotFound < StandardError; end
|
8
|
+
Column = Struct.new("Column", :name, :type, :primary_key)
|
9
|
+
Query = Struct.new("Query", :name, :sql, :statement)
|
10
|
+
|
11
|
+
class << self
|
12
|
+
attr_writer :table_name
|
13
|
+
|
14
|
+
def table_name
|
15
|
+
@table_name || to_s.snake_case
|
16
|
+
end
|
17
|
+
|
18
|
+
def table(name)
|
19
|
+
@table_name = name
|
20
|
+
end
|
21
|
+
|
22
|
+
def schema
|
23
|
+
@schema ||= Yescode::Database.schema[table_name]
|
24
|
+
end
|
25
|
+
|
26
|
+
def columns
|
27
|
+
@columns ||= schema.map do |r|
|
28
|
+
Column.new(
|
29
|
+
r["columnName"],
|
30
|
+
r["columnType"],
|
31
|
+
r["pk"] == 1
|
32
|
+
)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def column_names
|
37
|
+
@column_names ||= columns.map(&:name)
|
38
|
+
end
|
39
|
+
|
40
|
+
def primary_key_column
|
41
|
+
@primary_key_column ||= columns.find(&:primary_key).first
|
42
|
+
end
|
43
|
+
|
44
|
+
def inspect
|
45
|
+
attr_str = columns.map { |c| " #{c.name} => #{c.type}" }.join("\n")
|
46
|
+
|
47
|
+
"#{self} {\n#{attr_str}\n}"
|
48
|
+
end
|
49
|
+
|
50
|
+
def connect
|
51
|
+
column_names.each do |column|
|
52
|
+
attr_accessor column.to_sym
|
53
|
+
end
|
54
|
+
|
55
|
+
self
|
56
|
+
end
|
57
|
+
|
58
|
+
def queries(filename)
|
59
|
+
@query_filename = filename
|
60
|
+
filepath = File.join(".", "app", "models", filename)
|
61
|
+
queries = Yescode::Queries.queries(filepath)
|
62
|
+
@queries = {}
|
63
|
+
queries.each do |name, sql|
|
64
|
+
@queries[name] = Query.new(name, sql)
|
65
|
+
end
|
66
|
+
|
67
|
+
@queries
|
68
|
+
end
|
69
|
+
|
70
|
+
def request_cache
|
71
|
+
Yescode::RequestCache.store[table_name] ||= {}
|
72
|
+
end
|
73
|
+
|
74
|
+
def clear_request_cache!
|
75
|
+
Yescode::RequestCache.store[table_name] = nil
|
76
|
+
end
|
77
|
+
|
78
|
+
def execute_query(name, *params)
|
79
|
+
query = @queries[name]
|
80
|
+
key = params
|
81
|
+
request_cache[name] ||= { key => nil }
|
82
|
+
|
83
|
+
raise QueryNotFound, "Can't find a query in #{@query_filename} with name #{name}" unless query
|
84
|
+
|
85
|
+
query.statement ||= Yescode::Database.connection.prepare(query.sql)
|
86
|
+
Yescode::Database.logger&.debug(cached: !request_cache.dig(name, key).nil?, sql: query.sql, params:)
|
87
|
+
|
88
|
+
request_cache[name][key] ||= query.statement.execute(*params).to_a
|
89
|
+
end
|
90
|
+
|
91
|
+
def first(name, *params)
|
92
|
+
row = execute_query(name, *params).first
|
93
|
+
|
94
|
+
new(row) if row
|
95
|
+
end
|
96
|
+
|
97
|
+
def first!(name, *params)
|
98
|
+
row = execute_query(name, *params).first
|
99
|
+
|
100
|
+
raise RecordNotFound unless row
|
101
|
+
|
102
|
+
new(row) if row
|
103
|
+
end
|
104
|
+
|
105
|
+
def select(name, *params)
|
106
|
+
rows = execute_query(name, *params)
|
107
|
+
|
108
|
+
rows.map { |r| new(r) }
|
109
|
+
end
|
110
|
+
|
111
|
+
def all(name = :all, *params)
|
112
|
+
select(name, *params)
|
113
|
+
end
|
114
|
+
|
115
|
+
def value(name, *params)
|
116
|
+
execute_query(name, *params).first&.values&.first
|
117
|
+
end
|
118
|
+
|
119
|
+
def count(name = :count, *params)
|
120
|
+
value(name, *params)
|
121
|
+
end
|
122
|
+
|
123
|
+
def values(keys)
|
124
|
+
if keys.empty?
|
125
|
+
"default values"
|
126
|
+
else
|
127
|
+
"values (#{keys.map(&:inspect).join(', ')})"
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def insert_sql(keys)
|
132
|
+
"insert into #{table_name} (#{keys.join(', ')}) #{values(keys)} returning *"
|
133
|
+
end
|
134
|
+
|
135
|
+
def insert(params)
|
136
|
+
params.transform_keys!(&:to_sym)
|
137
|
+
params[:created_at] ||= Time.now.to_i if column_names.include?("created_at")
|
138
|
+
sql = insert_sql(params.keys)
|
139
|
+
inserted = Yescode::Database.get_first_row(sql, params)
|
140
|
+
|
141
|
+
clear_request_cache!
|
142
|
+
|
143
|
+
new(inserted)
|
144
|
+
rescue SQLite3::ConstraintException => e
|
145
|
+
Yescode::Database.logger.error(msg: e.message)
|
146
|
+
|
147
|
+
record = new(params)
|
148
|
+
record.rescue_constraint_error(e)
|
149
|
+
|
150
|
+
record
|
151
|
+
end
|
152
|
+
|
153
|
+
def insert_all
|
154
|
+
# TODO: do it
|
155
|
+
end
|
156
|
+
|
157
|
+
def update_all
|
158
|
+
# TODO: do it
|
159
|
+
end
|
160
|
+
|
161
|
+
def delete_all
|
162
|
+
result = Yescode::Database.execute "delete from #{table_name}"
|
163
|
+
clear_request_cache!
|
164
|
+
|
165
|
+
result
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
attr_accessor :errors
|
170
|
+
|
171
|
+
def initialize(args = {})
|
172
|
+
self.class.connect
|
173
|
+
load(args)
|
174
|
+
end
|
175
|
+
|
176
|
+
def load(args = {})
|
177
|
+
args.each do |k, v|
|
178
|
+
self.class.attr_accessor(k.to_sym) unless respond_to?(k.to_sym)
|
179
|
+
public_send("#{k}=", v)
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
def pk_column
|
184
|
+
self.class.primary_key_column.to_sym
|
185
|
+
end
|
186
|
+
|
187
|
+
def pk
|
188
|
+
to_h[pk_column]
|
189
|
+
end
|
190
|
+
|
191
|
+
def pk_param
|
192
|
+
{ pk_column => pk }
|
193
|
+
end
|
194
|
+
|
195
|
+
def update_params(params)
|
196
|
+
params.transform_keys!(&:to_sym)
|
197
|
+
params[:updated_at] ||= Time.now.to_i if self.class.column_names.include?("updated_at")
|
198
|
+
|
199
|
+
params
|
200
|
+
end
|
201
|
+
|
202
|
+
def update_sql(keys)
|
203
|
+
set_clause = keys.map { |k| "#{k} = :#{k}" }.join(", ")
|
204
|
+
|
205
|
+
"update #{self.class.table_name} set #{set_clause} where #{pk_column} = :#{pk_column} returning *"
|
206
|
+
end
|
207
|
+
|
208
|
+
def update(params)
|
209
|
+
update_params = update_params(params)
|
210
|
+
sql = update_sql(update_params.keys)
|
211
|
+
update_params.merge!(pk_param)
|
212
|
+
updated = Yescode::Database.get_first_row(sql, update_params)
|
213
|
+
self.class.clear_request_cache!
|
214
|
+
load(updated)
|
215
|
+
|
216
|
+
true
|
217
|
+
rescue SQLite3::ConstraintException => e
|
218
|
+
Yescode::Database.logger.error(msg: e.message)
|
219
|
+
rescue_constraint_error(e)
|
220
|
+
|
221
|
+
false
|
222
|
+
end
|
223
|
+
|
224
|
+
def delete
|
225
|
+
sql = "delete from #{self.class.table_name} where #{pk_column} = ?"
|
226
|
+
Yescode::Database.execute(sql, pk)
|
227
|
+
self.class.clear_request_cache!
|
228
|
+
|
229
|
+
true
|
230
|
+
end
|
231
|
+
|
232
|
+
def check?(constraint_name)
|
233
|
+
return false unless @errors
|
234
|
+
|
235
|
+
@errors[:check]&.include?(constraint_name.to_s)
|
236
|
+
end
|
237
|
+
|
238
|
+
def null?(column_name)
|
239
|
+
return false unless @errors
|
240
|
+
|
241
|
+
@errors[:null]&.include?(column_name.to_s)
|
242
|
+
end
|
243
|
+
|
244
|
+
def duplicate?(column_name)
|
245
|
+
return false unless @errors
|
246
|
+
|
247
|
+
@errors[:unique]&.include?(column_name.to_s)
|
248
|
+
end
|
249
|
+
|
250
|
+
def error?(name)
|
251
|
+
check?(name) || null?(name) || duplicate?(name)
|
252
|
+
end
|
253
|
+
|
254
|
+
def errors?
|
255
|
+
@errors.any?
|
256
|
+
end
|
257
|
+
|
258
|
+
def rescue_constraint_error(error)
|
259
|
+
message = error.message
|
260
|
+
name = message.gsub(/(CHECK|NOT NULL|UNIQUE) constraint failed: (\w+\.)?/, '')
|
261
|
+
@errors ||= { check: [], null: [], unique: [] }
|
262
|
+
|
263
|
+
if message.start_with?("CHECK")
|
264
|
+
@errors[:check] << name
|
265
|
+
elsif message.start_with?("NOT NULL")
|
266
|
+
@errors[:null] << name
|
267
|
+
elsif message.start_with?("UNIQUE")
|
268
|
+
@errors[:unique] << name
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
def to_h
|
273
|
+
@to_h ||= self.class.column_names.map { |c| [c.to_sym, public_send(c)] }.to_h
|
274
|
+
end
|
275
|
+
|
276
|
+
def saved?
|
277
|
+
!pk.nil?
|
278
|
+
end
|
279
|
+
|
280
|
+
def to_param
|
281
|
+
to_h.slice(self.class.primary_key_column.to_sym)
|
282
|
+
end
|
283
|
+
end
|
data/lib/yes_routes.rb
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class YesRoutes
|
4
|
+
using Refinements
|
5
|
+
class RouteDoesNotExistError < StandardError; end
|
6
|
+
class RouteParamsNilError < StandardError; end
|
7
|
+
|
8
|
+
class << self
|
9
|
+
attr_accessor :routes, :paths
|
10
|
+
|
11
|
+
def match(verb, path_string, class_name, method_name)
|
12
|
+
@paths ||= {}
|
13
|
+
@paths[[class_name, method_name]] = path_string
|
14
|
+
|
15
|
+
@routes ||= Hash.new { |hash, key| hash[key] = [] }
|
16
|
+
@routes[verb] << [path_string, class_name, method_name]
|
17
|
+
end
|
18
|
+
|
19
|
+
def path(class_name, method_name, params = {})
|
20
|
+
path_string = paths[[class_name, method_name]]
|
21
|
+
|
22
|
+
raise RouteParamsNilError, "Route params for :#{class_name}, :#{method_name} cannot be nil" if params.nil?
|
23
|
+
|
24
|
+
params = params.to_param if params.is_a?(YesRecord)
|
25
|
+
|
26
|
+
raise RouteDoesNotExistError, "Route for class #{class_name} and method #{method_name} doesn't exist" unless path_string
|
27
|
+
|
28
|
+
sub_params_in_path(path_string, params)
|
29
|
+
end
|
30
|
+
|
31
|
+
def get(path_string, class_name, method_name)
|
32
|
+
match("GET", path_string, class_name, method_name)
|
33
|
+
end
|
34
|
+
|
35
|
+
def post(path_string, class_name, method_name)
|
36
|
+
match("POST", path_string, class_name, method_name)
|
37
|
+
end
|
38
|
+
|
39
|
+
def resource(path_string, class_name)
|
40
|
+
snake_case = class_name.snake_case
|
41
|
+
[
|
42
|
+
[:get, "index", ""],
|
43
|
+
[:get, "new", "/new"],
|
44
|
+
[:post, "create", "/new"],
|
45
|
+
[:get, "show", "/:#{snake_case}"],
|
46
|
+
[:get, "edit", "/:#{snake_case}/edit"],
|
47
|
+
[:post, "update", "/:#{snake_case}/edit"],
|
48
|
+
[:post, "delete", "/:#{snake_case}/edit"]
|
49
|
+
].each do |method, method_name, suffix|
|
50
|
+
public_send(method, [path_string, suffix].join("/").gsub(%r{/+}, "/"), class_name, method_name)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def sub_params_in_path(path, params = {})
|
57
|
+
params.transform_keys!(&:inspect)
|
58
|
+
|
59
|
+
re = Regexp.union(params.keys)
|
60
|
+
|
61
|
+
path.gsub(re, params)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
data/lib/yes_static.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class YesStatic
|
4
|
+
def initialize(app, options = {})
|
5
|
+
@app = app
|
6
|
+
@root = options[:root] || Dir.pwd
|
7
|
+
end
|
8
|
+
|
9
|
+
def call(env)
|
10
|
+
ext = File.extname(env[Rack::PATH_INFO])
|
11
|
+
|
12
|
+
return @app.call(env) if ext.empty?
|
13
|
+
|
14
|
+
filepath = File.join(@root, env[Rack::PATH_INFO])
|
15
|
+
|
16
|
+
if File.exist?(filepath)
|
17
|
+
last_modified = ::File.mtime(filepath).httpdate
|
18
|
+
return [304, {}, []] if env["HTTP_IF_MODIFIED_SINCE"] == last_modified
|
19
|
+
|
20
|
+
[200, { "content-type" => Rack::Mime.mime_type(ext), "last-modified" => last_modified }, File.open(filepath)]
|
21
|
+
else
|
22
|
+
[404, {}, []]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/lib/yes_view.rb
ADDED
@@ -0,0 +1,99 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class YesView
|
4
|
+
using Refinements
|
5
|
+
include Emote::Helpers
|
6
|
+
|
7
|
+
class << self
|
8
|
+
attr_writer :template_path, :layout, :template_name
|
9
|
+
attr_accessor :paths, :logger
|
10
|
+
|
11
|
+
def template_path
|
12
|
+
@template_path || File.join(".", "app", "views")
|
13
|
+
end
|
14
|
+
|
15
|
+
def layout
|
16
|
+
@layout || "layout"
|
17
|
+
end
|
18
|
+
|
19
|
+
def template
|
20
|
+
File.join(template_path, template_name)
|
21
|
+
end
|
22
|
+
|
23
|
+
def view(template_name)
|
24
|
+
@template_name = template_name
|
25
|
+
end
|
26
|
+
|
27
|
+
def template_name
|
28
|
+
@template_name || "#{to_s.snake_case}.emote"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
attr_accessor :csrf_name, :csrf_value, :assets, :session, :ajax
|
33
|
+
|
34
|
+
def template
|
35
|
+
self.class.template
|
36
|
+
end
|
37
|
+
|
38
|
+
def render(tmpl, params = {})
|
39
|
+
case tmpl
|
40
|
+
when YesView
|
41
|
+
tmpl.csrf_name = csrf_name
|
42
|
+
tmpl.csrf_value = csrf_value
|
43
|
+
tmpl.session = session
|
44
|
+
tmpl.assets = assets
|
45
|
+
tmpl.ajax = ajax
|
46
|
+
emote(tmpl.template, params, tmpl)
|
47
|
+
when String
|
48
|
+
emote(tmpl, params)
|
49
|
+
else
|
50
|
+
raise StandardError, "Unsupported template type passed to render"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def csrf_field
|
55
|
+
"<input type=\"hidden\" name=\"#{csrf_name}\" value=\"#{csrf_value}\" />"
|
56
|
+
end
|
57
|
+
|
58
|
+
def form(params = {})
|
59
|
+
params = { method: "post" }.merge(params)
|
60
|
+
attr_string = params.map { |k, v| "#{k}=\"#{v}\"" }.join(" ")
|
61
|
+
|
62
|
+
<<~HTML
|
63
|
+
<form #{attr_string}>
|
64
|
+
#{csrf_field}
|
65
|
+
HTML
|
66
|
+
end
|
67
|
+
|
68
|
+
def _form
|
69
|
+
"</form>"
|
70
|
+
end
|
71
|
+
|
72
|
+
def form_link(str, controller_name, method_name, params = {})
|
73
|
+
<<~HTML
|
74
|
+
#{form(action: path(controller_name, method_name, params))}
|
75
|
+
<input type="submit" value="#{str}" />
|
76
|
+
#{_form}
|
77
|
+
HTML
|
78
|
+
end
|
79
|
+
|
80
|
+
def css
|
81
|
+
return [] unless assets
|
82
|
+
|
83
|
+
assets["css"].map { |filename| "/css/#{filename}" }
|
84
|
+
end
|
85
|
+
|
86
|
+
def js
|
87
|
+
return [] unless assets
|
88
|
+
|
89
|
+
assets["js"].map { |filename| "/js/#{filename}" }
|
90
|
+
end
|
91
|
+
|
92
|
+
def path(class_name, method_name, params = {})
|
93
|
+
Object.const_get(:App).path(class_name, method_name, params)
|
94
|
+
end
|
95
|
+
|
96
|
+
def fetch(class_name, method_name, params = {})
|
97
|
+
"<div data-href=\"#{path(class_name, method_name, params)}\"></div>"
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Yescode
|
4
|
+
class Asset
|
5
|
+
attr_reader :path
|
6
|
+
|
7
|
+
def initialize(filename, path:)
|
8
|
+
@filename = filename
|
9
|
+
@path = path
|
10
|
+
end
|
11
|
+
|
12
|
+
def full_path
|
13
|
+
File.join(path, @filename)
|
14
|
+
end
|
15
|
+
|
16
|
+
def content
|
17
|
+
File.read(full_path)
|
18
|
+
end
|
19
|
+
|
20
|
+
def ext
|
21
|
+
File.extname(@filename)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Yescode
|
4
|
+
class AssetCompiler
|
5
|
+
class << self
|
6
|
+
def compile(assets, ext)
|
7
|
+
# read contents of each file into array of strings
|
8
|
+
combined = StringIO.open do |s|
|
9
|
+
assets[ext].each do |filename|
|
10
|
+
source = File.join(".", "app", ext, filename)
|
11
|
+
s.puts File.read(source)
|
12
|
+
end
|
13
|
+
|
14
|
+
s.string
|
15
|
+
end
|
16
|
+
|
17
|
+
# hash the contents of each concatenated asset
|
18
|
+
hash = Digest::SHA1.hexdigest combined
|
19
|
+
|
20
|
+
# use hash to bust the cache
|
21
|
+
name = "#{hash}.#{ext}"
|
22
|
+
filename = File.join(".", "public", ext, name)
|
23
|
+
FileUtils.mkdir_p(File.join(".", "public", ext))
|
24
|
+
|
25
|
+
# output asset path for browser
|
26
|
+
# returns string of written filename
|
27
|
+
File.write(filename, combined)
|
28
|
+
|
29
|
+
name
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Yescode
|
4
|
+
class Assets
|
5
|
+
def initialize
|
6
|
+
@assets_path = File.join(".", "app")
|
7
|
+
@assets = Hash.new { |hash, key| hash[key] = [] }
|
8
|
+
end
|
9
|
+
|
10
|
+
def css(filenames = [])
|
11
|
+
@assets["css"] = filenames
|
12
|
+
end
|
13
|
+
|
14
|
+
def js(filenames = [])
|
15
|
+
@assets["js"] = filenames
|
16
|
+
end
|
17
|
+
|
18
|
+
def compile_assets
|
19
|
+
css = AssetCompiler.compile(@assets, "css") unless @assets["css"].empty?
|
20
|
+
js = AssetCompiler.compile(@assets, "js") unless @assets["js"].empty?
|
21
|
+
|
22
|
+
@assets["css"] = [css]
|
23
|
+
@assets["js"] = [js]
|
24
|
+
end
|
25
|
+
|
26
|
+
def to_h
|
27
|
+
@assets
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Yescode
|
4
|
+
class Database
|
5
|
+
class << self
|
6
|
+
attr_writer :connection_string
|
7
|
+
attr_accessor :logger
|
8
|
+
|
9
|
+
def connection_string
|
10
|
+
@connection_string || ENV["DATABASE_URL"]
|
11
|
+
end
|
12
|
+
|
13
|
+
def connection
|
14
|
+
@connection ||= SQLite3::Database.new(connection_string, { results_as_hash: true })
|
15
|
+
end
|
16
|
+
|
17
|
+
def version(filename)
|
18
|
+
File.basename(filename).split("_").first.to_i
|
19
|
+
end
|
20
|
+
|
21
|
+
def migrate(filenames)
|
22
|
+
execute "create table if not exists yescode_migrations ( version integer primary key )"
|
23
|
+
rows = execute("select version from yescode_migrations").map { |r| r["version"] }
|
24
|
+
file_versions = filenames.map { |f| version(f) }
|
25
|
+
|
26
|
+
return if (file_versions - rows).empty?
|
27
|
+
|
28
|
+
transaction do
|
29
|
+
filenames.each do |filename|
|
30
|
+
version = version(filename)
|
31
|
+
next if rows.include?(version)
|
32
|
+
|
33
|
+
queries = Queries.queries(filename)
|
34
|
+
execute(queries.find { |name, _| name == :up }[1])
|
35
|
+
execute "insert into yescode_migrations (version) values (?)", version
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def rollback_schema(filenames, step: 1)
|
41
|
+
execute "create table if not exists yescode_migrations ( version integer primary key )"
|
42
|
+
rows = execute("select version from yescode_migrations order by version desc limit #{step}").map { |r| r["version"] }
|
43
|
+
|
44
|
+
transaction do
|
45
|
+
rows.each do |row|
|
46
|
+
filename = filenames.find { |f| version(f) == row }
|
47
|
+
version = version(filename)
|
48
|
+
next unless rows.include?(version)
|
49
|
+
|
50
|
+
queries = Queries.queries(filename)
|
51
|
+
execute(queries.find { |name, _| name == :down }[1])
|
52
|
+
execute "delete from yescode_migrations where version = ?", version
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def transaction(mode = :deferred)
|
58
|
+
return yield(connection) if connection.transaction_active?
|
59
|
+
|
60
|
+
execute "begin #{mode} transaction"
|
61
|
+
@abort = false
|
62
|
+
yield self
|
63
|
+
|
64
|
+
true
|
65
|
+
rescue StandardError
|
66
|
+
@abort = true
|
67
|
+
raise
|
68
|
+
ensure
|
69
|
+
if @abort
|
70
|
+
execute "rollback transaction"
|
71
|
+
else
|
72
|
+
execute "commit transaction"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def rollback
|
77
|
+
@abort = true
|
78
|
+
end
|
79
|
+
|
80
|
+
def execute(sql, params = nil)
|
81
|
+
logger&.debug(sql:, params:)
|
82
|
+
|
83
|
+
connection.execute sql, params || []
|
84
|
+
end
|
85
|
+
|
86
|
+
def get_first_value(sql, params = nil)
|
87
|
+
logger&.debug(sql:, params:)
|
88
|
+
|
89
|
+
connection.get_first_value sql, params || []
|
90
|
+
end
|
91
|
+
|
92
|
+
def get_first_row(sql, params = nil)
|
93
|
+
logger&.debug(sql:, params:)
|
94
|
+
|
95
|
+
connection.get_first_row sql, params || []
|
96
|
+
end
|
97
|
+
|
98
|
+
def schema
|
99
|
+
return @schema if @schema
|
100
|
+
|
101
|
+
sql = <<-SQL
|
102
|
+
select
|
103
|
+
m.name as tableName,
|
104
|
+
pti.name as columnName,
|
105
|
+
pti.type as columnType,
|
106
|
+
m.sql as sql,
|
107
|
+
pti.pk as pk
|
108
|
+
from
|
109
|
+
sqlite_master m
|
110
|
+
left outer join
|
111
|
+
pragma_table_info(m.name) pti on pti.name <> m.name
|
112
|
+
where
|
113
|
+
m.type == 'table'
|
114
|
+
order by
|
115
|
+
tableName,
|
116
|
+
columnName
|
117
|
+
SQL
|
118
|
+
|
119
|
+
rows = Database.execute sql
|
120
|
+
|
121
|
+
@schema = rows.group_by do |r|
|
122
|
+
r["tableName"]
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
if connection_string && connection
|
128
|
+
execute "PRAGMA journal_model = WAL"
|
129
|
+
execute "PRAGMA foreign_keys = ON"
|
130
|
+
execute "PRAGMA busy_timeout = 5000"
|
131
|
+
execute "PRAGMA synchronous = NORMAL"
|
132
|
+
execute "PRAGMA wal_autocheckpoint = 0"
|
133
|
+
schema
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|