airctiverecord 0.2.0 → 0.2.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9dfcc33cb71a75dadf8007c2f4acab0d9274d617fa5b6a3d97dd95061a155c42
4
- data.tar.gz: 22b3443fdebc4ced36b670b02f77289b4749c204aed23b3719a8e30b65ee28b1
3
+ metadata.gz: 5b646edd0e5f82349a4aeb249a12c8b56ee34b6fb1f090351341c1bb429a9edb
4
+ data.tar.gz: b5a8aaffeb5eb8fcc403f37dccb08f0fcbd4e51bc20c3e5a6ab63c170cf98b50
5
5
  SHA512:
6
- metadata.gz: 4cc51162eb066c606ae86b331bc793a0b34803d03298daa07abb6d8509649337c3369676ab2bcefb252730c0916520b45b6aad2b932af436229aee52d76c6171
7
- data.tar.gz: 92988e793b0fdce815872a9fa64c9d7a60df73ef71deea731456e8e407b9f9a75fd5794986880a2f4c546c48f7818a988170e7d92d6aca5517fea17f219778a7
6
+ metadata.gz: 40749055a208071322ceaae739c4b93a2df42d1da1a19711fdc42db4dba1724fcb976b9ee876dcce8c4e2c4a72920187c7319e9c0aa6e04c54651767eef8293c
7
+ data.tar.gz: 41b2f812a0ba8b6a42e1c2b0bca4d8bc889d119668ef094348216e961c9bf67d8830a8bfffb620f69617e1eced2456af517005ea88908a1ad0eae4fe37ea8cf2
@@ -12,26 +12,26 @@ BASE_KEY = ENV.fetch("AIRTABLE_BASE_KEY")
12
12
  class Team < AirctiveRecord::Base
13
13
  self.base_key = BASE_KEY
14
14
  self.table_name = "Teams"
15
-
15
+
16
16
  attribute :name
17
17
  attribute :description
18
-
18
+
19
19
  has_many :users
20
20
  has_one :leader, class_name: "User", foreign_key: "Leader"
21
-
21
+
22
22
  validates :name, presence: true
23
23
  end
24
24
 
25
25
  class User < AirctiveRecord::Base
26
26
  self.base_key = BASE_KEY
27
27
  self.table_name = "Users"
28
-
28
+
29
29
  attribute :name
30
30
  attribute :email
31
-
31
+
32
32
  belongs_to :team
33
33
  has_many :tasks
34
-
34
+
35
35
  validates :name, presence: true
36
36
  validates :email, presence: true
37
37
  end
@@ -39,17 +39,17 @@ end
39
39
  class Task < AirctiveRecord::Base
40
40
  self.base_key = BASE_KEY
41
41
  self.table_name = "Tasks"
42
-
42
+
43
43
  attribute :title
44
44
  attribute :status
45
45
  attribute :description
46
-
46
+
47
47
  belongs_to :user
48
48
  has_one :team, through: :user
49
-
49
+
50
50
  validates :title, presence: true
51
51
  validates :status, inclusion: { in: %w[pending in_progress completed] }
52
-
52
+
53
53
  scope :pending, -> { where("{Status} = 'pending'") }
54
54
  scope :completed, -> { where("{Status} = 'completed'") }
55
55
  end
@@ -11,33 +11,33 @@ Norairrecord.api_key = ENV.fetch("AIRTABLE_API_KEY")
11
11
  class User < AirctiveRecord::Base
12
12
  self.base_key = ENV.fetch("AIRTABLE_BASE_KEY")
13
13
  self.table_name = "Users"
14
-
14
+
15
15
  # Define attributes
16
16
  attribute :name
17
17
  attribute :email
18
18
  attribute :age
19
19
  attribute :role
20
-
20
+
21
21
  # Validations
22
22
  validates :name, presence: true
23
23
  validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
24
24
  validates :age, numericality: { greater_than: 0, less_than: 150 }, allow_nil: true
25
25
  validates :role, inclusion: { in: %w[admin user guest] }, allow_nil: true
26
-
26
+
27
27
  # Callbacks
28
28
  before_save :normalize_email
29
29
  after_create :log_creation
30
-
30
+
31
31
  # Scopes
32
32
  scope :admins, -> { where("{Role} = 'admin'") }
33
33
  scope :active, -> { where("{Active} = TRUE()") }
34
-
34
+
35
35
  private
