Class: Keinaufwand::Sync::Record

Inherits:
ActiveRecord::Base
  • Object
show all
Defined in:
lib/keinaufwand/sync_core/record.rb

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.apply_json_conditions(relation, conditions) ⇒ Object



196
197
198
199
200
# File 'lib/keinaufwand/sync_core/record.rb', line 196

def self.apply_json_conditions(relation, conditions)
  conditions.reduce(relation) do |scope, (attribute, value)|
    scope.where(json_condition_sql(attribute, value), *json_condition_binds(value))
  end
end

.boot_keinaufwand_dynamics!Object



95
96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/keinaufwand/sync_core/record.rb', line 95

def self.boot_keinaufwand_dynamics!
  return if abstract_class? || name.blank? || name == "Keinaufwand::Record"

  schema_attributes.each_key do |attribute|
    define_dynamic_attribute(attribute)
  end

  schema_associations.each do |association, config|
    define_dynamic_association(association, config)
  end
rescue Errno::ENOENT
  nil
end

.configure_keinaufwand_schema!(schema) ⇒ Object



10
11
12
13
# File 'lib/keinaufwand/sync_core/record.rb', line 10

def self.configure_keinaufwand_schema!(schema)
  self.keinaufwand_schema = schema
  descendants.each(&:boot_keinaufwand_dynamics!) if schema && respond_to?(:descendants)
end

.define_dynamic_association(association, config) ⇒ Object



129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
# File 'lib/keinaufwand/sync_core/record.rb', line 129

def self.define_dynamic_association(association, config)
  association = association.to_s
  config = config.with_indifferent_access
  type = config[:type].to_s
  target_class_name = config[:class_name] || association.singularize.classify

  define_dynamic_foreign_key(association, type) if %w[belongs_to has_one].include?(type)
  define_dynamic_ids(association) if type == "has_and_belongs_to_many"

  define_method(association) do
    return instance_variable_get("@#{association}") if instance_variable_defined?("@#{association}")

    target_class = target_class_name.safe_constantize
    result = target_class ? load_keinaufwand_association(association, type, target_class, config) : nil
    instance_variable_set("@#{association}", result)
  end

  define_method("#{association}=") do |value|
    instance_variable_set("@#{association}", value)
  end
end

.define_dynamic_attribute(attribute) ⇒ Object



109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
# File 'lib/keinaufwand/sync_core/record.rb', line 109

def self.define_dynamic_attribute(attribute)
  attribute = attribute.to_s
  return if attribute == "id"

  define_method(attribute) do |variant = :large|
    value = read_keinaufwand_value(attribute)
    if asset_attribute?(attribute, value)
      value = derivative_value(value, variant)
      value = local_asset_url(value) if respond_to?(:local_asset_url)
    end
    value = localize_asset_urls(value) if attribute == "body" && respond_to?(:localize_asset_urls)
    cast_keinaufwand_value(attribute, value)
  end

  define_method("#{attribute}?") do
    value = public_send(attribute)
    value == true || value.present?
  end
end

.define_dynamic_foreign_key(association, type) ⇒ Object



151
152
153
154
155
156
157
158
# File 'lib/keinaufwand/sync_core/record.rb', line 151

def self.define_dynamic_foreign_key(association, type)
  key = "#{association}_id"
  return if schema_attribute?(key)

  define_method(key) do
    read_keinaufwand_value(key)
  end
end

.define_dynamic_ids(association) ⇒ Object



160
161
162
163
164
165
166
167
# File 'lib/keinaufwand/sync_core/record.rb', line 160

def self.define_dynamic_ids(association)
  key = "#{association.singularize}_ids"
  return if schema_attribute?(key)

  define_method(key) do
    Array(read_keinaufwand_value(key))
  end
end

.exists?(conditions = :none) ⇒ Boolean

Returns:

  • (Boolean)


31
32
33
34
35
36
37
# File 'lib/keinaufwand/sync_core/record.rb', line 31

def self.exists?(conditions = :none)
  if composite_primary_key? && conditions != :none && !conditions.is_a?(Hash) && !conditions.is_a?(Array)
    return super(id: conditions)
  end

  super
end

.find(*ids) ⇒ Object



22
23
24
25
26
27
28
29
# File 'lib/keinaufwand/sync_core/record.rb', line 22

def self.find(*ids)
  return super if ids.empty?

  ids = ids.flatten
  return where(id: ids).index_by(&:id).values_at(*ids.map(&:to_i)) if ids.many?

  find_by!(id: ids.first)
