kirei 0.2.1 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +74 -25
- data/bin/kirei +1 -1
- data/kirei.gemspec +5 -3
- data/lib/cli/commands/new_app/base_directories.rb +1 -1
- data/lib/cli/commands/new_app/execute.rb +3 -3
- data/lib/cli/commands/new_app/files/app.rb +16 -3
- data/lib/cli/commands/new_app/files/config_ru.rb +1 -1
- data/lib/cli/commands/new_app/files/db_rake.rb +50 -2
- data/lib/cli/commands/new_app/files/irbrc.rb +1 -1
- data/lib/cli/commands/new_app/files/rakefile.rb +1 -1
- data/lib/cli/commands/new_app/files/routes.rb +49 -12
- data/lib/cli/commands/new_app/files/sorbet_config.rb +1 -1
- data/lib/cli/commands/start.rb +1 -1
- data/lib/kirei/app.rb +76 -56
- data/lib/kirei/config.rb +4 -1
- data/lib/kirei/controller.rb +44 -0
- data/lib/kirei/errors/json_api_error.rb +25 -0
- data/lib/kirei/errors/json_api_error_source.rb +12 -0
- data/lib/kirei/logging/level.rb +33 -0
- data/lib/kirei/logging/logger.rb +198 -0
- data/lib/kirei/logging/metric.rb +40 -0
- data/lib/kirei/model/base_class_interface.rb +55 -0
- data/lib/kirei/{base_model.rb → model/class_methods.rb} +42 -108
- data/lib/kirei/model/human_id_generator.rb +40 -0
- data/lib/kirei/model.rb +52 -0
- data/lib/kirei/routing/base.rb +187 -0
- data/lib/kirei/routing/nilable_hooks_type.rb +10 -0
- data/lib/kirei/{middleware.rb → routing/rack_env_type.rb} +1 -10
- data/lib/kirei/routing/rack_response_type.rb +15 -0
- data/lib/kirei/routing/route.rb +13 -0
- data/lib/kirei/routing/router.rb +56 -0
- data/lib/kirei/routing/verb.rb +37 -0
- data/lib/kirei/services/result.rb +53 -0
- data/lib/kirei/services/runner.rb +47 -0
- data/lib/kirei/version.rb +1 -1
- data/lib/kirei.rb +31 -3
- data/sorbet/rbi/shims/base_model.rbi +1 -1
- data/sorbet/rbi/shims/ruby.rbi +15 -0
- metadata +55 -14
- data/lib/boot.rb +0 -23
- data/lib/kirei/app_base.rb +0 -72
- data/lib/kirei/base_controller.rb +0 -16
- data/lib/kirei/logger.rb +0 -196
- data/lib/kirei/router.rb +0 -61
@@ -0,0 +1,44 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Kirei
|
5
|
+
class Controller < Routing::Base
|
6
|
+
class << self
|
7
|
+
extend T::Sig
|
8
|
+
|
9
|
+
sig { returns(Routing::NilableHooksType) }
|
10
|
+
attr_reader :before_hooks, :after_hooks
|
11
|
+
end
|
12
|
+
|
13
|
+
extend T::Sig
|
14
|
+
|
15
|
+
#
|
16
|
+
# Statements to be executed before every action.
|
17
|
+
#
|
18
|
+
# In development mode, Rack Reloader might reload this file causing
|
19
|
+
# the before hooks to be executed multiple times.
|
20
|
+
#
|
21
|
+
sig do
|
22
|
+
params(
|
23
|
+
block: T.nilable(T.proc.void),
|
24
|
+
).void
|
25
|
+
end
|
26
|
+
def self.before(&block)
|
27
|
+
@before_hooks ||= T.let(Set.new, Routing::NilableHooksType)
|
28
|
+
@before_hooks.add(block) if block
|
29
|
+
end
|
30
|
+
|
31
|
+
#
|
32
|
+
# Statements to be executed after every action.
|
33
|
+
#
|
34
|
+
sig do
|
35
|
+
params(
|
36
|
+
block: T.nilable(T.proc.void),
|
37
|
+
).void
|
38
|
+
end
|
39
|
+
def self.after(&block)
|
40
|
+
@after_hooks ||= T.let(Set.new, Routing::NilableHooksType)
|
41
|
+
@after_hooks.add(block) if block
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Kirei
|
5
|
+
module Errors
|
6
|
+
#
|
7
|
+
# https://jsonapi.org/format/#errors
|
8
|
+
# Error objects MUST be returned as an array keyed by errors in the top level of a JSON:API document.
|
9
|
+
#
|
10
|
+
class JsonApiError < T::Struct
|
11
|
+
#
|
12
|
+
# An application-specific error code, expressed as a string value.
|
13
|
+
#
|
14
|
+
const :code, String
|
15
|
+
|
16
|
+
#
|
17
|
+
# A human-readable explanation specific to this occurrence of the problem.
|
18
|
+
# Like title, this field's value can be localized.
|
19
|
+
#
|
20
|
+
const :detail, T.nilable(String)
|
21
|
+
|
22
|
+
const :source, T.nilable(JsonApiErrorSource)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Kirei
|
5
|
+
module Logging
|
6
|
+
class Level < T::Enum
|
7
|
+
extend T::Sig
|
8
|
+
|
9
|
+
enums do
|
10
|
+
UNKNOWN = new(5) # An unknown message that should always be logged.
|
11
|
+
FATAL = new(4) # An unhandleable error that results in a program crash.
|
12
|
+
ERROR = new(3) # A handleable error condition.
|
13
|
+
WARN = new(2) # A warning.
|
14
|
+
INFO = new(1) # Generic (useful) information about system operation.
|
15
|
+
DEBUG = new(0) # Low-level information for developers.
|
16
|
+
end
|
17
|
+
|
18
|
+
sig { returns(String) }
|
19
|
+
def to_human
|
20
|
+
case self
|
21
|
+
when UNKNOWN then "UNKNOWN"
|
22
|
+
when FATAL then "FATAL"
|
23
|
+
when ERROR then "ERROR"
|
24
|
+
when WARN then "WARN"
|
25
|
+
when INFO then "INFO"
|
26
|
+
when DEBUG then "DEBUG"
|
27
|
+
else
|
28
|
+
T.absurd(self)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,198 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Kirei
|
5
|
+
# rubocop:disable Metrics
|
6
|
+
|
7
|
+
#
|
8
|
+
# Example Usage:
|
9
|
+
#
|
10
|
+
# Kirei::Logging::Logger.call(
|
11
|
+
# level: Kirei::Logging::Level::INFO,
|
12
|
+
# label: "Request started",
|
13
|
+
# meta: {
|
14
|
+
# key: "value",
|
15
|
+
# },
|
16
|
+
# )
|
17
|
+
#
|
18
|
+
# You can define a custom log transformer to transform the logline:
|
19
|
+
#
|
20
|
+
# Kirei::App.config.log_transformer = Proc.new { _1 }
|
21
|
+
#
|
22
|
+
# By default, "meta" is flattened, and sensitive values are masked using sane defaults that you
|
23
|
+
# can finetune via `Kirei::App.config.sensitive_keys`.
|
24
|
+
#
|
25
|
+
# You can also build on top of the provided log transformer:
|
26
|
+
#
|
27
|
+
# Kirei::App.config.log_transformer = Proc.new do |meta|
|
28
|
+
# flattened_meta = Kirei::Logging::Logger.flatten_hash_and_mask_sensitive_values(meta)
|
29
|
+
# # Do something with the flattened meta
|
30
|
+
# flattened_meta.map { _1.to_json }
|
31
|
+
# end
|
32
|
+
#
|
33
|
+
# NOTE:
|
34
|
+
# * The log transformer must return an array of strings to allow emitting multiple lines per log event.
|
35
|
+
# * Whenever possible, key names follow OpenTelemetry Semantic Conventions, https://opentelemetry.io/docs/concepts/semantic-conventions/
|
36
|
+
#
|
37
|
+
module Logging
|
38
|
+
class Logger
|
39
|
+
extend T::Sig
|
40
|
+
|
41
|
+
FILTERED = "[FILTERED]"
|
42
|
+
|
43
|
+
@instance = T.let(nil, T.nilable(Kirei::Logging::Logger))
|
44
|
+
|
45
|
+
sig { void }
|
46
|
+
def initialize
|
47
|
+
super
|
48
|
+
@queue = T.let(Thread::Queue.new, Thread::Queue)
|
49
|
+
@thread = T.let(start_logging_thread, Thread)
|
50
|
+
end
|
51
|
+
|
52
|
+
sig { returns(Kirei::Logging::Logger) }
|
53
|
+
def self.instance
|
54
|
+
@instance ||= new
|
55
|
+
end
|
56
|
+
|
57
|
+
sig { returns(::Logger) }
|
58
|
+
def self.logger
|
59
|
+
return @logger unless @logger.nil?
|
60
|
+
|
61
|
+
@logger = T.let(nil, T.nilable(::Logger))
|
62
|
+
@logger ||= ::Logger.new($stdout)
|
63
|
+
|
64
|
+
# we want the logline to be parseable to JSON
|
65
|
+
@logger.formatter = proc do |_severity, _datetime, _progname, msg|
|
66
|
+
"#{msg}\n"
|
67
|
+
end
|
68
|
+
|
69
|
+
@logger
|
70
|
+
end
|
71
|
+
|
72
|
+
sig do
|
73
|
+
params(
|
74
|
+
level: Logging::Level,
|
75
|
+
label: String,
|
76
|
+
meta: T::Hash[String, T.untyped],
|
77
|
+
).void
|
78
|
+
end
|
79
|
+
def self.call(level:, label:, meta: {})
|
80
|
+
return if ENV["NO_LOGS"] == "true"
|
81
|
+
return if level.serialize < App.config.log_level.serialize
|
82
|
+
|
83
|
+
# must extract data from current thread before passing this down to the logging thread
|
84
|
+
meta["enduser.id"] ||= Thread.current[:enduser_id] # OpenTelemetry::SemanticConventions::Trace::ENDUSER_ID
|
85
|
+
meta["service.instance.id"] ||= Thread.current[:request_id] # OpenTelemetry::SemanticConventions::Resource::SERVICE_INSTANCE_ID
|
86
|
+
|
87
|
+
instance.call(level: level, label: label, meta: meta)
|
88
|
+
end
|
89
|
+
|
90
|
+
sig do
|
91
|
+
params(
|
92
|
+
level: Logging::Level,
|
93
|
+
label: String,
|
94
|
+
meta: T::Hash[String, T.untyped],
|
95
|
+
).void
|
96
|
+
end
|
97
|
+
def call(level:, label:, meta: {})
|
98
|
+
Kirei::App.config.log_default_metadata.each_pair do |key, value|
|
99
|
+
meta[key] ||= value
|
100
|
+
end
|
101
|
+
|
102
|
+
meta["service.name"] ||= Kirei::App.config.app_name # OpenTelemetry::SemanticConventions::Resource::SERVICE_NAME
|
103
|
+
meta["service.version"] = Kirei::App.version # OpenTelemetry::SemanticConventions::Resource::SERVICE_VERSION
|
104
|
+
meta["timestamp"] ||= Time.now.utc.iso8601
|
105
|
+
meta["level"] ||= level.to_human
|
106
|
+
meta["label"] ||= label
|
107
|
+
|
108
|
+
@queue << meta
|
109
|
+
end
|
110
|
+
|
111
|
+
sig { returns(Thread) }
|
112
|
+
def start_logging_thread
|
113
|
+
Thread.new do
|
114
|
+
Kernel.loop do
|
115
|
+
log_data = T.let(@queue.pop, T::Hash[Symbol, T.untyped])
|
116
|
+
log_transformer = App.config.log_transformer
|
117
|
+
|
118
|
+
loglines = if log_transformer
|
119
|
+
log_transformer.call(log_data)
|
120
|
+
else
|
121
|
+
[Oj.dump(
|
122
|
+
Kirei::Logging::Logger.flatten_hash_and_mask_sensitive_values(log_data),
|
123
|
+
Kirei::OJ_OPTIONS,
|
124
|
+
)]
|
125
|
+
end
|
126
|
+
|
127
|
+
loglines.each do |logline|
|
128
|
+
logline = "\n#{logline}\n" if Kirei::App.environment == "development"
|
129
|
+
Kirei::Logging::Logger.logger.unknown(logline)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
# rubocop:disable Naming/MethodParameterName
|
136
|
+
sig do
|
137
|
+
params(
|
138
|
+
k: String,
|
139
|
+
v: String,
|
140
|
+
).returns(String)
|
141
|
+
end
|
142
|
+
def self.mask(k, v)
|
143
|
+
App.config.sensitive_keys.any? { k.match?(_1) } ? FILTERED : v
|
144
|
+
end
|
145
|
+
# rubocop:enable Naming/MethodParameterName
|
146
|
+
|
147
|
+
sig do
|
148
|
+
params(
|
149
|
+
hash: T::Hash[T.any(Symbol, String), T.untyped],
|
150
|
+
prefix: String,
|
151
|
+
).returns(T::Hash[String, T.untyped])
|
152
|
+
end
|
153
|
+
def self.flatten_hash_and_mask_sensitive_values(hash, prefix = "")
|
154
|
+
result = T.let({}, T::Hash[String, T.untyped])
|
155
|
+
Kirei::Helpers.deep_stringify_keys!(hash)
|
156
|
+
hash = T.cast(hash, T::Hash[String, T.untyped])
|
157
|
+
|
158
|
+
hash.each do |key, value|
|
159
|
+
new_prefix = Kirei::Helpers.blank?(prefix) ? key : "#{prefix}.#{key}"
|
160
|
+
|
161
|
+
case value
|
162
|
+
when Hash
|
163
|
+
# Some libraries have a custom Hash class that inhert from Hash, but act differently, e.g. OmniAuth::AuthHash.
|
164
|
+
# This results in `transform_keys` being available but without any effect.
|
165
|
+
value = value.to_h if value.class != Hash
|
166
|
+
result.merge!(flatten_hash_and_mask_sensitive_values(value.transform_keys(&:to_s), new_prefix))
|
167
|
+
when Array
|
168
|
+
value.each_with_index do |element, index|
|
169
|
+
if element.is_a?(Hash) || element.is_a?(Array)
|
170
|
+
result.merge!(flatten_hash_and_mask_sensitive_values({ index => element }, new_prefix))
|
171
|
+
else
|
172
|
+
result["#{new_prefix}.#{index}"] = element.is_a?(String) ? mask(key, element) : element
|
173
|
+
end
|
174
|
+
end
|
175
|
+
when String then result[new_prefix] = mask(key, value)
|
176
|
+
when Numeric, FalseClass, TrueClass, NilClass then result[new_prefix] = value
|
177
|
+
else
|
178
|
+
if value.respond_to?(:serialize)
|
179
|
+
serialized_value = value.serialize
|
180
|
+
if serialized_value.is_a?(Hash)
|
181
|
+
result.merge!(
|
182
|
+
flatten_hash_and_mask_sensitive_values(serialized_value.transform_keys(&:to_s), new_prefix),
|
183
|
+
)
|
184
|
+
else
|
185
|
+
result[new_prefix] = serialized_value&.to_s
|
186
|
+
end
|
187
|
+
else
|
188
|
+
result[new_prefix] = value&.to_s
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
result
|
194
|
+
end
|
195
|
+
end
|
196
|
+
# rubocop:enable Metrics
|
197
|
+
end
|
198
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Kirei
|
5
|
+
module Logging
|
6
|
+
class Metric
|
7
|
+
extend T::Sig
|
8
|
+
|
9
|
+
sig do
|
10
|
+
params(
|
11
|
+
metric_name: String,
|
12
|
+
value: Integer,
|
13
|
+
tags: T::Hash[String, T.untyped],
|
14
|
+
).void
|
15
|
+
end
|
16
|
+
def self.call(metric_name, value = 1, tags: {})
|
17
|
+
return if ENV["NO_METRICS"] == "true"
|
18
|
+
|
19
|
+
inject_defaults(tags)
|
20
|
+
|
21
|
+
# Do not `compact_blank` tags, since one might want to track empty strings/"false"/NULLs.
|
22
|
+
# NOT having any tag doesn't tell the user if the tag was empty or not set at all.
|
23
|
+
StatsD.increment(metric_name, value, tags: tags)
|
24
|
+
end
|
25
|
+
|
26
|
+
sig { params(tags: T::Hash[String, T.untyped]).returns(T::Hash[String, T.untyped]) }
|
27
|
+
def self.inject_defaults(tags)
|
28
|
+
App.config.metric_default_tags.each_pair do |key, default_value|
|
29
|
+
tags[key] ||= default_value
|
30
|
+
end
|
31
|
+
|
32
|
+
tags["enduser.id"] ||= Thread.current[:enduser_id]
|
33
|
+
tags["service.name"] ||= Kirei::App.config.app_name # OpenTelemetry::SemanticConventions::Resource::SERVICE_NAME
|
34
|
+
tags["service.version"] = Kirei::App.version # OpenTelemetry::SemanticConventions::Resource::SERVICE_VERSION
|
35
|
+
|
36
|
+
tags
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
# rubocop:disable Style/EmptyMethod
|
5
|
+
|
6
|
+
module Kirei
|
7
|
+
module Model
|
8
|
+
module BaseClassInterface
|
9
|
+
extend T::Sig
|
10
|
+
extend T::Helpers
|
11
|
+
interface!
|
12
|
+
|
13
|
+
sig { abstract.params(hash: T.untyped).returns(T.untyped) }
|
14
|
+
def find_by(hash); end
|
15
|
+
|
16
|
+
sig { abstract.params(hash: T.untyped).returns(T.untyped) }
|
17
|
+
def where(hash); end
|
18
|
+
|
19
|
+
sig { abstract.returns(T.untyped) }
|
20
|
+
def all; end
|
21
|
+
|
22
|
+
sig { abstract.params(hash: T.untyped).returns(T.untyped) }
|
23
|
+
def create(hash); end
|
24
|
+
|
25
|
+
sig { abstract.params(attributes: T.untyped).void }
|
26
|
+
def wrap_jsonb_non_primivitives!(attributes); end
|
27
|
+
|
28
|
+
sig { abstract.params(hash: T.untyped).returns(T.untyped) }
|
29
|
+
def resolve(hash); end
|
30
|
+
|
31
|
+
sig { abstract.params(hash: T.untyped).returns(T.untyped) }
|
32
|
+
def resolve_first(hash); end
|
33
|
+
|
34
|
+
sig { abstract.returns(T.untyped) }
|
35
|
+
def table_name; end
|
36
|
+
|
37
|
+
sig { abstract.returns(T.untyped) }
|
38
|
+
def query; end
|
39
|
+
|
40
|
+
sig { abstract.returns(T.untyped) }
|
41
|
+
def db; end
|
42
|
+
|
43
|
+
sig { abstract.returns(T.untyped) }
|
44
|
+
def human_id_length; end
|
45
|
+
|
46
|
+
sig { abstract.returns(T.untyped) }
|
47
|
+
def human_id_prefix; end
|
48
|
+
|
49
|
+
sig { abstract.returns(T.untyped) }
|
50
|
+
def generate_human_id; end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# rubocop:enable Style/EmptyMethod
|
@@ -2,120 +2,36 @@
|
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
4
|
module Kirei
|
5
|
-
module
|
6
|
-
extend T::Sig
|
7
|
-
extend T::Helpers
|
8
|
-
|
9
|
-
sig { returns(BaseClassInterface) }
|
10
|
-
def class; super; end # rubocop:disable all
|
11
|
-
|
12
|
-
# An update keeps the original object intact, and returns a new object with the updated values.
|
13
|
-
sig do
|
14
|
-
params(
|
15
|
-
hash: T::Hash[Symbol, T.untyped],
|
16
|
-
).returns(T.self_type)
|
17
|
-
end
|
18
|
-
def update(hash)
|
19
|
-
hash[:updated_at] = Time.now.utc if respond_to?(:updated_at) && hash[:updated_at].nil?
|
20
|
-
self.class.wrap_jsonb_non_primivitives!(hash)
|
21
|
-
self.class.db.where({ id: id }).update(hash)
|
22
|
-
self.class.find_by({ id: id })
|
23
|
-
end
|
24
|
-
|
25
|
-
# Delete keeps the original object intact. Returns true if the record was deleted.
|
26
|
-
# Calling delete multiple times will return false after the first (successful) call.
|
27
|
-
sig { returns(T::Boolean) }
|
28
|
-
def delete
|
29
|
-
count = self.class.db.where({ id: id }).delete
|
30
|
-
count == 1
|
31
|
-
end
|
32
|
-
|
33
|
-
# warning: this is not concurrency-safe
|
34
|
-
# save keeps the original object intact, and returns a new object with the updated values.
|
35
|
-
sig { returns(T.self_type) }
|
36
|
-
def save
|
37
|
-
previous_record = self.class.find_by({ id: id })
|
38
|
-
|
39
|
-
hash = serialize
|
40
|
-
Helpers.deep_symbolize_keys!(hash)
|
41
|
-
hash = T.cast(hash, T::Hash[Symbol, T.untyped])
|
42
|
-
|
43
|
-
if previous_record.nil?
|
44
|
-
self.class.create(hash)
|
45
|
-
else
|
46
|
-
update(hash)
|
47
|
-
end
|
48
|
-
end
|
49
|
-
|
50
|
-
module BaseClassInterface
|
51
|
-
extend T::Sig
|
52
|
-
extend T::Helpers
|
53
|
-
interface!
|
54
|
-
|
55
|
-
sig { abstract.params(hash: T.untyped).returns(T.untyped) }
|
56
|
-
def find_by(hash)
|
57
|
-
end
|
58
|
-
|
59
|
-
sig { abstract.params(hash: T.untyped).returns(T.untyped) }
|
60
|
-
def where(hash)
|
61
|
-
end
|
62
|
-
|
63
|
-
sig { abstract.returns(T.untyped) }
|
64
|
-
def all
|
65
|
-
end
|
66
|
-
|
67
|
-
sig { abstract.params(hash: T.untyped).returns(T.untyped) }
|
68
|
-
def create(hash)
|
69
|
-
end
|
70
|
-
|
71
|
-
sig { abstract.params(attributes: T.untyped).void }
|
72
|
-
def wrap_jsonb_non_primivitives!(attributes)
|
73
|
-
end
|
74
|
-
|
75
|
-
sig { abstract.params(hash: T.untyped).returns(T.untyped) }
|
76
|
-
def resolve(hash)
|
77
|
-
end
|
78
|
-
|
79
|
-
sig { abstract.params(hash: T.untyped).returns(T.untyped) }
|
80
|
-
def resolve_first(hash)
|
81
|
-
end
|
82
|
-
|
83
|
-
sig { abstract.returns(T.untyped) }
|
84
|
-
def table_name
|
85
|
-
end
|
86
|
-
|
87
|
-
sig { abstract.returns(T.untyped) }
|
88
|
-
def db
|
89
|
-
end
|
90
|
-
end
|
91
|
-
|
5
|
+
module Model
|
92
6
|
module ClassMethods
|
93
7
|
extend T::Sig
|
94
8
|
extend T::Generic
|
95
9
|
|
96
10
|
# the attached class is the class that extends this module
|
97
|
-
# e.g. "User"
|
98
|
-
# extend T::Generic
|
99
|
-
# has_attached_class!
|
11
|
+
# e.g. "User", "Airport", ..
|
100
12
|
has_attached_class!
|
101
13
|
|
102
|
-
include BaseClassInterface
|
14
|
+
include Kirei::Model::BaseClassInterface
|
103
15
|
|
104
16
|
# defaults to a pluralized, underscored version of the class name
|
105
17
|
sig { override.returns(String) }
|
106
18
|
def table_name
|
107
|
-
@table_name ||= T.let(
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
)
|
19
|
+
@table_name ||= T.let("#{model_name}s", T.nilable(String))
|
20
|
+
end
|
21
|
+
|
22
|
+
sig { returns(String) }
|
23
|
+
def model_name
|
24
|
+
Kirei::Helpers.underscore(T.must(name.split("::").last))
|
114
25
|
end
|
115
26
|
|
116
27
|
sig { override.returns(Sequel::Dataset) }
|
28
|
+
def query
|
29
|
+
db[table_name.to_sym]
|
30
|
+
end
|
31
|
+
|
32
|
+
sig { override.returns(Sequel::Database) }
|
117
33
|
def db
|
118
|
-
|
34
|
+
App.raw_db_connection
|
119
35
|
end
|
120
36
|
|
121
37
|
sig do
|
@@ -124,12 +40,12 @@ module Kirei
|
|
124
40
|
).returns(T::Array[T.attached_class])
|
125
41
|
end
|
126
42
|
def where(hash)
|
127
|
-
resolve(
|
43
|
+
resolve(query.where(hash))
|
128
44
|
end
|
129
45
|
|
130
46
|
sig { override.returns(T::Array[T.attached_class]) }
|
131
47
|
def all
|
132
|
-
resolve(
|
48
|
+
resolve(query.all)
|
133
49
|
end
|
134
50
|
|
135
51
|
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
@@ -156,7 +72,7 @@ module Kirei
|
|
156
72
|
all_attributes["updated_at"] = Time.now.utc
|
157
73
|
end
|
158
74
|
|
159
|
-
pkey = T.let(
|
75
|
+
pkey = T.let(query.insert(all_attributes), String)
|
160
76
|
|
161
77
|
T.must(find_by({ id: pkey }))
|
162
78
|
end
|
@@ -166,7 +82,7 @@ module Kirei
|
|
166
82
|
def wrap_jsonb_non_primivitives!(attributes)
|
167
83
|
# setting `@raw_db_connection.wrap_json_primitives = true`
|
168
84
|
# only works on JSON primitives, but not on blank hashes/arrays
|
169
|
-
return unless
|
85
|
+
return unless App.config.db_extensions.include?(:pg_json)
|
170
86
|
|
171
87
|
attributes.each_pair do |key, value|
|
172
88
|
next unless value.is_a?(Hash) || value.is_a?(Array)
|
@@ -181,7 +97,7 @@ module Kirei
|
|
181
97
|
).returns(T.nilable(T.attached_class))
|
182
98
|
end
|
183
99
|
def find_by(hash)
|
184
|
-
resolve_first(
|
100
|
+
resolve_first(query.where(hash))
|
185
101
|
end
|
186
102
|
|
187
103
|
# Extra or unknown properties present in the Hash do not raise exceptions at
|
@@ -196,7 +112,7 @@ module Kirei
|
|
196
112
|
).returns(T::Array[T.attached_class])
|
197
113
|
end
|
198
114
|
def resolve(query, strict = nil)
|
199
|
-
strict_loading = strict.nil? ?
|
115
|
+
strict_loading = strict.nil? ? App.config.db_strict_type_resolving : strict
|
200
116
|
|
201
117
|
query.map do |row|
|
202
118
|
row = T.cast(row, T::Hash[Symbol, T.untyped])
|
@@ -212,12 +128,30 @@ module Kirei
|
|
212
128
|
).returns(T.nilable(T.attached_class))
|
213
129
|
end
|
214
130
|
def resolve_first(query, strict = nil)
|
215
|
-
strict_loading = strict.nil? ?
|
131
|
+
strict_loading = strict.nil? ? App.config.db_strict_type_resolving : strict
|
216
132
|
|
217
133
|
resolve(query.limit(1), strict_loading).first
|
218
134
|
end
|
219
|
-
end
|
220
135
|
|
221
|
-
|
136
|
+
# defaults to 6
|
137
|
+
sig { override.returns(Integer) }
|
138
|
+
def human_id_length = 6
|
139
|
+
|
140
|
+
# defaults to "model_name" (table_name without the trailing "s")
|
141
|
+
sig { override.returns(String) }
|
142
|
+
def human_id_prefix = model_name
|
143
|
+
|
144
|
+
#
|
145
|
+
# Generates a human-readable ID for the record.
|
146
|
+
# The ID is prefixed with the table name and an underscore.
|
147
|
+
#
|
148
|
+
sig { override.returns(String) }
|
149
|
+
def generate_human_id
|
150
|
+
Kirei::Model::HumanIdGenerator.call(
|
151
|
+
length: human_id_length,
|
152
|
+
prefix: human_id_prefix,
|
153
|
+
)
|
154
|
+
end
|
155
|
+
end
|
222
156
|
end
|
223
157
|
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Kirei
|
5
|
+
module Model
|
6
|
+
class HumanIdGenerator
|
7
|
+
extend T::Sig
|
8
|
+
|
9
|
+
# Removed ambiguous characters 0, 1, O, I, l, 5, S
|
10
|
+
ALLOWED_CHARS = "2346789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrtuvwxyz"
|
11
|
+
private_constant :ALLOWED_CHARS
|
12
|
+
|
13
|
+
ALLOWED_CHARS_COUNT = T.let(ALLOWED_CHARS.size, Integer)
|
14
|
+
private_constant :ALLOWED_CHARS_COUNT
|
15
|
+
|
16
|
+
sig do
|
17
|
+
params(
|
18
|
+
length: Integer,
|
19
|
+
prefix: String,
|
20
|
+
).returns(String)
|
21
|
+
end
|
22
|
+
def self.call(length:, prefix:)
|
23
|
+
"#{prefix}_#{random_id(length)}"
|
24
|
+
end
|
25
|
+
|
26
|
+
sig { params(key_length: Integer).returns(String) }
|
27
|
+
private_class_method def self.random_id(key_length)
|
28
|
+
random_chars = Array.new(key_length)
|
29
|
+
|
30
|
+
key_length.times do |idx|
|
31
|
+
random_char_idx = SecureRandom.random_number(ALLOWED_CHARS_COUNT)
|
32
|
+
random_char = T.must(ALLOWED_CHARS[random_char_idx])
|
33
|
+
random_chars[idx] = random_char
|
34
|
+
end
|
35
|
+
|
36
|
+
random_chars.join
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|