36
-
36
+
37
37
  def normalize_email
38
38
  self.email = email.downcase.strip if email.present?
39
39
  end
40
-
40
+
41
41
  def log_creation
42
42
  puts "Created user: #{name} (#{email})"
43
43
  end
@@ -86,6 +86,6 @@ puts "Name is now: #{user.name}"
86
86
  puts "\nTrying to create invalid user..."
87
87
  invalid_user = User.new(email: "not-an-email")
88
88
  puts "Valid? #{invalid_user.valid?}"
89
- puts "Errors: #{invalid_user.errors.full_messages.join(', ')}"
89
+ puts "Errors: #{invalid_user.errors.full_messages.join(", ")}"
90
90
 
91
91
  puts "\nDone!"
@@ -7,17 +7,17 @@ require "airctiverecord"
7
7
  class User < AirctiveRecord::Base
8
8
  self.base_key = "appTest123"
9
9
  self.table_name = "Users"
10
-
10
+
11
11
  field :name, "Name"
12
12
  field :email, "Email"
13
13
  field :active, "Active", type: :boolean
14
14
  field :verified, "Verified", type: :boolean
15
15
  field :admin, "Admin", type: :boolean
16
-
16
+
17
17
  scope :active, -> { where(active: true) }
18
18
  scope :verified, -> { where(verified: true) }
19
19
  scope :admins, -> { where(admin: true) }
20
-
20
+
21
21
  def self.records(**params)
22
22
  puts "records(#{params.inspect})"
23
23
  []
@@ -7,18 +7,18 @@ require "airctiverecord"
7
7
  class User < AirctiveRecord::Base
8
8
  self.base_key = "appTest123"
9
9
  self.table_name = "Users"
10
-
10
+
11
11
  field :first_name, "First Name"
12
12
  field :email, "Email Address"
13
13
  field :role, "Role"
14
14
  field :active, "Active"
15
15
  field :age, "Age"
16
-
16
+
17
17
  scope :active, -> { where(active: true) }
18
18
  scope :admins, -> { where(role: "admin") }
19
19
  scope :adults, -> { where("AND({Age} >= 18, {Age} < 65)") }
20
20
  scope :recent, -> { order(created_at: :desc).limit(10) }
21
-
21
+
22
22
  def self.records(**params)
23
23
  puts "Would call Airtable API with:"
24
24
  puts params.inspect
@@ -44,7 +44,7 @@ User.where(age: 18..65).to_a
44
44
  puts
45
45
 
46
46
  puts "=== IN queries ==="
47
- User.where(role: ["admin", "moderator", "guest"]).to_a
47
+ User.where(role: %w[admin moderator guest]).to_a
48
48
  puts
49
49
 
50
50
  puts "=== raw formulas still work ==="
@@ -11,7 +11,7 @@ Norairrecord.api_key = ENV.fetch("AIRTABLE_API_KEY", "test_key")
11
11
  class Contact < AirctiveRecord::Base
12
12
  self.base_key = ENV.fetch("AIRTABLE_BASE_KEY", "appTest123")
13
13
  self.table_name = "Contacts"
14
-
14
+
15
15
  # Map Ruby attribute names to Airtable field names with spaces
16
16
  field :first_name, "First Name"
17
17
  field :last_name, "Last Name"
@@ -22,21 +22,21 @@ class Contact < AirctiveRecord::Base
22
22
  field :linkedin_url, "LinkedIn URL"
23
23
  field :date_added, "Date Added"
24
24
  field :is_vip, "VIP?"
25
-
25
+
26
26
  # Validations using Ruby attribute names
27
27
  validates :first_name, :last_name, presence: true
28
28
  validates :email_address, format: { with: URI::MailTo::EMAIL_REGEXP }, allow_blank: true
29
29
  validates :phone_number, length: { minimum: 10 }, allow_blank: true
30
-
30
+
31
31
  # Callbacks
32
32
  before_save :normalize_email
33
-
33
+
34
34
  # Scopes
35
35
  scope :vip, -> { where("{VIP?} = TRUE()") }
36
36
  scope :with_email, -> { where("{Email Address} != ''") }
37
-
37
+
38
38
  private
39
-
39
+
40
40
  def normalize_email
41
41
  self.email_address = email_address&.downcase&.strip
42
42
  end
@@ -10,15 +10,15 @@ end
10
10
 
11
11
  class Contact < AirpplicationRecord
