filemaker 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|