end

.inherited(subclass) ⇒ Object



90
91
92
93
# File 'lib/keinaufwand/sync_core/record.rb', line 90

def self.inherited(subclass)
  super
  subclass.boot_keinaufwand_dynamics!
end

.json_attribute?(attribute) ⇒ Boolean

Returns:

  • (Boolean)


55
56
57
# File 'lib/keinaufwand/sync_core/record.rb', line 55

def self.json_attribute?(attribute)
  !column_names.include?(attribute.to_s) && schema_attribute?(attribute)
end

.json_bind_value(value) ⇒ Object



265
266
267
268
269
270
271
272
273
274
# File 'lib/keinaufwand/sync_core/record.rb', line 265

def self.json_bind_value(value)
  case value
  when Time, ActiveSupport::TimeWithZone
    value.iso8601
  when Date
    value.iso8601
  else
    value
  end
end

.json_condition_binds(value) ⇒ Object



247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
# File 'lib/keinaufwand/sync_core/record.rb', line 247

def self.json_condition_binds(value)
  case value
  when Array
    return [] if value.empty?

    [value.compact.map { |item| json_bind_value(item) }]
  when Range
    return [json_bind_value(value.begin)] if value.end.nil?
    return [json_bind_value(value.end)] if value.begin.nil?

    [json_bind_value(value.begin), json_bind_value(value.end)]
  when nil
    []
  else
    [json_bind_value(value)]
  end
end

.json_condition_sql(attribute, value) ⇒ Object



225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
# File 'lib/keinaufwand/sync_core/record.rb', line 225

def self.json_condition_sql(attribute, value)
  expression = json_value(attribute)

  case value
  when Array
    return "1=0" if value.empty?
    return "(#{expression} IN (?) OR #{expression} IS NULL)" if value.include?(nil)

    "#{expression} IN (?)"
  when Range
    return "#{expression} >= ?" if value.end.nil?
    return "#{expression} #{value.exclude_end? ? '<' : '<='} ?" if value.begin.nil?
    return "#{expression} >= ? AND #{expression} < ?" if value.exclude_end?

    "#{expression} BETWEEN ? AND ?"
  when nil
    "#{expression} IS NULL"
  else
    "#{expression} = ?"
  end
end

.json_hash_conditions?(conditions) ⇒ Boolean

Returns:

  • (Boolean)


188
189
190
# File 'lib/keinaufwand/sync_core/record.rb', line 188

def self.json_hash_conditions?(conditions)
  conditions.is_a?(Hash) && conditions.any? { |key, _| json_attribute?(key) }
end

.json_order_argument?(argument) ⇒ Boolean

Returns:

  • (Boolean)


202
203
204
205
206
207
208
209
210
211
# File 'lib/keinaufwand/sync_core/record.rb', line 202

def self.json_order_argument?(argument)
  case argument
  when Symbol
    json_attribute?(argument)
  when Hash
    argument.keys.all? { |key| json_attribute?(key) }
  else
    false
  end
end

.json_order_sql(argument) ⇒ Object



213
214
215
216
217
218
219
220
221
222
223
# File 'lib/keinaufwand/sync_core/record.rb', line 213

def self.json_order_sql(argument)
  case argument
  when Symbol
    json_value(argument)
  when Hash
    argument.map do |attribute, direction|
      direction = direction.to_s.downcase == "desc" ? "DESC" : "ASC"
      "#{json_value(attribute)} #{direction}"
    end
  end
end

.json_value(attribute) ⇒ Object



39
40
41
42
43
# File 'lib/keinaufwand/sync_core/record.rb', line 39

def self.json_value(attribute)
  table = connection.quote_table_name(table_name)
  column = connection.quote_column_name("data")
  json_value_expression("#{table}.#{column}", attribute)
end

.json_value_expression(column, attribute) ⇒ Object



45
46
47
48
49
50
51
52
53
# File 'lib/keinaufwand/sync_core/record.rb', line 45