12
12
  self.table_name = "Contacts"
13
-
13
+
14
14
  field :name, "Name"
15
15
  field :email, "Email"
16
16
  field :company, "Company"
17
-
17
+
18
18
  # lookup fields (pulled from linked Company record)
19
19
  field :company_name, "Company Name (from Company)", readonly: true
20
20
  field :company_address, "Company Address (from Company)", readonly: true
21
-
21
+
22
22
  # formula fields (computed by airtable)
23
23
  field :full_name, "Full Name (formula)", readonly: true
24
24
  end
@@ -26,16 +26,16 @@ end
26
26
  contact = Contact.new(
27
27
  name: "Alice",
28
28
  email: "alice@example.com",
29
- company_name: "Acme Corp" # readonly field - silently ignored
29
+ company_name: "Acme Corp" # readonly field - silently ignored
30
30
  )
31
31
 
32
32
  puts "=== readonly fields are readable ==="
33
33
  contact.instance_variable_set(:@fields, {
34
- "Name" => "Alice",
35
- "Email" => "alice@example.com",
36
- "Company Name (from Company)" => "Acme Corp",
37
- "Full Name (formula)" => "Alice Smith"
38
- })
34
+ "Name" => "Alice",
35
+ "Email" => "alice@example.com",
36
+ "Company Name (from Company)" => "Acme Corp",
37
+ "Full Name (formula)" => "Alice Smith"
38
+ })
39
39
 
40
40
  puts "company_name: #{contact.company_name}"
41
41
  puts "full_name: #{contact.full_name}"
@@ -7,13 +7,13 @@ require "airctiverecord"
7
7
  class User < AirctiveRecord::Base
8
8
  self.base_key = "appTest123"
9
9
  self.table_name = "Users"
10
-
10
+
11
11
  field :role, "Role"
12
12
  field :active, "Active"
13
-
13
+
14
14
  scope :active, -> { where(active: true) }
15
15
  scope :admins, -> { where(role: "admin") }
16
-
16
+
17
17
  def self.records(**params)
18
18
  puts "User.records called with: #{params.inspect}"
19
19
  []
@@ -23,13 +23,13 @@ end
23
23
  class Post < AirctiveRecord::Base
24
24
  self.base_key = "appTest123"
25
25
  self.table_name = "Posts"
26
-
26
+
27
27
  field :status, "Status"
28
28
  field :featured, "Featured"
29
-
29
+
30
30
  scope :published, -> { where(status: "published") }
31
31
  scope :featured, -> { where(featured: true) }
32
-
32
+
33
33
  def self.records(**params)
34
34
  puts "Post.records called with: #{params.inspect}"
35
35
  []
@@ -8,13 +8,13 @@ module AirctiveRecord
8
8
  # just add activerecord-style defaults, norairrecord does the heavy lifting
9
9
  def has_many(name, options_arg = nil, **options_kwargs)
10
10
  options = options_arg || options_kwargs
11
-
11
+
12
12
  if options[:through]
13
13
  define_has_many_through(name, options)
14
14
  else
15
15
  column = options[:column] || options[:foreign_key] || "#{name.to_s.singularize}_ids"
16
16
  klass = options[:class_name] || name.to_s.classify
17
-
17
+
18
18
  super(name, { column: column, class: klass }.merge(options))
19
19
  end
20
20
  end
@@ -22,14 +22,14 @@ module AirctiveRecord
22
22
  def belongs_to(name, **options)
23
23
  column = options[:column] || options[:foreign_key] || "#{name}_id"
24
24
  klass = options[:class_name] || name.to_s.classify
25
-
25
+
26
26
  super(name, { column: column, class: klass }.merge(options))
27
27
  end
28
28
 
29
29
  def has_one(name, **options)
30
30
  column = options[:column] || options[:foreign_key] || "#{name}_id"
31
31
  klass = options[:class_name] || name.to_s.classify
32
-
32
+
33
33
  super(name, { column: column, class: klass }.merge(options))
34
34
  end
35
35
 
@@ -22,10 +22,10 @@ module AirctiveRecord
22
22
  airtable_field_name ||= attr_name
23
23
  readonly = options[:readonly] || options[:read_only]
24
24
  field_type = options[:type]
25
-
25
+
26
26
  field_mappings[attr_name] = airtable_field_name
27
27
  readonly_fields << airtable_field_name if readonly
