airctiverecord 0.2.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.
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AirctiveRecord
4
+ class Base < Norairrecord::Table
5
+ extend ActiveModel::Naming
6
+ extend ActiveModel::Translation
7
+ include ActiveModel::Conversion
8
+ include ActiveModel::Dirty
9
+ include AirctiveRecord::Validations
10
+ include AirctiveRecord::Callbacks
11
+ include AirctiveRecord::AttributeMethods
12
+ include AirctiveRecord::Associations
13
+ include AirctiveRecord::Scoping
14
+
15
+ class << self
16
+ def column_names
17
+ @column_names ||= []
18
+ end
19
+
20
+ def attribute(name, airtable_field_name = nil, **options)
21
+ column_names << name.to_s unless column_names.include?(name.to_s)
22
+ field(name, airtable_field_name, **options)
23
+ end
24
+
25
+ # each model gets its own Relation class
26
+ def relation_class
27
+ @relation_class ||= Class.new(AirctiveRecord::Relation)
28
+ end
29
+
30
+ def relation_class_name
31
+ "#{name}::Relation"
32
+ end
33
+ end
34
+
35
+ def initialize(attributes = {}, **kwargs)
36
+ # Extract id and created_at if present
37
+ id = kwargs.delete(:id)
38
+ created_at = kwargs.delete(:created_at)
39
+
40
+ # Merge positional hash and kwargs to handle both styles
41
+ all_attrs = attributes.is_a?(Hash) ? attributes.merge(kwargs) : kwargs
42
+
43
+ # Norairrecord::Table expects field names as STRING keys
44
+ # We need to convert Ruby attribute names to Airtable field names
45
+ mapped_attrs = {}
46
+ all_attrs.each do |key, value|
47
+ field_name = self.class.field_mappings[key.to_s] || key.to_s
48
+ mapped_attrs[field_name.to_s] = value # Ensure string keys
49
+ end
50
+
51
+ # Call norairrecord's initialize properly
52
+ if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("3.0.0")
53
+ if mapped_attrs.empty?
54
+ super(id: id, created_at: created_at)
55
+ else
56
+ super(mapped_attrs, id: id, created_at: created_at)
57
+ end
58
+ else
59
+ super(mapped_attrs, id: id, created_at: created_at)
60
+ end
61
+
62
+ clear_changes_information
63
+ end
64
+
65
+ def assign_attributes(attrs)
66
+ self.attributes = attrs
67
+ end
68
+
69
+ def serializable_fields
70
+ # exclude readonly fields from serialization
71
+ fields.reject { |k, v| self.class.readonly_fields.include?(k) }
72
+ end
73
+
74
+ def save(**options)
75
+ return false unless valid?
76
+
77
+ begin
78
+ run_callbacks :save do
79
+ if new_record?
80
+ run_callbacks :create do
81
+ super(**options)
82
+ end
83
+ else
84
+ run_callbacks :update do
85
+ super(**options)
86
+ end
87
+ end
88
+ end
89
+ changes_applied
90
+ true
91
+ rescue => e
92
+ false
93
+ end
94
+ end
95
+
96
+ def save!(**options)
97
+ raise RecordInvalid, errors.full_messages.join(", ") unless valid?
98
+ save(**options) || raise(RecordNotSaved, "Failed to save record")
99
+ end
100
+
101
+ def update(attributes)
102
+ attributes.each { |key, value| send("#{key}=", value) }
103
+ save
104
+ end
105
+
106
+ def update!(attributes)
107
+ attributes.each { |key, value| send("#{key}=", value) }
108
+ save!
109
+ end
110
+
111
+ def reload
112
+ return self if new_record?
113
+ reloaded = self.class.find(id)
114
+ @fields = reloaded.fields
115
+ @created_at = reloaded.created_at
116
+ clear_changes_information
117
+ self
118
+ end
119
+
120
+ def persisted?
121
+ !new_record?
122
+ end
123
+
124
+ def to_param
125
+ id
126
+ end
127
+
128
+ def to_key
129
+ persisted? ? [id] : nil
130
+ end
131
+
132
+ def to_model
133
+ self
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AirctiveRecord
4
+ module Callbacks
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ include ActiveModel::Validations::Callbacks
9
+
10
+ define_model_callbacks :save, :create, :update, :destroy
11
+ define_model_callbacks :initialize, only: :after
12
+ end
13
+
14
+ def destroy
15
+ run_callbacks :destroy do
16
+ super
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AirctiveRecord
4
+ # Base Relation class - models create subclasses of this
5
+ # Each model's relation knows about that model's field mappings and scopes
6
+ class Relation < Airrel::Relation
7
+ # override where to apply field mappings when building formulas from hashes
8
+ def where(conditions)
9
+ if conditions.is_a?(Hash)
10
+ # build formula with field mappings
11
+ formula = Airrel::FormulaBuilder.hash_to_formula(conditions, klass.field_mappings)
12
+ super(formula)
13
+ else
14
+ super
15
+ end
16
+ end
17
+
18
+ # override to use field mappings when building airtable params
19
+ def to_airtable_params
20
+ params = {}
21
+
22
+ # filter
23
+ if @where_clause.any?
24
+ params[:filter] = @where_clause.to_airtable_formula
25
+ end
26
+
27
+ # sort - use field mappings
28
+ if @order_values.any?
29
+ params[:sort] = @order_values.map do |field, direction|
30
+ mapped_field = klass.field_mappings[field.to_s] || field.to_s
31
+ { field: mapped_field, direction: direction.to_s }
32
+ end
33
+ end
34
+
35
+ # limit
36
+ params[:max_records] = @limit_value if @limit_value
37
+
38
+ # offset
39
+ params[:offset] = @offset_value if @offset_value
40
+
41
+ params
42
+ end
43
+
44
+ # spawn returns a new instance of the same Relation subclass
45
+ def spawn
46
+ self.class.new(klass).tap do |relation|
47
+ relation.instance_variable_set(:@where_clause, @where_clause)
48
+ relation.instance_variable_set(:@order_values, @order_values.dup)
49
+ relation.instance_variable_set(:@limit_value, @limit_value)
50
+ relation.instance_variable_set(:@offset_value, @offset_value)
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AirctiveRecord
4
+ module Scoping
5
+ extend ActiveSupport::Concern
6
+
7
+ class_methods do
8
+ # returns a chainable Relation object (model-specific!)
9
+ def all
10
+ relation_class.new(self)
11
+ end
12
+
13
+ # scopes are defined on the model's specific Relation class
14
+ def scope(name, body)
15
+ unless body.respond_to?(:call)
16
+ raise ArgumentError, "The scope body needs to be callable."
17
+ end
18
+
19
+ # define on the class
20
+ singleton_class.send(:define_method, name) do |*args|
21
+ all.public_send(name, *args)
22
+ end
23
+
24
+ # define on this model's Relation class
25
+ relation_class.send(:define_method, name) do |*args|
26
+ instance_exec(*args, &body)
27
+ end
28
+ end
29
+
30
+ # delegate finder methods to all
31
+ def where(conditions)
32
+ all.where(conditions)
33
+ end
34
+
35
+ def order(*args)
36
+ all.order(*args)
37
+ end
38
+
39
+ def limit(value)
40
+ all.limit(value)
41
+ end
42
+
43
+ def offset(value)
44
+ all.offset(value)
45
+ end
46
+
47
+ def find_by(conditions)
48
+ all.find_by(conditions)
49
+ end
50
+
51
+ def find_by!(conditions)
52
+ all.find_by!(conditions)
53
+ end
54
+
55
+ def first(limit = nil)
56
+ all.first(limit)
57
+ end
58
+
59
+ def last(limit = nil)
60
+ all.last(limit)
61
+ end
62
+
63
+ def count
64
+ all.count
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AirctiveRecord
4
+ module Validations
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ include ActiveModel::Validations
9
+
10
+ define_model_callbacks :validation
11
+ end
12
+
13
+ def valid?(context = nil)
14
+ run_callbacks :validation do
15
+ super
16
+ end
17
+ end
18
+
19
+ def save(**options)
20
+ return false unless valid?
21
+ super
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AirctiveRecord
4
+ VERSION = "0.2.0"
5
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "airctiverecord/version"
4
+ require "norairrecord"
5
+ require "active_model"
6
+ require "airrel"
7
+
8
+ module AirctiveRecord
9
+ class Error < StandardError; end
10
+ class RecordInvalid < Error; end
11
+ class RecordNotSaved < Error; end
12
+
13
+ autoload :Base, "airctiverecord/base"
14
+ autoload :Callbacks, "airctiverecord/callbacks"
15
+ autoload :Validations, "airctiverecord/validations"
16
+ autoload :AttributeMethods, "airctiverecord/attribute_methods"
17
+ autoload :Associations, "airctiverecord/associations"
18
+ autoload :Scoping, "airctiverecord/scoping"
19
+ autoload :Relation, "airctiverecord/relation"
20
+ end
@@ -0,0 +1,4 @@
1
+ module Airctiverecord
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,121 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: airctiverecord
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - 24c02
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: norairrecord
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '0.5'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '0.5'
26
+ - !ruby/object:Gem::Dependency
27
+ name: activemodel
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '6.0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '6.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: activesupport
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '6.0'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '6.0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: airrel
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: 0.2.0
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: 0.2.0
68
+ description: Provides a familiar ActiveRecord-like interface for Airtable, built on
69
+ top of norairrecord with validations, callbacks, and associations.
70
+ email:
71
+ - 163450896+24c02@users.noreply.github.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - ".idea/airctiverecord.iml"
77
+ - ".idea/modules.xml"
78
+ - ".idea/vcs.xml"
79
+ - ".idea/workspace.xml"
80
+ - README.md
81
+ - Rakefile
82
+ - examples/associations_example.rb
83
+ - examples/basic_usage.rb
84
+ - examples/boolean_fields.rb
85
+ - examples/chainable_queries.rb
86
+ - examples/field_mapping_example.rb
87
+ - examples/readonly_fields.rb
88
+ - examples/scope_isolation.rb
89
+ - lib/airctiverecord.rb
90
+ - lib/airctiverecord/associations.rb
91
+ - lib/airctiverecord/attribute_methods.rb
92
+ - lib/airctiverecord/base.rb
93
+ - lib/airctiverecord/callbacks.rb
94
+ - lib/airctiverecord/relation.rb
95
+ - lib/airctiverecord/scoping.rb
96
+ - lib/airctiverecord/validations.rb
97
+ - lib/airctiverecord/version.rb
98
+ - sig/airctiverecord.rbs
99
+ homepage: https://github.com/24c02/airctiverecord
100
+ licenses: []
101
+ metadata:
102
+ homepage_uri: https://github.com/24c02/airctiverecord
103
+ source_code_uri: https://github.com/24c02/airctiverecord
104
+ rdoc_options: []
105
+ require_paths:
106
+ - lib
107
+ required_ruby_version: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ version: 3.2.0
112
+ required_rubygems_version: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ requirements: []
118
+ rubygems_version: 3.6.7
119
+ specification_version: 4
120
+ summary: ActiveRecord/ActiveModel compatible API for Airtable via norairrecord
121
+ test_files: []