def self.json_value_expression(column, attribute)
  if connection.adapter_name.match?(/postgres/i)
    expression = "#{column} #>> #{connection.quote("{#{attribute.to_s.split(".").join(",")}}")}"
    postgres_cast(expression, attribute)
  else
    expression = "json_extract(#{column}, #{connection.quote("$.#{attribute}")})"
    sqlite_cast(expression, attribute)
  end
end

.load_keinaufwand_schemaObject



63
64
65
66
# File 'lib/keinaufwand/sync_core/record.rb', line 63

def self.load_keinaufwand_schema
  version = Keinaufwand::Api.configuration.version || "2026-07-02"
  self.keinaufwand_schema = Keinaufwand::Api::Schema.load(Keinaufwand::Api.configuration.schema_path(version))
end

.model_schemaObject



68
69
70
# File 'lib/keinaufwand/sync_core/record.rb', line 68

def self.model_schema
  schema&.for(name.demodulize)
end

.postgres_cast(expression, attribute) ⇒ Object



169
170
171
172
173
174
175
176
177
178
# File 'lib/keinaufwand/sync_core/record.rb', line 169

def self.postgres_cast(expression, attribute)
  case schema_attribute_type(attribute)
  when "integer" then "(#{expression})::integer"
  when "float", "decimal" then "(#{expression})::numeric"
  when "date" then "(#{expression})::date"
  when "datetime", "time" then "(#{expression})::timestamp"
  when "boolean" then "(#{expression})::boolean"
  else expression
  end
end

.schemaObject



59
60
61
# File 'lib/keinaufwand/sync_core/record.rb', line 59

def self.schema
  keinaufwand_schema || load_keinaufwand_schema
end

.schema_associationsObject



76
77
78
# File 'lib/keinaufwand/sync_core/record.rb', line 76

def self.schema_associations
  model_schema&.fetch("associations", {}) || {}
end

.schema_attribute?(attribute) ⇒ Boolean

Returns:

  • (Boolean)


80
81
82
83
# File 'lib/keinaufwand/sync_core/record.rb', line 80

def self.schema_attribute?(attribute)
  attribute = attribute.to_s
  schema_attributes.key?(attribute) || schema_attributes.key?(attribute.split(".").first)
end

.schema_attribute_type(attribute) ⇒ Object



85
86
87
88
# File 'lib/keinaufwand/sync_core/record.rb', line 85

def self.schema_attribute_type(attribute)
  attribute = attribute.to_s
  (schema_attributes[attribute].presence || schema_attributes[attribute.split(".").first]).to_s
end

.schema_attributesObject



72
73
74
# File 'lib/keinaufwand/sync_core/record.rb', line 72

def self.schema_attributes
  model_schema&.fetch("attributes", {}) || {}
end

.split_json_conditions(conditions) ⇒ Object



192
193
194
# File 'lib/keinaufwand/sync_core/record.rb', line 192

def self.split_json_conditions(conditions)
  conditions.partition { |key, _| json_attribute?(key) }.map(&:to_h)
end

.sqlite_cast(expression, attribute) ⇒ Object



180
181
182
183
184
185
186
# File 'lib/keinaufwand/sync_core/record.rb', line 180

def self.sqlite_cast(expression, attribute)
  case schema_attribute_type(attribute)
  when "integer" then "CAST(#{expression} AS INTEGER)"
  when "float", "decimal" then "CAST(#{expression} AS REAL)"
  else expression
  end
end

.where_present(*attributes) ⇒ Object



15
16
17
18
19
20
# File 'lib/keinaufwand/sync_core/record.rb', line 15

def self.where_present(*attributes)
  attributes.reduce(all) do |scope, attribute|
    expression = json_value(attribute)
    scope.where("#{expression} IS NOT NULL").where("#{expression} != ?", "")
  end
end

Instance Method Details

#asset_attribute?(attribute, value) ⇒ Boolean

Returns:

  • (Boolean)


373
374
375
# File 'lib/keinaufwand/sync_core/record.rb', line 373

def asset_attribute?(attribute, value)
  attribute.end_with?("_url") && value.is_a?(Hash)
end

#build_inline_keinaufwand_record(attributes, target_class) ⇒ Object



337
338
339
340
341
# File 'lib/keinaufwand/sync_core/record.rb', line 337

def build_inline_keinaufwand_record(attributes, target_class)
  return unless attributes.is_a?(Hash)

  target_class.new(id: attributes["id"] || attributes[:id], type: target_class.name, data: attributes.except("id", :id))
end

#build_inline_keinaufwand_records(value, target_class) ⇒ Object



329
330
331
332
333
334
335
# File 'lib/keinaufwand/sync_core/record.rb', line 329

def build_inline_keinaufwand_records(value, target_class)
  if value.is_a?(Array)
    value.map { |attributes| build_inline_keinaufwand_record(attributes, target_class) }.compact
  elsif value.is_a?(Hash)
    build_inline_keinaufwand_record(value, target_class)
  end
end

#cast_keinaufwand_value(attribute, value) ⇒ Object



343
344
345
346
347
348
349
350
351
352
353
# File 'lib/keinaufwand/sync_core/record.rb', line 343

def cast_keinaufwand_value(attribute, value)
  case self.class.schema_attribute_type(attribute)
  when "datetime", "time" then parse_keinaufwand_time(value) || value
  when "date" then parse_keinaufwand_date(value) || value
  when "integer" then value.to_i if value.present?
  when "float" then value.to_f if value.present?
  when "decimal" then value.to_d if value.present?
  when "boolean" then value == true || value.to_s == "true"
  else value
  end
end

#derivative_value(value, size) ⇒ Object



377
378
379
380
381
# File 'lib/keinaufwand/sync_core/record.rb', line 377

def derivative_value(value, size)
  return value unless value.is_a?(Hash)

  value[size.to_s] || value[size.to_sym] || value.values.compact.first
end

#idObject



276
277
278
# File 'lib/keinaufwand/sync_core/record.rb', line 276

def id
  read_attribute(:id)
end

#id=(value) ⇒ Object



280
281
282
283
284
285
286
287
288
# File 'lib/keinaufwand/sync_core/record.rb', line 280

def id=(value)
  if value.is_a?(Array)
    write_keinaufwand_column("id", value.first)
    write_keinaufwand_column("type", value.second)
  else
    write_keinaufwand_column("id", value)
    write_keinaufwand_column("type", self.class.name) if self[:type].blank? && self.class.name.present?
  end
end

#load_keinaufwand_association(association, type, target_class, config) ⇒ Object



309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
# File 'lib/keinaufwand/sync_core/record.rb', line 309

def load_keinaufwand_association(association, type, target_class, config)
  inline_value = read_keinaufwand_value(association)

  case type
  when "belongs_to", "has_one"
    target_id = read_keinaufwand_value("#{association}_id")
    local = target_class.find_by(id: target_id) if target_id.present?
    local || build_inline_keinaufwand_records(inline_value, target_class)
  when "has_many"
    foreign_key = config[:foreign_key] || "#{self.class.name.demodulize.underscore}_id"
    local = target_class.where(foreign_key => id)
    local.exists? ? local : build_inline_keinaufwand_records(inline_value, target_class)
  when "has_and_belongs_to_many"
    target_ids = Array(read_keinaufwand_value("#{association.singularize}_ids"))
    target_class.where(id: target_ids)
  else
    build_inline_keinaufwand_records(inline_value, target_class)
  end
end

#parse_keinaufwand_date(value) ⇒ Object



364
365
366
367
368
369
370
371
# File 'lib/keinaufwand/sync_core/record.rb', line 364

def parse_keinaufwand_date(value)
  return value if value.is_a?(Date)
  return if value.blank?

  Date.parse(value.to_s)
rescue ArgumentError
  nil
end

#parse_keinaufwand_time(value) ⇒ Object



355
356
357
358
359
360
361
362
# File 'lib/keinaufwand/sync_core/record.rb', line 355

def parse_keinaufwand_time(value)
  return value if value.is_a?(Time) || defined?(ActiveSupport::TimeWithZone) && value.is_a?(ActiveSupport::TimeWithZone)
  return if value.blank?

  Time.zone ? Time.zone.parse(value.to_s) : Time.parse(value.to_s)
rescue ArgumentError
  nil
end

#primary_key_values_present?Boolean

Returns:

  • (Boolean)


290
291
292
# File 'lib/keinaufwand/sync_core/record.rb', line 290

def primary_key_values_present?
  Array(self.class.primary_key).all? { |column| @attributes.fetch_value(column).present? }
end

#read_keinaufwand_value(attribute) ⇒ Object



305
306
307
# File 'lib/keinaufwand/sync_core/record.rb', line 305

def read_keinaufwand_value(attribute)
  data[attribute.to_s] if data.is_a?(Hash)
end

#remember_transaction_record_stateObject



298
299
300
301
302
303
# File 'lib/keinaufwand/sync_core/record.rb', line 298

def remember_transaction_record_state
  super
  return unless self.class.composite_primary_key? && @_start_transaction_state

  @_start_transaction_state[:id] = self.class.primary_key.map { |column| @attributes.fetch_value(column) }
end

#write_keinaufwand_column(attribute, value) ⇒ Object



294
295
296
# File 'lib/keinaufwand/sync_core/record.rb', line 294

def write_keinaufwand_column(attribute, value)
  @attributes.write_from_user(attribute, value)
end