filemaker 0.0.1 → 0.0.2
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 +4 -4
- data/README.md +129 -17
- data/filemaker.gemspec +1 -0
- data/lib/filemaker.rb +47 -0
- data/lib/filemaker/api/query_commands/findquery.rb +20 -3
- data/lib/filemaker/configuration.rb +1 -1
- data/lib/filemaker/core_ext/hash.rb +19 -15
- data/lib/filemaker/error.rb +5 -0
- data/lib/filemaker/metadata/field.rb +21 -5
- data/lib/filemaker/model.rb +132 -0
- data/lib/filemaker/model/builder.rb +52 -0
- data/lib/filemaker/model/components.rb +25 -0
- data/lib/filemaker/model/criteria.rb +101 -0
- data/lib/filemaker/model/field.rb +38 -0
- data/lib/filemaker/model/fields.rb +80 -0
- data/lib/filemaker/model/findable.rb +35 -0
- data/lib/filemaker/model/optional.rb +69 -0
- data/lib/filemaker/model/pagination.rb +41 -0
- data/lib/filemaker/model/persistable.rb +96 -0
- data/lib/filemaker/model/relations.rb +72 -0
- data/lib/filemaker/model/relations/belongs_to.rb +30 -0
- data/lib/filemaker/model/relations/has_many.rb +79 -0
- data/lib/filemaker/model/relations/proxy.rb +35 -0
- data/lib/filemaker/model/selectable.rb +146 -0
- data/lib/filemaker/railtie.rb +17 -0
- data/lib/filemaker/record.rb +25 -0
- data/lib/filemaker/resultset.rb +12 -4
- data/lib/filemaker/server.rb +7 -5
- data/lib/filemaker/version.rb +1 -1
- data/spec/filemaker/api/query_commands/compound_find_spec.rb +13 -1
- data/spec/filemaker/configuration_spec.rb +23 -0
- data/spec/filemaker/layout_spec.rb +0 -1
- data/spec/filemaker/model/criteria_spec.rb +304 -0
- data/spec/filemaker/model/relations_spec.rb +85 -0
- data/spec/filemaker/model_spec.rb +73 -0
- data/spec/filemaker/record_spec.rb +12 -1
- data/spec/spec_helper.rb +1 -0
- data/spec/support/filemaker.yml +13 -0
- data/spec/support/models.rb +38 -0
- metadata +44 -2
@@ -0,0 +1,52 @@
|
|
1
|
+
module Filemaker
|
2
|
+
module Model
|
3
|
+
module Builder
|
4
|
+
module_function
|
5
|
+
|
6
|
+
# Given an array of resultset, build out the exact same number of model
|
7
|
+
# objects.
|
8
|
+
def collection(resultset, klass)
|
9
|
+
models = []
|
10
|
+
|
11
|
+
resultset.each do |record|
|
12
|
+
object = klass.new
|
13
|
+
|
14
|
+
object.instance_variable_set('@new_record', false)
|
15
|
+
object.instance_variable_set('@record_id', record.record_id)
|
16
|
+
object.instance_variable_set('@mod_id', record.mod_id)
|
17
|
+
|
18
|
+
record.keys.each do |fm_field_name|
|
19
|
+
# record.keys are all lowercase
|
20
|
+
field = klass.find_field_by_name(fm_field_name)
|
21
|
+
next unless field
|
22
|
+
|
23
|
+
object.public_send("#{field.name}=", record[fm_field_name])
|
24
|
+
end
|
25
|
+
|
26
|
+
models << object
|
27
|
+
end
|
28
|
+
|
29
|
+
models
|
30
|
+
end
|
31
|
+
|
32
|
+
def single(resultset, klass)
|
33
|
+
record = resultset.first
|
34
|
+
object = klass.new
|
35
|
+
|
36
|
+
object.instance_variable_set('@new_record', false)
|
37
|
+
object.instance_variable_set('@record_id', record.record_id)
|
38
|
+
object.instance_variable_set('@mod_id', record.mod_id)
|
39
|
+
|
40
|
+
record.keys.each do |fm_field_name|
|
41
|
+
# record.keys are all lowercase
|
42
|
+
field = klass.find_field_by_name(fm_field_name)
|
43
|
+
next unless field
|
44
|
+
|
45
|
+
object.public_send("#{field.name}=", record[fm_field_name])
|
46
|
+
end
|
47
|
+
|
48
|
+
object
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'filemaker/model/fields'
|
2
|
+
require 'filemaker/model/findable'
|
3
|
+
require 'filemaker/model/relations'
|
4
|
+
require 'filemaker/model/persistable'
|
5
|
+
|
6
|
+
module Filemaker
|
7
|
+
module Model
|
8
|
+
module Components
|
9
|
+
extend ActiveSupport::Concern
|
10
|
+
|
11
|
+
included do
|
12
|
+
extend Findable
|
13
|
+
extend ActiveModel::Callbacks
|
14
|
+
end
|
15
|
+
|
16
|
+
include ActiveModel::Model
|
17
|
+
include ActiveModel::Serializers::JSON
|
18
|
+
include ActiveModel::Serializers::Xml
|
19
|
+
include ActiveModel::Validations::Callbacks
|
20
|
+
include Fields
|
21
|
+
include Relations
|
22
|
+
include Persistable
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
require 'filemaker/model/selectable'
|
2
|
+
require 'filemaker/model/optional'
|
3
|
+
require 'filemaker/model/builder'
|
4
|
+
require 'filemaker/model/pagination'
|
5
|
+
|
6
|
+
module Filemaker
|
7
|
+
module Model
|
8
|
+
# Criteria encapsulates query arguments and options to represent a single
|
9
|
+
# query. It has convenient query DSL like +where+ and +in+ to represent both
|
10
|
+
# -find and -findquery FileMaker query. On top of that you can negate any
|
11
|
+
# query with the +not+ clause to omit selection.
|
12
|
+
class Criteria
|
13
|
+
include Enumerable
|
14
|
+
include Selectable
|
15
|
+
include Optional
|
16
|
+
include Pagination
|
17
|
+
|
18
|
+
# @return [Filemaker::Model] the class of the model
|
19
|
+
attr_reader :klass
|
20
|
+
|
21
|
+
# @return [Hash. Array] represents the query arguments
|
22
|
+
attr_reader :selector
|
23
|
+
|
24
|
+
# @return [Hash] options like skip, limit and order
|
25
|
+
attr_reader :options
|
26
|
+
|
27
|
+
# @return [Array] keep track of where clause and in clause to not mix them
|
28
|
+
attr_reader :chains
|
29
|
+
|
30
|
+
def initialize(klass)
|
31
|
+
@klass = klass
|
32
|
+
@options = {}
|
33
|
+
@chains = []
|
34
|
+
@_page = 1
|
35
|
+
end
|
36
|
+
|
37
|
+
def to_s
|
38
|
+
"#{selector}, #{options}"
|
39
|
+
end
|
40
|
+
|
41
|
+
def each
|
42
|
+
execute.each { |record| yield record } if block_given?
|
43
|
+
end
|
44
|
+
|
45
|
+
def first
|
46
|
+
limit(1).execute.first
|
47
|
+
end
|
48
|
+
|
49
|
+
def all
|
50
|
+
execute
|
51
|
+
end
|
52
|
+
|
53
|
+
def limit?
|
54
|
+
!options[:max].nil?
|
55
|
+
end
|
56
|
+
|
57
|
+
# The count this criteria is capable of returning
|
58
|
+
#
|
59
|
+
# @return [Integer] the count
|
60
|
+
def count
|
61
|
+
limit(0)
|
62
|
+
if chains.include?(:where)
|
63
|
+
klass.api.find(selector, options).count
|
64
|
+
elsif chains.include?(:in)
|
65
|
+
klass.api.query(selector, options).count
|
66
|
+
else
|
67
|
+
klass.api.findall(options).count
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
protected
|
72
|
+
|
73
|
+
def execute
|
74
|
+
resultset = []
|
75
|
+
paginated = chains.include?(:page)
|
76
|
+
|
77
|
+
if chains.include?(:where)
|
78
|
+
# Use -find
|
79
|
+
resultset = klass.api.find(selector, options)
|
80
|
+
elsif chains.include?(:in)
|
81
|
+
# Use -findquery
|
82
|
+
resultset = klass.api.query(selector, options)
|
83
|
+
else
|
84
|
+
# Use -findall
|
85
|
+
limit(1) unless limit?
|
86
|
+
resultset = klass.api.findall(options)
|
87
|
+
end
|
88
|
+
|
89
|
+
models = Filemaker::Model::Builder.collection(resultset, klass)
|
90
|
+
|
91
|
+
if defined?(Kaminari) && paginated
|
92
|
+
Kaminari.paginate_array(models, resultset.count)
|
93
|
+
.page(@_page)
|
94
|
+
.per(options[:max])
|
95
|
+
else
|
96
|
+
models
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Filemaker
|
2
|
+
module Model
|
3
|
+
class Field
|
4
|
+
attr_reader :name, :type, :default_value, :fm_name
|
5
|
+
|
6
|
+
def initialize(name, type, options = {})
|
7
|
+
@name = name
|
8
|
+
@type = type
|
9
|
+
@default_value = coerce(options.fetch(:default) { nil })
|
10
|
+
|
11
|
+
# We need to downcase because Filemaker::Record is
|
12
|
+
# HashWithIndifferentAndCaseInsensitiveAccess
|
13
|
+
@fm_name = (options.fetch(:fm_name) { name }).to_s.downcase
|
14
|
+
end
|
15
|
+
|
16
|
+
# From FileMaker to Ruby
|
17
|
+
def coerce(value)
|
18
|
+
return nil if value.nil?
|
19
|
+
|
20
|
+
if @type == String
|
21
|
+
value.to_s
|
22
|
+
elsif @type == Integer
|
23
|
+
value.to_i
|
24
|
+
elsif @type == BigDecimal
|
25
|
+
BigDecimal.new(value.to_s)
|
26
|
+
elsif @type == Date
|
27
|
+
return value if value.is_a? Date
|
28
|
+
Date.parse(value.to_s)
|
29
|
+
elsif @type == DateTime
|
30
|
+
return value if value.is_a? DateTime
|
31
|
+
DateTime.parse(value.to_s)
|
32
|
+
else
|
33
|
+
value
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'filemaker/model/field'
|
2
|
+
|
3
|
+
module Filemaker
|
4
|
+
module Model
|
5
|
+
module Fields
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
TYPE_MAPPINGS = {
|
9
|
+
string: String,
|
10
|
+
date: Date,
|
11
|
+
datetime: DateTime,
|
12
|
+
money: BigDecimal,
|
13
|
+
integer: Integer,
|
14
|
+
number: BigDecimal
|
15
|
+
}
|
16
|
+
|
17
|
+
included do
|
18
|
+
class_attribute :fields, :identity
|
19
|
+
self.fields = {}
|
20
|
+
end
|
21
|
+
|
22
|
+
def apply_defaults
|
23
|
+
attribute_names.each do |name|
|
24
|
+
field = fields[name]
|
25
|
+
attributes[name] = field.default_value
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def attribute_names
|
30
|
+
self.class.attribute_names
|
31
|
+
end
|
32
|
+
|
33
|
+
def fm_names
|
34
|
+
fields.values.map(&:fm_name)
|
35
|
+
end
|
36
|
+
|
37
|
+
module ClassMethods
|
38
|
+
def attribute_names
|
39
|
+
fields.keys
|
40
|
+
end
|
41
|
+
|
42
|
+
%w(string date datetime money integer number).each do |type|
|
43
|
+
define_method(type) do |*args|
|
44
|
+
# TODO: It will be good if we can accept lambda also
|
45
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
46
|
+
field_names = args
|
47
|
+
|
48
|
+
field_names.each do |name|
|
49
|
+
add_field(name, TYPE_MAPPINGS[type.to_sym], options)
|
50
|
+
create_accessors(name)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def add_field(name, type, options)
|
56
|
+
fields[name] = Filemaker::Model::Field.new(name, type, options)
|
57
|
+
self.identity = fields[name] if options[:identity]
|
58
|
+
end
|
59
|
+
|
60
|
+
def create_accessors(name)
|
61
|
+
define_method(name) { attributes[name] }
|
62
|
+
define_method("#{name}=") do |value|
|
63
|
+
attributes[name] = fields[name].coerce(value)
|
64
|
+
end
|
65
|
+
define_method("#{name}?") do
|
66
|
+
attributes[name] == true || attributes[name].present?
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Find FileMaker's real name given either the attribute name or the real
|
71
|
+
# FileMaker name.
|
72
|
+
def find_field_by_name(name)
|
73
|
+
fields.values.find do |f|
|
74
|
+
f.name == name.to_sym || f.fm_name == name.to_s
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Filemaker
|
2
|
+
module Model
|
3
|
+
module Findable
|
4
|
+
delegate \
|
5
|
+
:limit,
|
6
|
+
:skip,
|
7
|
+
:order,
|
8
|
+
:find,
|
9
|
+
:first,
|
10
|
+
:recid,
|
11
|
+
:in,
|
12
|
+
:not_in,
|
13
|
+
:or,
|
14
|
+
:eq,
|
15
|
+
:cn,
|
16
|
+
:bw,
|
17
|
+
:ew,
|
18
|
+
:gt,
|
19
|
+
:gte,
|
20
|
+
:lt,
|
21
|
+
:lte,
|
22
|
+
:neq,
|
23
|
+
:equals,
|
24
|
+
:contains,
|
25
|
+
:begins_with,
|
26
|
+
:ends_with,
|
27
|
+
:not,
|
28
|
+
:where, to: :criteria
|
29
|
+
|
30
|
+
def criteria
|
31
|
+
Criteria.new(self)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module Filemaker
|
2
|
+
module Model
|
3
|
+
module Optional
|
4
|
+
# Number of records to skip. For pagination.
|
5
|
+
#
|
6
|
+
# @param [Integer] value The number to skip.
|
7
|
+
#
|
8
|
+
# @return [Filemaker::Model::Criteria]
|
9
|
+
def skip(value)
|
10
|
+
return self if value.nil?
|
11
|
+
options[:skip] = value.to_i
|
12
|
+
yield options if block_given?
|
13
|
+
self
|
14
|
+
end
|
15
|
+
|
16
|
+
# Limit the number of records returned.
|
17
|
+
#
|
18
|
+
# @param [Integer] value The number of records to return.
|
19
|
+
#
|
20
|
+
# @return [Filemaker::Model::Criteria]
|
21
|
+
def limit(value)
|
22
|
+
return self if value.nil?
|
23
|
+
options[:max] = value.to_i
|
24
|
+
yield options if block_given?
|
25
|
+
self
|
26
|
+
end
|
27
|
+
|
28
|
+
# Order the records. Model field name will be converted to real FileMaker
|
29
|
+
# field name.
|
30
|
+
#
|
31
|
+
# @example Sort is position aware!
|
32
|
+
# criteria.order('name desc, email')
|
33
|
+
#
|
34
|
+
# @param [String] value The sorting string
|
35
|
+
#
|
36
|
+
# @return [Filemaker::Model::Criteria]
|
37
|
+
def order(value)
|
38
|
+
return self if value.nil?
|
39
|
+
sortfield = []
|
40
|
+
sortorder = []
|
41
|
+
sort_spec = value.split(',').map(&:strip)
|
42
|
+
|
43
|
+
sort_spec.each do |spec|
|
44
|
+
fieldname, direction = spec.split(' ')
|
45
|
+
direction = 'asc' unless direction
|
46
|
+
|
47
|
+
field = klass.find_field_by_name(fieldname)
|
48
|
+
|
49
|
+
next unless field
|
50
|
+
|
51
|
+
direction = 'ascend' if direction.downcase == 'asc'
|
52
|
+
direction = 'descend' if direction.downcase == 'desc'
|
53
|
+
|
54
|
+
sortfield << field.fm_name
|
55
|
+
sortorder << direction
|
56
|
+
end
|
57
|
+
|
58
|
+
unless sortfield.empty?
|
59
|
+
options[:sortfield] = sortfield
|
60
|
+
options[:sortorder] = sortorder
|
61
|
+
end
|
62
|
+
|
63
|
+
yield options if block_given?
|
64
|
+
|
65
|
+
self
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Filemaker
|
2
|
+
module Model
|
3
|
+
module Pagination
|
4
|
+
# Calling `page` will trigger pagination.
|
5
|
+
def page(value)
|
6
|
+
chains << :page
|
7
|
+
@_page = value.to_i
|
8
|
+
update_skip
|
9
|
+
end
|
10
|
+
|
11
|
+
def per(value)
|
12
|
+
limit(value)
|
13
|
+
update_skip
|
14
|
+
end
|
15
|
+
|
16
|
+
# A simple getter to retrieve the current page value. If no one set it up
|
17
|
+
# through the `page(4)` way, then at least it defaults to 1.
|
18
|
+
def __page
|
19
|
+
@_page || 1
|
20
|
+
end
|
21
|
+
|
22
|
+
# A simple getter to retrieve the limit value. It will default to
|
23
|
+
# Model.per_page
|
24
|
+
#
|
25
|
+
# Will have stacklevel too deep if we have `per(nil)`. Somehow, the
|
26
|
+
# `per_page` must be set either at the `Model.per_page`,
|
27
|
+
# `Kaminari.config.default_per_page`, or right here where I just throw a
|
28
|
+
# 25 value at it.
|
29
|
+
def __per
|
30
|
+
per(klass.per_page || 25) unless limit?
|
31
|
+
options[:max]
|
32
|
+
end
|
33
|
+
|
34
|
+
def update_skip
|
35
|
+
skip = (__page - 1) * __per
|
36
|
+
skip(skip) unless skip.zero?
|
37
|
+
self
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|