28
-
28
+
29
29
  define_attribute_methods attr_name
30
30
 
31
31
  define_method(attr_name) do
@@ -35,14 +35,14 @@ module AirctiveRecord
35
35
 
36
36
  if readonly
37
37
  # readonly fields silently ignore sets (airtable rejects them anyway)
38
- define_method("#{attr_name}=") do |value|
38
+ define_method("#{attr_name}=") do |_value|
39
39
  nil
40
40
  end
41
41
  else
42
42
  define_method("#{attr_name}=") do |value|
43
43
  field_name = self.class.field_mappings[attr_name]
44
44
  return if self[field_name] == value
45
-
45
+
46
46
  send("#{attr_name}_will_change!") unless self[field_name] == value
47
47
  self[field_name] = value
48
48
  end
@@ -66,9 +66,7 @@ module AirctiveRecord
66
66
  end
67
67
  end
68
68
 
69
- def attributes
70
- fields
71
- end
69
+ def attributes = fields
72
70
 
73
71
  def attributes=(attrs)
74
72
  attrs.each do |key, value|
@@ -27,19 +27,17 @@ module AirctiveRecord
27
27
  @relation_class ||= Class.new(AirctiveRecord::Relation)
28
28
  end
29
29
 
30
- def relation_class_name
31
- "#{name}::Relation"
32
- end
30
+ def relation_class_name = "#{name}::Relation"
33
31
  end
34
32
 
35
33
  def initialize(attributes = {}, **kwargs)
36
34
  # Extract id and created_at if present
37
35
  id = kwargs.delete(:id)
38
36
  created_at = kwargs.delete(:created_at)
39
-
37
+
40
38
  # Merge positional hash and kwargs to handle both styles
41
39
  all_attrs = attributes.is_a?(Hash) ? attributes.merge(kwargs) : kwargs
42
-
40
+
43
41
  # Norairrecord::Table expects field names as STRING keys
44
42
  # We need to convert Ruby attribute names to Airtable field names
45
43
  mapped_attrs = {}
@@ -47,7 +45,7 @@ module AirctiveRecord
47
45
  field_name = self.class.field_mappings[key.to_s] || key.to_s
48
46
  mapped_attrs[field_name.to_s] = value # Ensure string keys
49
47
  end
50
-
48
+
51
49
  # Call norairrecord's initialize properly
52
50
  if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("3.0.0")
53
51
  if mapped_attrs.empty?
@@ -58,7 +56,7 @@ module AirctiveRecord
58
56
  else
59
57
  super(mapped_attrs, id: id, created_at: created_at)
60
58
  end
61
-
59
+
62
60
  clear_changes_information
63
61
  end
64
62
 
@@ -68,12 +66,12 @@ module AirctiveRecord
68
66
 
69
67
  def serializable_fields
70
68
  # exclude readonly fields from serialization
71
- fields.reject { |k, v| self.class.readonly_fields.include?(k) }
69
+ fields.reject { |k, _v| self.class.readonly_fields.include?(k) }
72
70
  end
73
71
 
74
72
  def save(**options)
75
73
  return false unless valid?
76
-
74
+
77
75
  begin
78
76
  run_callbacks :save do
79
77
  if new_record?
@@ -88,13 +86,14 @@ module AirctiveRecord
88
86
  end
89
87
  changes_applied
90
88
  true
91
- rescue => e
89
+ rescue StandardError
92
90
  false
93
91
  end
94
92
  end
95
93
 
96
94
  def save!(**options)
97
95
  raise RecordInvalid, errors.full_messages.join(", ") unless valid?
96
+
98
97
  save(**options) || raise(RecordNotSaved, "Failed to save record")
99
98
  end
100
99
 
@@ -110,6 +109,7 @@ module AirctiveRecord
110
109
 
111
110
  def reload
112
111
  return self if new_record?
112
+
113
113
  reloaded = self.class.find(id)
114
114
  @fields = reloaded.fields
115
115
  @created_at = reloaded.created_at
@@ -117,20 +117,12 @@ module AirctiveRecord
117
117
  self
118
118
  end
119
119
 
120
- def persisted?
121
- !new_record?
122
- end
120
+ def persisted? = !new_record?
123
121
 
124
- def to_param
125
- id
126
- end
122
+ def to_param = id
127
123
 
128
- def to_key
129
- persisted? ? [id] : nil
130
- end
124
+ def to_key = persisted? ? [id] : nil
131
125
 
