Skip to content

Commit

Permalink
Adds BaseModel #create #save (#3)
Browse files Browse the repository at this point in the history
  • Loading branch information
swiknaba authored Dec 31, 2023
1 parent d654dcb commit 06201a8
Show file tree
Hide file tree
Showing 7 changed files with 104 additions and 6 deletions.
3 changes: 3 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,8 @@ Style/StringLiteralsInInterpolation:
Style/FrozenStringLiteralComment:
Enabled: false

Style/AccessModifierDeclarations:
EnforcedStyle: inline

Layout/LineLength:
Max: 120
3 changes: 2 additions & 1 deletion lib/cli/commands/start.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# typed: false
# typed: true

require "fileutils"

Expand All @@ -9,6 +9,7 @@ def self.call(args)
case args[0]
when "new"
app_name = args[1] || "MyApp"
# @TODO(lud, 31.12.2023): classify is from ActiveSupport -> remove this?
app_name = app_name.gsub(/[-\s]/, "_").classify
NewApp::Execute.call(app_name: app_name)
else
Expand Down
7 changes: 6 additions & 1 deletion lib/kirei/app_base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,12 @@ def raw_db_connection
@raw_db_connection = Sequel.connect(AppBase.config.db_url || default_db_url)

config.db_extensions.each do |ext|
@raw_db_connection.extension(ext)
T.cast(@raw_db_connection, Sequel::Database).extension(ext)
end

if config.db_extensions.include?(:pg_json)
# https://github.com/jeremyevans/sequel/blob/5.75.0/lib/sequel/extensions/pg_json.rb#L8
@raw_db_connection.wrap_json_primitives = true
end

@raw_db_connection
Expand Down
64 changes: 63 additions & 1 deletion lib/kirei/base_model.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,28 @@ def class; super; end # rubocop:disable all
).returns(T.self_type)
end
def update(hash)
hash[:updated_at] = Time.now.utc if respond_to?(:updated_at) && hash[:updated_at].nil?
self.class.db.where({ id: id }).update(hash)
self.class.find_by({ id: id })
end

# warning: this is not concurrency-safe
# save keeps the original object intact, and returns a new object with the updated values.
sig { returns(T.self_type) }
def save
previous_record = self.class.find_by({ id: id })

hash = serialize
Helpers.deep_symbolize_keys!(hash)
hash = T.cast(hash, T::Hash[Symbol, T.untyped])

if previous_record.nil?
self.class.create(hash)
else
update(hash)
end
end

module BaseClassInterface
extend T::Sig
extend T::Helpers
Expand All @@ -33,6 +51,10 @@ def find_by(hash)
def where(hash)
end

sig { abstract.params(hash: T.untyped).returns(T.untyped) }
def create(hash)
end

sig { abstract.params(hash: T.untyped).returns(T.untyped) }
def resolve(hash)
end
Expand Down Expand Up @@ -88,6 +110,44 @@ def where(hash)
resolve(db.where(hash))
end

# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
# default values defined in the model are used, if omitted in the hash
sig do
override.params(
hash: T::Hash[Symbol, T.untyped],
).returns(T.attached_class)
end
def create(hash)
# instantiate a new object to ensure we use default values defined in the model
without_id = !hash.key?(:id)
hash[:id] = "kirei-fake-id" if without_id
new_record = from_hash(Helpers.deep_stringify_keys(hash))
all_attributes = T.let(new_record.serialize, T::Hash[String, T.untyped])
all_attributes.delete("id") if without_id && all_attributes["id"] == "kirei-fake-id"

# setting `@raw_db_connection.wrap_json_primitives = true`
# only works on JSON primitives, but not on blank hashes/arrays
if AppBase.config.db_extensions.include?(:pg_json)
all_attributes.each_pair do |key, value|
next unless value.is_a?(Hash) || value.is_a?(Array)

all_attributes[key] = T.unsafe(Sequel).pg_jsonb_wrap(value)
end
end

