lockstep_rails 0.3.22
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/MIT-LICENSE +20 -0
- data/README.md +216 -0
- data/Rakefile +31 -0
- data/app/assets/config/lockstep_rails_manifest.js +0 -0
- data/app/concepts/lockstep/active_records/association.rb +78 -0
- data/app/concepts/lockstep/api_record.rb +1118 -0
- data/app/concepts/lockstep/api_records/scopes.rb +20 -0
- data/app/concepts/lockstep/client.rb +162 -0
- data/app/concepts/lockstep/error.rb +50 -0
- data/app/concepts/lockstep/exceptions.rb +4 -0
- data/app/concepts/lockstep/query.rb +409 -0
- data/app/concepts/lockstep/query_methods.rb +75 -0
- data/app/concepts/lockstep/relation_array.rb +100 -0
- data/app/helpers/types.rb +3 -0
- data/app/models/lockstep/account.rb +15 -0
- data/app/models/lockstep/connection.rb +19 -0
- data/app/models/lockstep/contact.rb +9 -0
- data/app/models/lockstep/customer_summary.rb +6 -0
- data/app/models/lockstep/invoice.rb +7 -0
- data/app/models/lockstep/invoice_summary.rb +6 -0
- data/app/models/lockstep/invoices/address.rb +24 -0
- data/app/models/lockstep/note.rb +7 -0
- data/app/models/lockstep/payment.rb +7 -0
- data/app/models/lockstep/payment_summary.rb +6 -0
- data/app/models/lockstep/user.rb +10 -0
- data/app/platform_api/model_template.rb.erb +27 -0
- data/app/platform_api/schema/action_result.rb +15 -0
- data/app/platform_api/schema/activity.rb +141 -0
- data/app/platform_api/schema/activity_fetch_result.rb +26 -0
- data/app/platform_api/schema/activity_stream_item.rb +58 -0
- data/app/platform_api/schema/activity_x_ref.rb +37 -0
- data/app/platform_api/schema/aging.rb +24 -0
- data/app/platform_api/schema/api_key.rb +71 -0
- data/app/platform_api/schema/api_key_fetch_result.rb +26 -0
- data/app/platform_api/schema/app_enrollment.rb +88 -0
- data/app/platform_api/schema/app_enrollment_custom_field.rb +67 -0
- data/app/platform_api/schema/app_enrollment_custom_field_fetch_result.rb +26 -0
- data/app/platform_api/schema/app_enrollment_fetch_result.rb +26 -0
- data/app/platform_api/schema/application.rb +89 -0
- data/app/platform_api/schema/application_fetch_result.rb +26 -0
- data/app/platform_api/schema/ar_aging_header_info.rb +47 -0
- data/app/platform_api/schema/ar_header_info.rb +118 -0
- data/app/platform_api/schema/assembly.rb +68 -0
- data/app/platform_api/schema/at_risk_invoice_summary.rb +90 -0
- data/app/platform_api/schema/at_risk_invoice_summary_fetch_result.rb +26 -0
- data/app/platform_api/schema/attachment.rb +92 -0
- data/app/platform_api/schema/attachment_fetch_result.rb +26 -0
- data/app/platform_api/schema/attachment_header_info.rb +41 -0
- data/app/platform_api/schema/batch_sync.rb +18 -0
- data/app/platform_api/schema/bulk_currency_conversion.rb +19 -0
- data/app/platform_api/schema/cashflow_report.rb +35 -0
- data/app/platform_api/schema/code_definition.rb +58 -0
- data/app/platform_api/schema/code_definition_fetch_result.rb +26 -0
- data/app/platform_api/schema/company.rb +243 -0
- data/app/platform_api/schema/company_fetch_result.rb +26 -0
- data/app/platform_api/schema/company_sync.rb +142 -0
- data/app/platform_api/schema/connector_info.rb +27 -0
- data/app/platform_api/schema/constructor_info.rb +128 -0
- data/app/platform_api/schema/contact.rb +148 -0
- data/app/platform_api/schema/contact_fetch_result.rb +26 -0
- data/app/platform_api/schema/contact_sync.rb +109 -0
- data/app/platform_api/schema/country.rb +62 -0
- data/app/platform_api/schema/country_fetch_result.rb +26 -0
- data/app/platform_api/schema/credit_memo_applied.rb +104 -0
- data/app/platform_api/schema/credit_memo_applied_fetch_result.rb +26 -0
- data/app/platform_api/schema/credit_memo_applied_sync.rb +67 -0
- data/app/platform_api/schema/credit_memo_invoice.rb +77 -0
- data/app/platform_api/schema/currency.rb +31 -0
- data/app/platform_api/schema/currency_fetch_result.rb +26 -0
- data/app/platform_api/schema/currency_rate.rb +28 -0
- data/app/platform_api/schema/custom_attribute_data.rb +18 -0
- data/app/platform_api/schema/custom_attribute_named_argument.rb +24 -0
- data/app/platform_api/schema/custom_attribute_typed_argument.rb +16 -0
- data/app/platform_api/schema/custom_field_definition.rb +76 -0
- data/app/platform_api/schema/custom_field_definition_fetch_result.rb +26 -0
- data/app/platform_api/schema/custom_field_sync.rb +71 -0
- data/app/platform_api/schema/custom_field_value.rb +75 -0
- data/app/platform_api/schema/custom_field_value_fetch_result.rb +26 -0
- data/app/platform_api/schema/customer_details.rb +98 -0
- data/app/platform_api/schema/customer_details_payment.rb +60 -0
- data/app/platform_api/schema/customer_summary.rb +88 -0
- data/app/platform_api/schema/customer_summary_fetch_result.rb +26 -0
- data/app/platform_api/schema/daily_sales_outstanding_report.rb +25 -0
- data/app/platform_api/schema/developer_account_submit.rb +23 -0
- data/app/platform_api/schema/email.rb +160 -0
- data/app/platform_api/schema/email_fetch_result.rb +26 -0
- data/app/platform_api/schema/erp.rb +23 -0
- data/app/platform_api/schema/erp_fetch_result.rb +26 -0
- data/app/platform_api/schema/erp_info.rb +18 -0
- data/app/platform_api/schema/erp_info_data.rb +23 -0
- data/app/platform_api/schema/event_info.rb +59 -0
- data/app/platform_api/schema/exception.rb +41 -0
- data/app/platform_api/schema/field_info.rb +105 -0
- data/app/platform_api/schema/financial_account.rb +90 -0
- data/app/platform_api/schema/financial_account_balance_history.rb +90 -0
- data/app/platform_api/schema/financial_account_balance_history_fetch_result.rb +26 -0
- data/app/platform_api/schema/financial_account_fetch_result.rb +26 -0
- data/app/platform_api/schema/financial_year_setting.rb +74 -0
- data/app/platform_api/schema/financial_year_setting_fetch_result.rb +26 -0
- data/app/platform_api/schema/invite.rb +25 -0
- data/app/platform_api/schema/invite_data.rb +18 -0
- data/app/platform_api/schema/invite_submit.rb +15 -0
- data/app/platform_api/schema/invoice.rb +226 -0
- data/app/platform_api/schema/invoice_address.rb +97 -0
- data/app/platform_api/schema/invoice_fetch_result.rb +29 -0
- data/app/platform_api/schema/invoice_history.rb +188 -0
- data/app/platform_api/schema/invoice_history_fetch_result.rb +26 -0
- data/app/platform_api/schema/invoice_line.rb +142 -0
- data/app/platform_api/schema/invoice_line_sync.rb +208 -0
- data/app/platform_api/schema/invoice_payment_detail.rb +65 -0
- data/app/platform_api/schema/invoice_summary.rb +85 -0
- data/app/platform_api/schema/invoice_summary_fetch_result.rb +26 -0
- data/app/platform_api/schema/invoice_sync.rb +280 -0
- data/app/platform_api/schema/lead.rb +32 -0
- data/app/platform_api/schema/member_info.rb +36 -0
- data/app/platform_api/schema/method_base.rb +128 -0
- data/app/platform_api/schema/method_info.rb +137 -0
- data/app/platform_api/schema/module.rb +44 -0
- data/app/platform_api/schema/module_handle.rb +15 -0
- data/app/platform_api/schema/note.rb +72 -0
- data/app/platform_api/schema/note_fetch_result.rb +26 -0
- data/app/platform_api/schema/parameter_info.rb +64 -0
- data/app/platform_api/schema/payment.rb +147 -0
- data/app/platform_api/schema/payment_applied.rb +95 -0
- data/app/platform_api/schema/payment_applied_fetch_result.rb +26 -0
- data/app/platform_api/schema/payment_applied_sync.rb +74 -0
- data/app/platform_api/schema/payment_detail.rb +110 -0
- data/app/platform_api/schema/payment_detail_fetch_result.rb +26 -0
- data/app/platform_api/schema/payment_detail_header.rb +43 -0
- data/app/platform_api/schema/payment_fetch_result.rb +26 -0
- data/app/platform_api/schema/payment_summary.rb +79 -0
- data/app/platform_api/schema/payment_summary_fetch_result.rb +26 -0
- data/app/platform_api/schema/payment_sync.rb +112 -0
- data/app/platform_api/schema/problem_details.rb +31 -0
- data/app/platform_api/schema/property_info.rb +60 -0
- data/app/platform_api/schema/provisioning.rb +28 -0
- data/app/platform_api/schema/provisioning_finalize_request.rb +28 -0
- data/app/platform_api/schema/provisioning_response.rb +42 -0
- data/app/platform_api/schema/risk_rate.rb +57 -0
- data/app/platform_api/schema/runtime_field_handle.rb +13 -0
- data/app/platform_api/schema/runtime_method_handle.rb +13 -0
- data/app/platform_api/schema/runtime_type_handle.rb +13 -0
- data/app/platform_api/schema/state.rb +22 -0
- data/app/platform_api/schema/state_fetch_result.rb +26 -0
- data/app/platform_api/schema/status.rb +72 -0
- data/app/platform_api/schema/struct_layout_attribute.rb +16 -0
- data/app/platform_api/schema/sync_entity_result.rb +34 -0
- data/app/platform_api/schema/sync_request.rb +71 -0
- data/app/platform_api/schema/sync_request_fetch_result.rb +26 -0
- data/app/platform_api/schema/sync_submit.rb +15 -0
- data/app/platform_api/schema/test_argument_exception.rb +41 -0
- data/app/platform_api/schema/test_timeout_exception.rb +41 -0
- data/app/platform_api/schema/transfer_owner.rb +16 -0
- data/app/platform_api/schema/transfer_owner_submit.rb +15 -0
- data/app/platform_api/schema/type.rb +278 -0
- data/app/platform_api/schema/type_info.rb +287 -0
- data/app/platform_api/schema/uri.rb +15 -0
- data/app/platform_api/schema/user_account.rb +152 -0
- data/app/platform_api/schema/user_account_fetch_result.rb +26 -0
- data/app/platform_api/schema/user_role.rb +50 -0
- data/app/platform_api/schema/user_role_fetch_result.rb +26 -0
- data/app/platform_api/schema/webhook.rb +96 -0
- data/app/platform_api/schema/webhook_fetch_result.rb +26 -0
- data/app/platform_api/schema/webhook_history_table_storage.rb +64 -0
- data/app/platform_api/schema/webhook_history_table_storage_fetch_result.rb +26 -0
- data/app/platform_api/swagger.json +22056 -0
- data/config/routes.rb +2 -0
- data/lib/lockstep_rails/engine.rb +4 -0
- data/lib/lockstep_rails/version.rb +3 -0
- data/lib/lockstep_rails.rb +5 -0
- data/lib/tasks/lockstep_rails_tasks.rake +4 -0
- data/lib/tasks/update_api_schema.rake +159 -0
- metadata +230 -0
@@ -0,0 +1,1118 @@
|
|
1
|
+
require "rubygems"
|
2
|
+
require "bundler/setup"
|
3
|
+
require "active_model"
|
4
|
+
require "erb"
|
5
|
+
require "dry-types"
|
6
|
+
require "json"
|
7
|
+
require "active_support/hash_with_indifferent_access"
|
8
|
+
require "lockstep/query"
|
9
|
+
require "lockstep/query_methods"
|
10
|
+
require "lockstep/error"
|
11
|
+
require "lockstep/exceptions"
|
12
|
+
require "lockstep/relation_array"
|
13
|
+
|
14
|
+
module Lockstep
|
15
|
+
class ApiRecord
|
16
|
+
include Lockstep::ApiRecords::Scopes
|
17
|
+
# Lockstep::ApiRecord provides an easy way to use Ruby to interace with a api.lockstep.io backend
|
18
|
+
# Usage:
|
19
|
+
# class Post < Lockstep::ApiRecord
|
20
|
+
# fields :title, :author, :body
|
21
|
+
# end
|
22
|
+
|
23
|
+
# @@has_many_relations = {}.with_indifferent_access
|
24
|
+
# @@belongs_to_relations = {}.with_indifferent_access
|
25
|
+
|
26
|
+
include ActiveModel::Validations
|
27
|
+
include ActiveModel::Validations::Callbacks
|
28
|
+
include ActiveModel::Conversion
|
29
|
+
include ActiveModel::AttributeMethods
|
30
|
+
extend ActiveModel::Naming
|
31
|
+
extend ActiveModel::Callbacks
|
32
|
+
HashWithIndifferentAccess = ActiveSupport::HashWithIndifferentAccess
|
33
|
+
|
34
|
+
attr_accessor :error_instances
|
35
|
+
|
36
|
+
define_model_callbacks :save, :create, :update, :destroy
|
37
|
+
|
38
|
+
validate :validate_enum
|
39
|
+
|
40
|
+
# Instantiates a Lockstep::ApiRecord object
|
41
|
+
#
|
42
|
+
# @params [Hash], [Boolean] a `Hash` of attributes and a `Boolean` that should be false only if the object already exists
|
43
|
+
# @return [Lockstep::ApiRecord] an object that subclasses `Parseresource::Base`
|
44
|
+
def initialize(attributes = {}, new = true)
|
45
|
+
#attributes = HashWithIndifferentAccess.new(attributes)
|
46
|
+
|
47
|
+
if new
|
48
|
+
@unsaved_attributes = attributes
|
49
|
+
@unsaved_attributes.stringify_keys!
|
50
|
+
else
|
51
|
+
@unsaved_attributes = {}
|
52
|
+
end
|
53
|
+
@attributes = {}
|
54
|
+
self.error_instances = []
|
55
|
+
|
56
|
+
attributes.each do |k, v|
|
57
|
+
# Typecast using dry-types
|
58
|
+
if (type = schema[k])
|
59
|
+
attributes[k] = type[v]
|
60
|
+
elsif v.present? and (enum = enum_config[k]).present? and enum.keys.include?(v.to_s)
|
61
|
+
attributes[k] = enum[v]
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
self.attributes.merge!(attributes)
|
66
|
+
self.attributes unless self.attributes.empty?
|
67
|
+
create_setters_and_getters!
|
68
|
+
end
|
69
|
+
|
70
|
+
def self.schema
|
71
|
+
@schema ||= {}.with_indifferent_access
|
72
|
+
end
|
73
|
+
|
74
|
+
def schema
|
75
|
+
self.class.schema
|
76
|
+
end
|
77
|
+
|
78
|
+
# Explicitly adds a field to the model.
|
79
|
+
#
|
80
|
+
# @param [Symbol] name the name of the field, eg `:author`.
|
81
|
+
# @param [Boolean] val the return value of the field. Only use this within the class.
|
82
|
+
def self.field(fname, type = nil)
|
83
|
+
schema[fname] = type
|
84
|
+
|
85
|
+
fname = fname.to_sym
|
86
|
+
class_eval do
|
87
|
+
define_method(fname) do
|
88
|
+
val = get_attribute("#{fname}")
|
89
|
+
|
90
|
+
# If enum, substitute with the enum key
|
91
|
+
if val.present? && (enum = enum_config[fname]).present?
|
92
|
+
val = enum.key(val)
|
93
|
+
end
|
94
|
+
|
95
|
+
val
|
96
|
+
end
|
97
|
+
end
|
98
|
+
unless self.respond_to? "#{fname}="
|
99
|
+
class_eval do
|
100
|
+
define_method("#{fname}=") do |val|
|
101
|
+
set_attribute("#{fname}", val)
|
102
|
+
|
103
|
+
val
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
# Add multiple fields in one line. Same as `#field`, but accepts multiple args.
|
110
|
+
#
|
111
|
+
# @param [Array] *args an array of `Symbol`s, `eg :author, :body, :title`.
|
112
|
+
def self.fields(*args)
|
113
|
+
args.each { |f| field(f) }
|
114
|
+
end
|
115
|
+
|
116
|
+
def belongs_to_relations
|
117
|
+
@belongs_to_relations ||= {}
|
118
|
+
end
|
119
|
+
|
120
|
+
# Similar to its ActiveRecord counterpart.
|
121
|
+
#
|
122
|
+
# @param [Hash] options Added so that you can specify :class_name => '...'. It does nothing at all, but helps you write self-documenting code.
|
123
|
+
# @primary_key is the attribute of the referenced :class_name
|
124
|
+
# @foreign_key is the attribute of the parent class
|
125
|
+
def self.belongs_to(parent, config = {})
|
126
|
+
config = config.with_indifferent_access
|
127
|
+
class_name = config[:class_name]
|
128
|
+
raise "Class name cannot be empty in #{parent}: #{self.name}" if class_name.blank?
|
129
|
+
|
130
|
+
included = config[:included] || false
|
131
|
+
primary_key = config[:primary_key]
|
132
|
+
foreign_key = config[:foreign_key]
|
133
|
+
polymorphic = config[:polymorphic]
|
134
|
+
loader = config[:loader]
|
135
|
+
|
136
|
+
primary_key ||= class_name.constantize.id_ref
|
137
|
+
foreign_key ||= class_name.constantize.id_ref
|
138
|
+
field(parent)
|
139
|
+
belongs_to_relations[parent] = {
|
140
|
+
name: parent, class_name: class_name,
|
141
|
+
included: included, primary_key: primary_key, foreign_key: foreign_key,
|
142
|
+
loader: loader, polymorphic: polymorphic,
|
143
|
+
}
|
144
|
+
|
145
|
+
# define_method("build_#{parent}") do |attributes_hash|
|
146
|
+
# build_belongs_to_association(parent, attributes_hash)
|
147
|
+
# end
|
148
|
+
end
|
149
|
+
|
150
|
+
# Creates setter and getter in order access the specified relation for this Model
|
151
|
+
#
|
152
|
+
# @param [Hash] options Added so that you can specify :class_name => '...'. It does nothing at all, but helps you write self-documenting code.
|
153
|
+
# @primary_key is the attribute of the parent_class
|
154
|
+
# @foreign_key is the attribute of the referenced :class_name
|
155
|
+
# @polymorphic is used to assign polymorphic association with the related ApiModel. It expects a hash with the key-value, or a lambda function which returns a hash
|
156
|
+
def self.has_many(parent, config = {})
|
157
|
+
config = config.with_indifferent_access
|
158
|
+
class_name = config[:class_name]
|
159
|
+
raise "Class name cannot be empty in #{parent}: #{self.name}" if class_name.blank?
|
160
|
+
|
161
|
+
included = config[:included] || false
|
162
|
+
primary_key = config[:primary_key]
|
163
|
+
foreign_key = config[:foreign_key]
|
164
|
+
polymorphic = config[:polymorphic]
|
165
|
+
loader = config[:loader]
|
166
|
+
|
167
|
+
primary_key ||= self.id_ref
|
168
|
+
foreign_key ||= self.id_ref
|
169
|
+
field(parent)
|
170
|
+
has_many_relations[parent] = {
|
171
|
+
name: parent, class_name: class_name, included: included,
|
172
|
+
primary_key: primary_key, foreign_key: foreign_key, polymorphic: polymorphic,
|
173
|
+
loader: loader,
|
174
|
+
}
|
175
|
+
end
|
176
|
+
|
177
|
+
def self.load_schema(schema)
|
178
|
+
schema.schema.each do |field, type|
|
179
|
+
field(field, type)
|
180
|
+
end
|
181
|
+
|
182
|
+
schema.belongs_to_relations.each do |relation, config|
|
183
|
+
params = {}
|
184
|
+
config.except(:name).each { |k, v| params[k.to_sym] = v }
|
185
|
+
self.belongs_to(relation, params)
|
186
|
+
end
|
187
|
+
|
188
|
+
schema.has_many_relations.each do |relation, config|
|
189
|
+
params = {}
|
190
|
+
config.except(:name).each { |k, v| params[k.to_sym] = v }
|
191
|
+
self.has_many(relation, params)
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
def to_pointer
|
196
|
+
klass_name = self.class.model_name.to_s
|
197
|
+
{ "__type" => "Pointer", "className" => klass_name.to_s, self.id_ref => self.id }
|
198
|
+
end
|
199
|
+
|
200
|
+
def self.to_date_object(date)
|
201
|
+
date = date.to_time if date.respond_to?(:to_time)
|
202
|
+
{ "__type" => "Date", "iso" => date.getutc.iso8601 } if date && (date.is_a?(Date) || date.is_a?(DateTime) || date.is_a?(Time))
|
203
|
+
end
|
204
|
+
|
205
|
+
# Creates setter methods for model fields
|
206
|
+
def create_setters!(k, v)
|
207
|
+
unless self.respond_to? "#{k}="
|
208
|
+
self.class.send(:define_method, "#{k}=") do |val|
|
209
|
+
set_attribute("#{k}", val)
|
210
|
+
|
211
|
+
val
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
def method_missing(method, *args, &block)
|
217
|
+
raise StandardError.new("#{method} has not been defined for #{self.class.name}")
|
218
|
+
# super
|
219
|
+
end
|
220
|
+
|
221
|
+
def self.method_missing(method_name, *args)
|
222
|
+
method_name = method_name.to_s
|
223
|
+
if method_name.start_with?("find_by_")
|
224
|
+
attrib = method_name.gsub(/^find_by_/, "")
|
225
|
+
finder_name = "find_all_by_#{attrib}"
|
226
|
+
|
227
|
+
define_singleton_method(finder_name) do |target_value|
|
228
|
+
where({ attrib.to_sym => target_value }).first
|
229
|
+
end
|
230
|
+
|
231
|
+
send(finder_name, args[0])
|
232
|
+
elsif method_name.start_with?("find_all_by_")
|
233
|
+
attrib = method_name.gsub(/^find_all_by_/, "")
|
234
|
+
finder_name = "find_all_by_#{attrib}"
|
235
|
+
|
236
|
+
define_singleton_method(finder_name) do |target_value|
|
237
|
+
where({ attrib.to_sym => target_value }).all
|
238
|
+
end
|
239
|
+
|
240
|
+
send(finder_name, args[0])
|
241
|
+
else
|
242
|
+
super(method_name.to_sym, *args)
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
# Creates getter methods for model fields
|
247
|
+
def create_getters!(k, v)
|
248
|
+
unless self.respond_to? "#{k}"
|
249
|
+
self.class.send(:define_method, "#{k}") do
|
250
|
+
get_attribute("#{k}")
|
251
|
+
end
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
def create_setters_and_getters!
|
256
|
+
@attributes.each_pair do |k, v|
|
257
|
+
create_setters!(k, v)
|
258
|
+
create_getters!(k, v)
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
# @@settings ||= nil
|
263
|
+
|
264
|
+
# Explicitly set Parse.com API keys.
|
265
|
+
#
|
266
|
+
# @param [String] app_id the Application ID of your Parse database
|
267
|
+
# @param [String] master_key the Master Key of your Parse database
|
268
|
+
# def self.load!(app_id, master_key)
|
269
|
+
# @@settings = { "app_id" => app_id, "master_key" => master_key }
|
270
|
+
# end
|
271
|
+
|
272
|
+
# def self.settings
|
273
|
+
# load_settings
|
274
|
+
# end
|
275
|
+
|
276
|
+
# # Gets the current class's model name for the URI
|
277
|
+
# def self.model_name_uri
|
278
|
+
# # This is a workaround to allow the user to specify a custom class
|
279
|
+
# if defined?(self.parse_class_name)
|
280
|
+
# "#{self.parse_class_name}"
|
281
|
+
# else
|
282
|
+
# "#{self.model_name.to_s}"
|
283
|
+
# end
|
284
|
+
# end
|
285
|
+
|
286
|
+
def self.id_ref=(val)
|
287
|
+
@id_ref = val
|
288
|
+
end
|
289
|
+
|
290
|
+
def self.id_ref
|
291
|
+
raise StandardError.new("id_ref has not been defined for #{self.name}") if @id_ref.blank?
|
292
|
+
|
293
|
+
@id_ref
|
294
|
+
end
|
295
|
+
|
296
|
+
# Alias for id_ref. Used by polymorphic association
|
297
|
+
def self.primary_key
|
298
|
+
self.id_ref
|
299
|
+
end
|
300
|
+
|
301
|
+
def id_ref
|
302
|
+
self.class.id_ref
|
303
|
+
end
|
304
|
+
|
305
|
+
def self.model_name_uri=(val)
|
306
|
+
@model_name_uri = val
|
307
|
+
|
308
|
+
@model_name_uri
|
309
|
+
end
|
310
|
+
|
311
|
+
def self.model_name_uri
|
312
|
+
@model_name_uri
|
313
|
+
end
|
314
|
+
|
315
|
+
def self.config
|
316
|
+
@config ||= Rails.application.config_for(:lockstep_client)
|
317
|
+
end
|
318
|
+
|
319
|
+
# Gets the current class's Lockstep.io base_uri
|
320
|
+
def self.model_base_uri
|
321
|
+
raise StandardError.new("Cannot establish connection for auto-generated Schema. Create a new model if you want to retrieve data from Lockstep Platform") if self.name.starts_with?("Schema::")
|
322
|
+
raise StandardError.new("URL Path is not defined for #{self.name}") if model_name_uri.blank?
|
323
|
+
|
324
|
+
base_url = config[:base_url]
|
325
|
+
base_url += "/" unless base_url.ends_with?("/")
|
326
|
+
base_url += model_name_uri
|
327
|
+
base_url += "/" unless base_url.ends_with?("/")
|
328
|
+
base_url
|
329
|
+
end
|
330
|
+
|
331
|
+
# Gets the current instance's parent class's Parse.com base_uri
|
332
|
+
def model_base_uri
|
333
|
+
self.class.send(:model_base_uri)
|
334
|
+
end
|
335
|
+
|
336
|
+
# Creates a RESTful resource
|
337
|
+
# sends requests to [base_uri]/[classname]
|
338
|
+
#
|
339
|
+
def self.resource
|
340
|
+
# load_settings
|
341
|
+
|
342
|
+
#refactor to settings['app_id'] etc
|
343
|
+
# app_id = @@settings['app_id']
|
344
|
+
# master_key = @@settings['master_key']
|
345
|
+
# RestClient::Resource.new(self.model_base_uri, app_id, master_key)
|
346
|
+
Lockstep::Client.new(self.model_base_uri)
|
347
|
+
end
|
348
|
+
|
349
|
+
def self.query_path=(value)
|
350
|
+
@query_path = value
|
351
|
+
end
|
352
|
+
|
353
|
+
def self.query_path
|
354
|
+
@query_path || "query"
|
355
|
+
end
|
356
|
+
|
357
|
+
# Batch requests
|
358
|
+
# Sends multiple requests to /batch
|
359
|
+
# Set slice_size to send larger batches. Defaults to 20 to prevent timeouts.
|
360
|
+
# Parse doesn't support batches of over 20.
|
361
|
+
#
|
362
|
+
# def self.batch_save(save_objects, slice_size = 20, method = nil)
|
363
|
+
# return true if save_objects.blank?
|
364
|
+
#
|
365
|
+
# res = self.resource
|
366
|
+
#
|
367
|
+
# # Batch saves seem to fail if they're too big. We'll slice it up into multiple posts if they are.
|
368
|
+
# save_objects.each_slice(slice_size) do |objects|
|
369
|
+
# # attributes_for_saving
|
370
|
+
# batch_json = { "requests" => [] }
|
371
|
+
#
|
372
|
+
# objects.each do |item|
|
373
|
+
# method ||= (item.new?) ? "POST" : "PATCH"
|
374
|
+
# object_path = "/1/#{item.class.model_name_uri}"
|
375
|
+
# object_path = "#{object_path}/#{item.id}" if item.id
|
376
|
+
# json = {
|
377
|
+
# "method" => method,
|
378
|
+
# "path" => object_path
|
379
|
+
# }
|
380
|
+
# json["body"] = item.attributes_for_saving unless method == "DELETE"
|
381
|
+
# batch_json["requests"] << json
|
382
|
+
# end
|
383
|
+
# res.post(batch_json.to_json, :content_type => "application/json") do |resp, req, res, &block|
|
384
|
+
# response = JSON.parse(resp) rescue nil
|
385
|
+
# if resp.code == 400
|
386
|
+
# return false
|
387
|
+
# end
|
388
|
+
# if response && response.is_a?(Array) && response.length == objects.length
|
389
|
+
# merge_all_attributes(objects, response) unless method == "DELETE"
|
390
|
+
# end
|
391
|
+
# end
|
392
|
+
# end
|
393
|
+
# true
|
394
|
+
# end
|
395
|
+
|
396
|
+
def self.merge_all_attributes(objects, response)
|
397
|
+
objects.each_with_index do |item, index|
|
398
|
+
next unless response[index]
|
399
|
+
new_attributes = response[index].transform_keys { |key| key.underscore }
|
400
|
+
item.merge_attributes(new_attributes)
|
401
|
+
end
|
402
|
+
|
403
|
+
true
|
404
|
+
end
|
405
|
+
|
406
|
+
# def self.save_all(objects)
|
407
|
+
# batch_save(objects)
|
408
|
+
# end
|
409
|
+
|
410
|
+
# def self.destroy_all(objects = nil)
|
411
|
+
# objects ||= self.all
|
412
|
+
# batch_save(objects, 20, "DELETE")
|
413
|
+
# end
|
414
|
+
|
415
|
+
# def self.delete_all(o)
|
416
|
+
# raise StandardError.new("delete_all doesn't exist. Did you mean destroy_all?")
|
417
|
+
# end
|
418
|
+
|
419
|
+
def self.bulk_import(new_objects, slice_size = 20)
|
420
|
+
return true if new_objects.blank?
|
421
|
+
|
422
|
+
# Batch saves seem to fail if they're too big. We'll slice it up into multiple posts if they are.
|
423
|
+
new_objects.each_slice(slice_size) do |objects|
|
424
|
+
# attributes_for_saving
|
425
|
+
batch_json = []
|
426
|
+
|
427
|
+
objects.each do |item|
|
428
|
+
raise StandardError.new("Bulk Import cannot only create records at the moment. It cannot update records") unless item.new?
|
429
|
+
|
430
|
+
batch_json << item.attributes_for_saving.transform_keys { |key| key.camelize(:lower) }
|
431
|
+
end
|
432
|
+
|
433
|
+
resp = self.resource.post("", body: batch_json)
|
434
|
+
# TODO attach errors if resp code is 400
|
435
|
+
|
436
|
+
if resp.code.to_s == "400"
|
437
|
+
# Error format in JSON
|
438
|
+
# "errors": {
|
439
|
+
# "[0].EmailAddress": [
|
440
|
+
# "The EmailAddress field is not a valid e-mail address."
|
441
|
+
# ]
|
442
|
+
# }
|
443
|
+
error_response = JSON.parse(resp.body)
|
444
|
+
errors = error_response["errors"]
|
445
|
+
errors.each do |key, messages|
|
446
|
+
splits = key.split(".")
|
447
|
+
attribute = splits.last&.underscore
|
448
|
+
# Remove the [] from the first split to get the position in integer
|
449
|
+
position = splits.first[1..(splits.first.size - 2)].to_i
|
450
|
+
messages.each do |message|
|
451
|
+
new_objects[position].errors.add attribute, ": #{message}"
|
452
|
+
end
|
453
|
+
end
|
454
|
+
return false
|
455
|
+
elsif resp.code.to_s != "200"
|
456
|
+
return false
|
457
|
+
end
|
458
|
+
|
459
|
+
response = JSON.parse(resp.body)
|
460
|
+
if response && response.is_a?(Array) && response.length == objects.length
|
461
|
+
# return response.map { |item|
|
462
|
+
# Lockstep::Contact.new(item.transform_keys { |key| key.underscore }, false)
|
463
|
+
# }
|
464
|
+
merge_all_attributes(objects, response)
|
465
|
+
end
|
466
|
+
end
|
467
|
+
true
|
468
|
+
end
|
469
|
+
|
470
|
+
# def self.load_settings
|
471
|
+
# @@settings ||= begin
|
472
|
+
# path = "config/parse_resource.yml"
|
473
|
+
# environment = defined?(Rails) && Rails.respond_to?(:env) ? Rails.env : ENV["RACK_ENV"]
|
474
|
+
# if FileTest.exist? (path)
|
475
|
+
# YAML.load(ERB.new(File.new(path).read).result)[environment]
|
476
|
+
# elsif ENV["PARSE_RESOURCE_APPLICATION_ID"] && ENV["PARSE_RESOURCE_MASTER_KEY"]
|
477
|
+
# settings = HashWithIndifferentAccess.new
|
478
|
+
# settings['app_id'] = ENV["PARSE_RESOURCE_APPLICATION_ID"]
|
479
|
+
# settings['master_key'] = ENV["PARSE_RESOURCE_MASTER_KEY"]
|
480
|
+
# settings
|
481
|
+
# else
|
482
|
+
# raise "Cannot load parse_resource.yml and API keys are not set in environment"
|
483
|
+
# end
|
484
|
+
# end
|
485
|
+
# @@settings
|
486
|
+
# end
|
487
|
+
|
488
|
+
# Creates a RESTful resource for file uploads
|
489
|
+
# sends requests to [base_uri]/files
|
490
|
+
#
|
491
|
+
# def self.upload(file_instance, filename, options={})
|
492
|
+
# load_settings
|
493
|
+
#
|
494
|
+
# base_uri = "https://api.parse.com/1/files"
|
495
|
+
#
|
496
|
+
# #refactor to settings['app_id'] etc
|
497
|
+
# app_id = @@settings['app_id']
|
498
|
+
# master_key = @@settings['master_key']
|
499
|
+
#
|
500
|
+
# options[:content_type] ||= 'image/jpg' # TODO: Guess mime type here.
|
501
|
+
# file_instance = File.new(file_instance, 'rb') if file_instance.is_a? String
|
502
|
+
#
|
503
|
+
# filename = filename.parameterize
|
504
|
+
#
|
505
|
+
# private_resource = RestClient::Resource.new "#{base_uri}/#{filename}", app_id, master_key
|
506
|
+
# private_resource.post(file_instance, options) do |resp, req, res, &block|
|
507
|
+
# return false if resp.code == 400
|
508
|
+
# return JSON.parse(resp) rescue {"code" => 0, "error" => "unknown error"}
|
509
|
+
# end
|
510
|
+
# false
|
511
|
+
# end
|
512
|
+
|
513
|
+
# Find a Lockstep::ApiRecord object by ID
|
514
|
+
#
|
515
|
+
# @param [String] id the ID of the Parse object you want to find.
|
516
|
+
# @return [Lockstep::ApiRecord] an object that subclasses Lockstep::ApiRecord.
|
517
|
+
def self.find(id)
|
518
|
+
raise Lockstep::Exceptions::RecordNotFound, "Couldn't find #{name} without an ID" if id.blank?
|
519
|
+
record = where(self.id_ref => id).first
|
520
|
+
raise Lockstep::Exceptions::RecordNotFound, "Couldn't find #{name} with id: #{id}" if record.blank?
|
521
|
+
record
|
522
|
+
end
|
523
|
+
|
524
|
+
# Find a Lockstep::ApiRecord object by given key/value pair
|
525
|
+
#
|
526
|
+
def self.find_by(*args)
|
527
|
+
raise Lockstep::Exceptions::RecordNotFound, "Couldn't find an object without arguments" if args.blank?
|
528
|
+
key, value = args.first.first
|
529
|
+
unless valid_attribute?(key, raise_exception: true)
|
530
|
+
raise StandardError.new("Attribute '#{key}' has not been defined for #{self.name}")
|
531
|
+
end
|
532
|
+
|
533
|
+
record = where(key => value).first
|
534
|
+
record
|
535
|
+
end
|
536
|
+
|
537
|
+
# Find a Lockstep::ApiRecord object by chaining #where method calls.
|
538
|
+
#
|
539
|
+
def self.where(*args)
|
540
|
+
query_builder.where(*args)
|
541
|
+
end
|
542
|
+
|
543
|
+
def self.execute
|
544
|
+
query_builder.execute
|
545
|
+
end
|
546
|
+
|
547
|
+
include Lockstep::QueryMethods
|
548
|
+
|
549
|
+
def self.chunk(attribute)
|
550
|
+
query_builder.chunk(attribute)
|
551
|
+
end
|
552
|
+
|
553
|
+
# Create a Lockstep::ApiRecord object.
|
554
|
+
#
|
555
|
+
# @param [Hash] attributes a `Hash` of attributes
|
556
|
+
# @return [Lockstep::ApiRecord] an object that subclasses `Lockstep::ApiRecord`. Or returns `false` if object fails to save.
|
557
|
+
def self.create(attributes = {})
|
558
|
+
attributes = HashWithIndifferentAccess.new(attributes)
|
559
|
+
obj = new(attributes)
|
560
|
+
obj.save
|
561
|
+
obj
|
562
|
+
end
|
563
|
+
|
564
|
+
# Replaced with a batch destroy_all method.
|
565
|
+
# def self.destroy_all(all)
|
566
|
+
# all.each do |object|
|
567
|
+
# object.destroy
|
568
|
+
# end
|
569
|
+
# end
|
570
|
+
|
571
|
+
def self.class_attributes
|
572
|
+
@class_attributes ||= {}
|
573
|
+
end
|
574
|
+
|
575
|
+
def self.valid_attribute?(key, raise_exception: false)
|
576
|
+
# Valid only if the record is not an API record.
|
577
|
+
# Default scopes build queries using ApiRecord to avoid conflicts. In this case, the query results in an
|
578
|
+
# exception as the fields wouldn't have been defined in the ApiRecord
|
579
|
+
return true if self.name == "Lockstep::ApiRecord"
|
580
|
+
|
581
|
+
attr = key.to_s
|
582
|
+
Lockstep::Query::PREDICATES.keys.each do |predicate|
|
583
|
+
if attr.end_with?(predicate)
|
584
|
+
attr = attr.gsub(predicate, "")
|
585
|
+
break
|
586
|
+
end
|
587
|
+
end
|
588
|
+
valid = schema.has_key?(attr)
|
589
|
+
if raise_exception && !valid
|
590
|
+
raise StandardError.new("Attribute '#{attr}' has not been defined for #{self.name}")
|
591
|
+
end
|
592
|
+
|
593
|
+
valid
|
594
|
+
end
|
595
|
+
|
596
|
+
def persisted?
|
597
|
+
if id
|
598
|
+
true
|
599
|
+
else
|
600
|
+
false
|
601
|
+
end
|
602
|
+
end
|
603
|
+
|
604
|
+
def new?
|
605
|
+
!persisted?
|
606
|
+
end
|
607
|
+
|
608
|
+
# delegate from Class method
|
609
|
+
def resource
|
610
|
+
self.class.resource
|
611
|
+
end
|
612
|
+
|
613
|
+
# create RESTful resource for the specific Parse object
|
614
|
+
# sends requests to [base_uri]/[classname]/[objectId]
|
615
|
+
def instance_resource
|
616
|
+
self.class.resource["#{self.id}"]
|
617
|
+
end
|
618
|
+
|
619
|
+
def pointerize(hash)
|
620
|
+
new_hash = {}
|
621
|
+
hash.each do |k, v|
|
622
|
+
if v.respond_to?(:to_pointer)
|
623
|
+
new_hash[k] = v.to_pointer
|
624
|
+
elsif v.is_a?(Date) || v.is_a?(Time) || v.is_a?(DateTime)
|
625
|
+
new_hash[k] = self.class.to_date_object(v)
|
626
|
+
else
|
627
|
+
new_hash[k] = v
|
628
|
+
end
|
629
|
+
end
|
630
|
+
new_hash
|
631
|
+
end
|
632
|
+
|
633
|
+
def save
|
634
|
+
if valid?
|
635
|
+
run_callbacks :save do
|
636
|
+
if new?
|
637
|
+
create
|
638
|
+
else
|
639
|
+
update
|
640
|
+
end
|
641
|
+
end
|
642
|
+
else
|
643
|
+
false
|
644
|
+
end
|
645
|
+
rescue
|
646
|
+
false
|
647
|
+
end
|
648
|
+
|
649
|
+
def create
|
650
|
+
attrs = attributes_for_saving.transform_keys { |key| key.camelize(:lower) }
|
651
|
+
resp = self.resource.post("", body: [attrs])
|
652
|
+
result = post_result(resp)
|
653
|
+
end
|
654
|
+
|
655
|
+
def update(attributes = {})
|
656
|
+
attributes = HashWithIndifferentAccess.new(attributes)
|
657
|
+
|
658
|
+
@unsaved_attributes.merge!(attributes)
|
659
|
+
# put_attrs = attributes_for_saving.to_json
|
660
|
+
|
661
|
+
attrs = attributes_for_saving.transform_keys { |key| key.camelize(:lower) }
|
662
|
+
resp = self.resource.patch(self.id, body: attrs)
|
663
|
+
result = post_result(resp)
|
664
|
+
end
|
665
|
+
|
666
|
+
# Merges in the return value of a save and resets the unsaved_attributes
|
667
|
+
def merge_attributes(results)
|
668
|
+
results.transform_keys! { |key| key.underscore }
|
669
|
+
@attributes.merge!(results)
|
670
|
+
@attributes.merge!(@unsaved_attributes)
|
671
|
+
|
672
|
+
merge_relations
|
673
|
+
@unsaved_attributes = {}
|
674
|
+
|
675
|
+
create_setters_and_getters!
|
676
|
+
@attributes
|
677
|
+
end
|
678
|
+
|
679
|
+
def merge_relations
|
680
|
+
# KK 11-17-2012 The response after creation does not return full description of
|
681
|
+
# the object nor the relations it contains. Make another request here.
|
682
|
+
# TODO: @@has_many_relations structure has been changed from array to hash, need to evaluate the impact here
|
683
|
+
if has_many_relations.keys.map { |relation| relation.to_s.to_sym }
|
684
|
+
#todo: make this a little smarter by checking if there are any Pointer objects in the objects attributes.
|
685
|
+
# @attributes = self.class.to_s.constantize.where(:objectId => @attributes[self.id_ref]).first.attributes
|
686
|
+
@attributes = self.class.to_s.constantize.where(self.id_ref => @attributes[self.id_ref]).first.attributes
|
687
|
+
end
|
688
|
+
end
|
689
|
+
|
690
|
+
def post_result(resp)
|
691
|
+
if resp.code.to_s == "200" || resp.code.to_s == "201"
|
692
|
+
body = JSON.parse(resp.body)
|
693
|
+
# Create method always responds with an array, whereas update responds with the object
|
694
|
+
body = body.first if body.is_a?(Array)
|
695
|
+
|
696
|
+
merge_attributes(body)
|
697
|
+
|
698
|
+
return true
|
699
|
+
elsif resp.code.to_s == "400"
|
700
|
+
error_response = JSON.parse(resp.body)
|
701
|
+
errors = error_response["errors"]
|
702
|
+
errors.each do |key, messages|
|
703
|
+
attribute = key.split(".").last&.underscore
|
704
|
+
messages.each do |message|
|
705
|
+
self.errors.add attribute, ": #{message}"
|
706
|
+
end
|
707
|
+
end
|
708
|
+
else
|
709
|
+
error_response = JSON.parse(resp.body)
|
710
|
+
if error_response["error"]
|
711
|
+
pe = LockstepError.new(error_response["code"], error_response["error"])
|
712
|
+
else
|
713
|
+
pe = LockstepError.new(resp.code.to_s)
|
714
|
+
end
|
715
|
+
self.errors.add(pe.code.to_s.to_sym, pe.msg)
|
716
|
+
self.error_instances << pe
|
717
|
+
return false
|
718
|
+
end
|
719
|
+
end
|
720
|
+
|
721
|
+
def attributes_for_saving
|
722
|
+
@unsaved_attributes = pointerize(@unsaved_attributes)
|
723
|
+
put_attrs = @unsaved_attributes
|
724
|
+
|
725
|
+
put_attrs = relations_for_saving(put_attrs)
|
726
|
+
|
727
|
+
put_attrs.delete(self.id_ref)
|
728
|
+
put_attrs.delete("created")
|
729
|
+
put_attrs.delete("modified")
|
730
|
+
put_attrs
|
731
|
+
end
|
732
|
+
|
733
|
+
def relations_for_saving(put_attrs)
|
734
|
+
all_add_item_queries = {}
|
735
|
+
all_remove_item_queries = {}
|
736
|
+
@unsaved_attributes.each_pair do |key, value|
|
737
|
+
next if !value.is_a? Array
|
738
|
+
|
739
|
+
# Go through the array in unsaved and check if they are in array in attributes (saved stuff)
|
740
|
+
add_item_ops = []
|
741
|
+
@unsaved_attributes[key].each do |item|
|
742
|
+
found_item_in_saved = false
|
743
|
+
@attributes[key].each do |item_in_saved|
|
744
|
+
if !!(defined? item.attributes) && item.attributes[self.id_ref] == item_in_saved.attributes[self.id_ref]
|
745
|
+
found_item_in_saved = true
|
746
|
+
end
|
747
|
+
end
|
748
|
+
|
749
|
+
if !found_item_in_saved && !!(defined? item.id)
|
750
|
+
# need to send additem operation to parse
|
751
|
+
put_attrs.delete(key) # arrays should not be sent along with REST to parse api
|
752
|
+
add_item_ops << { "__type" => "Pointer", "className" => item.class.to_s, self.id_ref => item.id }
|
753
|
+
end
|
754
|
+
end
|
755
|
+
all_add_item_queries.merge!({ key => { "__op" => "Add", "objects" => add_item_ops } }) if !add_item_ops.empty?
|
756
|
+
|
757
|
+
# Go through saved and if it isn't in unsaved perform a removeitem operation
|
758
|
+
remove_item_ops = []
|
759
|
+
unless @unsaved_attributes.empty?
|
760
|
+
@attributes[key].each do |item|
|
761
|
+
found_item_in_unsaved = false
|
762
|
+
@unsaved_attributes[key].each do |item_in_unsaved|
|
763
|
+
if !!(defined? item.attributes) && item.attributes[self.id_ref] == item_in_unsaved.attributes[self.id_ref]
|
764
|
+
found_item_in_unsaved = true
|
765
|
+
end
|
766
|
+
end
|
767
|
+
|
768
|
+
if !found_item_in_unsaved && !!(defined? item.id)
|
769
|
+
# need to send removeitem operation to parse
|
770
|
+
remove_item_ops << { "__type" => "Pointer", "className" => item.class.to_s, self.id_ref => item.id }
|
771
|
+
end
|
772
|
+
end
|
773
|
+
end
|
774
|
+
all_remove_item_queries.merge!({ key => { "__op" => "Remove", "objects" => remove_item_ops } }) if !remove_item_ops.empty?
|
775
|
+
end
|
776
|
+
|
777
|
+
# TODO figure out a more elegant way to get this working. the remove_item merge overwrites the add.
|
778
|
+
# Use a seperate query to add objects to the relation.
|
779
|
+
#if !all_add_item_queries.empty?
|
780
|
+
# #result = self.instance_resource.put(all_add_item_queries.to_json, {:content_type => "application/json"}) do |resp, req, res, &block|
|
781
|
+
# # return puts(resp, req, res, false, &block)
|
782
|
+
# #end
|
783
|
+
# puts result
|
784
|
+
#end
|
785
|
+
|
786
|
+
put_attrs.merge!(all_add_item_queries) unless all_add_item_queries.empty?
|
787
|
+
put_attrs.merge!(all_remove_item_queries) unless all_remove_item_queries.empty?
|
788
|
+
put_attrs
|
789
|
+
end
|
790
|
+
|
791
|
+
def update_attributes(attributes = {})
|
792
|
+
self.update(attributes)
|
793
|
+
end
|
794
|
+
|
795
|
+
def update_attribute(key, value)
|
796
|
+
send(key.to_s + "=", value)
|
797
|
+
update
|
798
|
+
end
|
799
|
+
|
800
|
+
def destroy
|
801
|
+
resp = self.resource.delete(self.id)
|
802
|
+
if resp.code.to_s == "200"
|
803
|
+
@attributes = {}
|
804
|
+
@unsaved_attributes = {}
|
805
|
+
return true
|
806
|
+
end
|
807
|
+
false
|
808
|
+
end
|
809
|
+
|
810
|
+
def reload
|
811
|
+
return false if new?
|
812
|
+
|
813
|
+
fresh_object = self.class.find(id)
|
814
|
+
@attributes = {}
|
815
|
+
@attributes.update(fresh_object.instance_variable_get("@attributes"))
|
816
|
+
@unsaved_attributes = {}
|
817
|
+
|
818
|
+
self
|
819
|
+
end
|
820
|
+
|
821
|
+
def dirty?
|
822
|
+
@unsaved_attributes.length > 0
|
823
|
+
end
|
824
|
+
|
825
|
+
def clean?
|
826
|
+
!dirty?
|
827
|
+
end
|
828
|
+
|
829
|
+
# provides access to @attributes for getting and setting
|
830
|
+
def attributes
|
831
|
+
@attributes ||= self.class.class_attributes
|
832
|
+
@attributes
|
833
|
+
end
|
834
|
+
|
835
|
+
def attributes=(value)
|
836
|
+
if value.is_a?(Hash) && value.present?
|
837
|
+
value.each do |k, v|
|
838
|
+
send "#{k}=", v
|
839
|
+
end
|
840
|
+
end
|
841
|
+
@attributes
|
842
|
+
end
|
843
|
+
|
844
|
+
def get_attribute(k)
|
845
|
+
attrs = @unsaved_attributes[k.to_s] ? @unsaved_attributes : @attributes
|
846
|
+
case attrs[k]
|
847
|
+
when Hash
|
848
|
+
klass_name = attrs[k]["className"]
|
849
|
+
klass_name = "User" if klass_name == "_User"
|
850
|
+
case attrs[k]["__type"]
|
851
|
+
when "Pointer"
|
852
|
+
result = klass_name.to_s.constantize.find(attrs[k][self.id_ref])
|
853
|
+
when "Object"
|
854
|
+
result = klass_name.to_s.constantize.new(attrs[k], false)
|
855
|
+
when "Date"
|
856
|
+
result = DateTime.parse(attrs[k]["iso"]).in_time_zone
|
857
|
+
when "File"
|
858
|
+
result = attrs[k]["url"]
|
859
|
+
when "Relation"
|
860
|
+
objects_related_to_self = klass_name.constantize.where("$relatedTo" => { "object" => { "__type" => "Pointer", "className" => self.class.to_s, self.id_ref => self.id }, "key" => k }).all
|
861
|
+
attrs[k] = Lockstep::RelationArray.new self, objects_related_to_self, k, klass_name
|
862
|
+
@unsaved_attributes[k] = Lockstep::RelationArray.new self, objects_related_to_self, k, klass_name
|
863
|
+
result = @unsaved_attributes[k]
|
864
|
+
end
|
865
|
+
else
|
866
|
+
# TODO changed from @@has_many_relations to @@has_many_relations.keys as we have changed the has_many_relations
|
867
|
+
# from array to hash to capture more data points. Not sure of the impact of this.
|
868
|
+
#relation will assign itself if an array, this will add to unsave_attributes
|
869
|
+
if has_many_relations.keys.index(k.to_s)
|
870
|
+
if attrs[k].nil?
|
871
|
+
# result = nil
|
872
|
+
result = load_association(:has_many, k)
|
873
|
+
else
|
874
|
+
@unsaved_attributes[k] = attrs[k].clone
|
875
|
+
result = @unsaved_attributes[k]
|
876
|
+
end
|
877
|
+
elsif belongs_to_relations.keys.index(k.to_s)
|
878
|
+
if attrs[k].nil?
|
879
|
+
# result = nil
|
880
|
+
result = load_association(:belongs_to, k)
|
881
|
+
else
|
882
|
+
@unsaved_attributes[k] = attrs[k].clone
|
883
|
+
result = @unsaved_attributes[k]
|
884
|
+
end
|
885
|
+
else
|
886
|
+
result = attrs["#{k}"]
|
887
|
+
end
|
888
|
+
end
|
889
|
+
result
|
890
|
+
end
|
891
|
+
|
892
|
+
# Alias of get_attribute
|
893
|
+
def _read_attribute(attr)
|
894
|
+
get_attribute(attr)
|
895
|
+
end
|
896
|
+
|
897
|
+
def set_attribute(k, v)
|
898
|
+
if v.is_a?(Date) || v.is_a?(Time) || v.is_a?(DateTime)
|
899
|
+
v = self.class.to_date_object(v)
|
900
|
+
# elsif v.respond_to?(:to_pointer)
|
901
|
+
# v = v.to_pointer
|
902
|
+
elsif (type = schema[k])
|
903
|
+
# Typecast the result value using dry-types
|
904
|
+
v = type[v]
|
905
|
+
elsif v.present? and (enum = enum_config[k]).present? and enum.keys.include?(v.to_s)
|
906
|
+
v = enum[v]
|
907
|
+
end
|
908
|
+
|
909
|
+
@unsaved_attributes[k.to_s] = v unless v == @attributes[k.to_s] # || @unsaved_attributes[k.to_s]
|
910
|
+
@attributes[k.to_s] = v
|
911
|
+
v
|
912
|
+
end
|
913
|
+
|
914
|
+
def self.has_many_relations
|
915
|
+
@has_many_relations ||= {}.with_indifferent_access
|
916
|
+
end
|
917
|
+
|
918
|
+
def has_many_relations
|
919
|
+
self.class.has_many_relations
|
920
|
+
end
|
921
|
+
|
922
|
+
# Alias for has_many_relations
|
923
|
+
def self.lockstep_has_many_relations
|
924
|
+
has_many_relations
|
925
|
+
end
|
926
|
+
|
927
|
+
def self.belongs_to_relations
|
928
|
+
@belongs_to_relations ||= {}.with_indifferent_access
|
929
|
+
end
|
930
|
+
|
931
|
+
def belongs_to_relations
|
932
|
+
self.class.belongs_to_relations
|
933
|
+
end
|
934
|
+
|
935
|
+
# Alias for belongs_to_relations
|
936
|
+
def self.lockstep_belongs_to_relations
|
937
|
+
belongs_to_relations
|
938
|
+
end
|
939
|
+
|
940
|
+
def primary_key
|
941
|
+
self.id_ref
|
942
|
+
end
|
943
|
+
|
944
|
+
# aliasing for idiomatic Ruby
|
945
|
+
def id
|
946
|
+
get_attribute(self.id_ref) rescue nil
|
947
|
+
end
|
948
|
+
|
949
|
+
def objectId
|
950
|
+
get_attribute(self.id_ref) rescue nil
|
951
|
+
end
|
952
|
+
|
953
|
+
def created_at
|
954
|
+
get_attribute("created")
|
955
|
+
end
|
956
|
+
|
957
|
+
def updated_at
|
958
|
+
get_attribute("modified")
|
959
|
+
end
|
960
|
+
|
961
|
+
def self.included(base)
|
962
|
+
base.extend(ClassMethods)
|
963
|
+
end
|
964
|
+
|
965
|
+
module ClassMethods
|
966
|
+
end
|
967
|
+
|
968
|
+
#if we are comparing objects, use id if they are both Lockstep::ApiRecord objects
|
969
|
+
def ==(another_object)
|
970
|
+
if another_object.class <= Lockstep::ApiRecord
|
971
|
+
self.id == another_object.id
|
972
|
+
else
|
973
|
+
super
|
974
|
+
end
|
975
|
+
end
|
976
|
+
|
977
|
+
def load_association(association_type, relation)
|
978
|
+
@loaded_associations ||= []
|
979
|
+
return nil if @loaded_associations.include?(relation)
|
980
|
+
|
981
|
+
@loaded_associations << relation # Prevent the load_association from being called the 2nd time
|
982
|
+
|
983
|
+
val = nil
|
984
|
+
case association_type
|
985
|
+
when :has_many
|
986
|
+
relation_config = has_many_relations[relation]
|
987
|
+
|
988
|
+
if relation_config[:loader].present?
|
989
|
+
val = relation_config[:loader].call(self)
|
990
|
+
else
|
991
|
+
return val unless relation_config[:foreign_key].present? and relation_config[:primary_key].present?
|
992
|
+
relation_klass = relation_config[:class_name].constantize
|
993
|
+
return val unless relation_klass.model_name_uri.present?
|
994
|
+
|
995
|
+
query = { relation_config[:foreign_key] => self.send(relation_config[:primary_key]) }
|
996
|
+
if relation_config[:polymorphic]
|
997
|
+
polymorphic_config = Lockstep::RelationArray.has_many_polymorphic_attributes(self, relation_config[:polymorphic])
|
998
|
+
query.merge!(polymorphic_config)
|
999
|
+
end
|
1000
|
+
related_objects = relation_klass.send(:where, query).execute
|
1001
|
+
val = Lockstep::RelationArray.new self, related_objects, relation, relation_config[:class_name]
|
1002
|
+
end
|
1003
|
+
when :belongs_to
|
1004
|
+
relation_config = belongs_to_relations[relation]
|
1005
|
+
if relation_config[:loader].present?
|
1006
|
+
val = relation_config[:loader].call(self)
|
1007
|
+
else
|
1008
|
+
val = relation_config[:class_name].constantize.send(:find_by, relation_config[:primary_key] => self.send(relation_config[:foreign_key]))
|
1009
|
+
end
|
1010
|
+
end
|
1011
|
+
|
1012
|
+
set_attribute(relation, val)
|
1013
|
+
val
|
1014
|
+
end
|
1015
|
+
|
1016
|
+
# @has_many_associations polymorphic properties builder.
|
1017
|
+
# Polymorphic properties is used to further scope down the has_many association while querying or creating
|
1018
|
+
#
|
1019
|
+
# def has_many_polymorphic_attributes(polymorphic_config)
|
1020
|
+
# return polymorphic_config if polymorphic_config.is_a?(Hash)
|
1021
|
+
# return polymorphic_config.call(self) if polymorphic_config.is_a?(Proc)
|
1022
|
+
#
|
1023
|
+
# nil
|
1024
|
+
# end
|
1025
|
+
|
1026
|
+
# TODO: Implement the ability to build_belongs_to_association
|
1027
|
+
# Challenge is that it has to update the record's association_id once the new association is created
|
1028
|
+
# def build_belongs_to_association(parent, attributes_hash)
|
1029
|
+
# if (val = get_attribute(parent)).present?
|
1030
|
+
# return val
|
1031
|
+
# end
|
1032
|
+
#
|
1033
|
+
# relation_config = @@belongs_to_relations[parent]
|
1034
|
+
# # Assign the parent records primary_key to the foreign_key of the association
|
1035
|
+
# foreign_key = relation_config[:foreign_key]
|
1036
|
+
# primary_key = relation_config[:primary_key]
|
1037
|
+
# attributes_hash[primary_key] = delegate.send(foreign_key)
|
1038
|
+
# # TODO implement polymorphic association support
|
1039
|
+
# object = self.class_name.constantize.new(attributes_hash)
|
1040
|
+
# set_attribute(parent, object)
|
1041
|
+
#
|
1042
|
+
# object
|
1043
|
+
# end
|
1044
|
+
|
1045
|
+
# Enum implementation - Start
|
1046
|
+
def self.enum_config
|
1047
|
+
@enum_config ||= {}.with_indifferent_access
|
1048
|
+
end
|
1049
|
+
|
1050
|
+
def enum_config
|
1051
|
+
self.class.enum_config
|
1052
|
+
end
|
1053
|
+
|
1054
|
+
def self.enum(config)
|
1055
|
+
config.each do |attribute, values|
|
1056
|
+
# Standardise values to hash
|
1057
|
+
if values.is_a?(Array)
|
1058
|
+
value_map = {}.with_indifferent_access
|
1059
|
+
values.each { |item| value_map[item] = item }
|
1060
|
+
elsif values.is_a?(Hash)
|
1061
|
+
value_map = values.with_indifferent_access
|
1062
|
+
else
|
1063
|
+
raise StandardError.new("Invalid values for enum #{attribute}")
|
1064
|
+
end
|
1065
|
+
|
1066
|
+
# Convert values to string if the value is symbol
|
1067
|
+
value_map.each { |k, v| value_map[k] = v.to_s if v.is_a?(Symbol) }
|
1068
|
+
|
1069
|
+
enum_config[attribute] = value_map
|
1070
|
+
class_eval do
|
1071
|
+
value_map.each do |k, v|
|
1072
|
+
define_method("#{k}!") do
|
1073
|
+
set_attribute(attribute, v)
|
1074
|
+
return save if persisted?
|
1075
|
+
|
1076
|
+
true
|
1077
|
+
end
|
1078
|
+
|
1079
|
+
define_method("#{k}?") do
|
1080
|
+
get_attribute(attribute) == v
|
1081
|
+
end
|
1082
|
+
end
|
1083
|
+
end
|
1084
|
+
end
|
1085
|
+
end
|
1086
|
+
|
1087
|
+
def validate_enum
|
1088
|
+
enum_config.each do |attribute, values_map|
|
1089
|
+
value = get_attribute(attribute)
|
1090
|
+
next if value.nil?
|
1091
|
+
|
1092
|
+
unless values_map.values.include?(value)
|
1093
|
+
errors.add attribute, "has an invalid value"
|
1094
|
+
end
|
1095
|
+
end
|
1096
|
+
end
|
1097
|
+
|
1098
|
+
# Enum implementation - End
|
1099
|
+
|
1100
|
+
def self.alias_attribute(new_name, old_name)
|
1101
|
+
define_method(new_name) do
|
1102
|
+
send(old_name)
|
1103
|
+
end
|
1104
|
+
|
1105
|
+
define_method("#{new_name}=") do |value|
|
1106
|
+
send("#{old_name}=", value)
|
1107
|
+
end
|
1108
|
+
end
|
1109
|
+
|
1110
|
+
def to_json(options = {})
|
1111
|
+
as_json(options).to_json
|
1112
|
+
end
|
1113
|
+
|
1114
|
+
def as_json(options = {})
|
1115
|
+
@attributes.merge(@unsaved_attributes).as_json
|
1116
|
+
end
|
1117
|
+
end
|
1118
|
+
end
|