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.
- checksums.yaml +7 -0
- data/.idea/airctiverecord.iml +136 -0
- data/.idea/modules.xml +8 -0
- data/.idea/vcs.xml +6 -0
- data/.idea/workspace.xml +81 -0
- data/README.md +341 -0
- data/Rakefile +12 -0
- data/examples/associations_example.rb +88 -0
- data/examples/basic_usage.rb +91 -0
- data/examples/boolean_fields.rb +52 -0
- data/examples/chainable_queries.rb +60 -0
- data/examples/field_mapping_example.rb +101 -0
- data/examples/readonly_fields.rb +52 -0
- data/examples/scope_isolation.rb +67 -0
- data/lib/airctiverecord/associations.rb +48 -0
- data/lib/airctiverecord/attribute_methods.rb +98 -0
- data/lib/airctiverecord/base.rb +136 -0
- data/lib/airctiverecord/callbacks.rb +20 -0
- data/lib/airctiverecord/relation.rb +54 -0
- data/lib/airctiverecord/scoping.rb +68 -0
- data/lib/airctiverecord/validations.rb +24 -0
- data/lib/airctiverecord/version.rb +5 -0
- data/lib/airctiverecord.rb +20 -0
- data/sig/airctiverecord.rbs +4 -0
- metadata +121 -0
|
@@ -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,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
|
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: []
|