if new_record.respond_to?(:created_at) && all_attributes["created_at"].nil?
all_attributes["created_at"] = Time.now.utc
end
if new_record.respond_to?(:updated_at) && all_attributes["updated_at"].nil?
all_attributes["updated_at"] = Time.now.utc
end

pkey = T.let(db.insert(all_attributes), String)

T.must(find_by({ id: pkey }))
end
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity

sig do
override.params(
hash: T::Hash[Symbol, T.untyped],
Expand Down Expand Up @@ -125,7 +185,9 @@ def resolve(query, strict = nil)
).returns(T.nilable(T.attached_class))
end
def resolve_first(query, strict = nil)
resolve(query.limit(1), strict).first
strict_loading = strict.nil? ? AppBase.config.db_strict_type_resolving : strict

resolve(query.limit(1), strict_loading).first
end
end

Expand Down
24 changes: 22 additions & 2 deletions lib/kirei/helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,34 @@ def blank?(string)
string.nil? || string.to_s.empty?
end

sig { params(object: T.untyped).returns(T.untyped) }
def deep_stringify_keys(object)
deep_transform_keys(object) { _1.to_s rescue _1 } # rubocop:disable Style/RescueModifier
end

sig { params(object: T.untyped).returns(T.untyped) }
def deep_stringify_keys!(object)
deep_transform_keys!(object) { _1.to_s rescue _1 } # rubocop:disable Style/RescueModifier
end

sig { params(object: T.untyped).returns(T.untyped) }
def deep_symbolize_keys(object)
deep_transform_keys(object) { _1.to_sym rescue _1 } # rubocop:disable Style/RescueModifier
end

sig { params(object: T.untyped).returns(T.untyped) }
def deep_symbolize_keys!(object)
deep_transform_keys!(object) { _1.to_sym rescue _1 } # rubocop:disable Style/RescueModifier
end

# Simplified version from Rails' ActiveSupport
sig do
params(
object: T.untyped, # could be anything due to recursive calls
block: Proc,
).returns(T.untyped) # could be anything due to recursive calls
end
def deep_transform_keys(object, &block)
private def deep_transform_keys(object, &block)
case object
when Hash
object.each_with_object({}) do |(key, value), result|
Expand All @@ -49,7 +69,7 @@ def deep_transform_keys(object, &block)
block: Proc,
).returns(T.untyped) # could be anything due to recursive calls
end
def deep_transform_keys!(object, &block)
private def deep_transform_keys!(object, &block)
case object
when Hash
# using `each_key` results in a `RuntimeError: can't add a new key into hash during iteration`
Expand Down
2 changes: 1 addition & 1 deletion lib/kirei/logger.rb
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ def self.mask(k, v)
end
def self.flatten_hash_and_mask_sensitive_values(hash, prefix = :'')
result = T.let({}, T::Hash[Symbol, T.untyped])
Kirei::Helpers.deep_transform_keys!(hash) { _1.to_sym rescue _1 } # rubocop:disable Style/RescueModifier
Kirei::Helpers.deep_symbolize_keys!(hash)

hash.each do |key, value|
new_prefix = Kirei::Helpers.blank?(prefix) ? key : :"#{prefix}.#{key}"
Expand Down
7 changes: 7 additions & 0 deletions sorbet/rbi/shims/base_model.rbi
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
# rubocop:disable Style/EmptyMethod
module Kirei
module BaseModel
include Kernel # "self" is a class since we include the module in a class
include T::Props::Serializable

sig { returns(T.any(String, Integer)) }
def id; end

Expand All @@ -12,6 +15,10 @@ module Kirei
sig { returns(String) }
def name; end
end

module BaseClassInterface
# include T::Props::Serializable::ClassMethods
end
end
end
# rubocop:enable Style/EmptyMethod

0 comments on commit 06201a8

Please sign in to comment.