132
- def to_model
133
- self
134
- end
126
+ def to_model = self
135
127
  end
136
128
  end
@@ -18,11 +18,9 @@ module AirctiveRecord
18
18
  # override to use field mappings when building airtable params
19
19
  def to_airtable_params
20
20
  params = {}
21
-
21
+
22
22
  # filter
23
- if @where_clause.any?
24
- params[:filter] = @where_clause.to_airtable_formula
25
- end
23
+ params[:filter] = @where_clause.to_airtable_formula if @where_clause.any?
26
24
 
27
25
  # sort - use field mappings
28
26
  if @order_values.any?
@@ -34,7 +32,7 @@ module AirctiveRecord
34
32
 
35
33
  # limit
36
34
  params[:max_records] = @limit_value if @limit_value
37
-
35
+
38
36
  # offset
39
37
  params[:offset] = @offset_value if @offset_value
40
38
 
@@ -5,22 +5,18 @@ module AirctiveRecord
5
5
  extend ActiveSupport::Concern
6
6
 
7
7
  class_methods do
8
- # returns a chainable Relation object (model-specific!)
9
- def all
10
- relation_class.new(self)
11
- end
8
+ # returns a chainable Relation object
9
+ def all = relation_class.new(self)
12
10
 
13
11
  # scopes are defined on the model's specific Relation class
14
12
  def scope(name, body)
15
- unless body.respond_to?(:call)
16
- raise ArgumentError, "The scope body needs to be callable."
17
- end
13
+ raise ArgumentError, "The scope body needs to be callable." unless body.respond_to?(:call)
18
14
 
19
15
  # define on the class
20
16
  singleton_class.send(:define_method, name) do |*args|
21
17
  all.public_send(name, *args)
22
18
  end
23
-
19
+
24
20
  # define on this model's Relation class
25
21
  relation_class.send(:define_method, name) do |*args|
26
22
  instance_exec(*args, &body)
@@ -28,41 +24,23 @@ module AirctiveRecord
28
24
  end
29
25
 
30
26
  # delegate finder methods to all
31
- def where(conditions)
32
- all.where(conditions)
33
- end
27
+ def where(conditions) = all.where(conditions)
34
28
 
35
- def order(*args)
36
- all.order(*args)
37
- end
29
+ def order(*args) = all.order(*args)
38
30
 
39
- def limit(value)
40
- all.limit(value)
41
- end
31
+ def limit(value) = all.limit(value)
42
32
 
43
- def offset(value)
44
- all.offset(value)
45
- end
33
+ def offset(value) = all.offset(value)
46
34
 
47
- def find_by(conditions)
48
- all.find_by(conditions)
49
- end
35
+ def find_by(conditions) = all.find_by(conditions)
50
36
 
51
- def find_by!(conditions)
52
- all.find_by!(conditions)
53
- end
37
+ def find_by!(conditions) = all.find_by!(conditions)
54
38
 
55
- def first(limit = nil)
56
- all.first(limit)
57
- end
39
+ def first(limit = nil) = all.first(limit)
58
40
 
59
- def last(limit = nil)
60
- all.last(limit)
61
- end
41
+ def last(limit = nil) = all.last(limit)
62
42
 
63
- def count
64
- all.count
65
- end
43
+ def count = all.count
66
44
  end
67
45
  end
68
46
  end
@@ -18,6 +18,7 @@ module AirctiveRecord
18
18
 
19
19
  def save(**options)
20
20
  return false unless valid?
21
+
21
22
  super
22
23
  end
23
24
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AirctiveRecord
4
- VERSION = "0.2.0"
4
+ VERSION = "0.2.1"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: airctiverecord
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - 24c02
@@ -9,20 +9,6 @@ bindir: exe
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
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
12
  - !ruby/object:Gem::Dependency
27
13
  name: activemodel
28
14
  requirement: !ruby/object:Gem::Requirement
@@ -65,6 +51,20 @@ dependencies:
65
51
  - - ">="
66
52
  - !ruby/object:Gem::Version
67
53
  version: 0.2.0
54
+ - !ruby/object:Gem::Dependency
55
+ name: norairrecord
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '0.5'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '0.5'
68
68
  description: Provides a familiar ActiveRecord-like interface for Airtable, built on
69
69
  top of norairrecord with validations, callbacks, and associations.
70
70
  email: