Module: ASModel::CRUD

Included in:
RightsRestriction
Defined in:
backend/app/model/ASModel_crud.rb

Overview

Code for converting JSONModels into DB records and back again.

Defined Under Namespace

Modules: ClassMethods

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.included(base) ⇒ Object



8
9
10
11
12
# File 'backend/app/model/ASModel_crud.rb', line 8

def self.included(base)
  base.extend(ClassMethods)
  base.include(JSONModel)
  base.extend(JSONModel)
end

.set_audit_fields(json, obj) ⇒ Object



17
18
19
20
21
22
23
24
25
26
27
28
# File 'backend/app/model/ASModel_crud.rb', line 17

def self.set_audit_fields(json, obj)
  ['created_by', 'last_modified_by'].each do |field|
    json[field] = obj[field.intern] if obj[field.intern]
  end

  ['system_mtime', 'user_mtime', 'create_time'].each do |field|
    val = obj[field.intern]
    next if !val

    json[field] = val.getutc.iso8601
  end
end

Instance Method Details

#apply_nested_records(json, new_record = false) ⇒ Object

Several JSONModels consist of logical subrecords that are stored as separate models in the database (in separate tables).

When we get a JSON blob for a record with subrecords, we want to create a database record for each subrecords (or, if a URI referencing an existing subrecord was given, use the existing object), then associate those subrecords with the main record.



60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
# File 'backend/app/model/ASModel_crud.rb', line 60

def apply_nested_records(json, new_record = false)
  self.remove_nested_records if !new_record

  self.class.nested_records.each do |nested_record|
    # Read the subrecords from our JSON blob and fetch or create
    # the corresponding subrecord from the database.
    model = Kernel.const_get(nested_record[:association][:class_name])

    if nested_record[:association][:type] === :one_to_one
      add_record_method = nested_record[:association][:name].to_s
    elsif nested_record[:association][:type] === :many_to_one
      add_record_method = "#{nested_record[:association][:name].to_s.singularize}="
    else
      add_record_method = "add_#{nested_record[:association][:name].to_s.singularize}"
    end

    records = json[nested_record[:json_property]]

    is_array = true
    if nested_record[:association][:type] === :one_to_one || nested_record[:is_array] === false
      is_array = false
      records = [records]
    end

    updated_records = []
    (records or []).each_with_index do |json_or_uri, i|
      next if json_or_uri.nil?

      db_record = nil

      begin
        needs_linking = true

        if json_or_uri.is_a? String
          # A URI.  Just grab its database ID and look it up.
          db_record = model[JSONModel(nested_record[:jsonmodel]).id_for(json_or_uri)]
          updated_records << json_or_uri
        else
          # Create a database record for the JSON blob and return its ID
          subrecord_json = JSONModel(nested_record[:jsonmodel]).from_hash(json_or_uri, true, true)

          # The value of subrecord_json can be mutated by the various
          # transformations performed by the model layer.  Make sure we
          # keep the modified version of the JSON here.
          updated_records << subrecord_json

          if model.respond_to? :ensure_exists
            # Give our classes an opportunity to provide their own logic here
            db_record = model.ensure_exists(subrecord_json, self)
          else
            extra_opts = {}

            if nested_record[:association][:key]
              extra_opts[nested_record[:association][:key]] = self.id

              # We'll skip the call to the .add method because this step
              # will have already linked the nested record to this one.
              needs_linking = false
            end

            db_record = model.create_from_json(subrecord_json, extra_opts)
          end
        end

        if db_record.system_modified?
          # If the subrecord got changed by the system, mark ourselves as
          # modified too.
          self.mark_as_system_modified
        end

        self.send(add_record_method, db_record) if (db_record && needs_linking)
      rescue Sequel::ValidationFailed => e
        # Modify the exception keys by prefixing each with the path up until this point.
        e.instance_eval do
          if @errors
            prefix = nested_record[:json_property]
            prefix = "#{prefix}/#{i}" if is_array

            new_errors = {}
            @errors.each do |k, v|
              new_errors["#{prefix}/#{k}"] = v
            end

            @errors = new_errors
          end
        end

        raise e
      end
    end

    json[nested_record[:json_property]] = is_array ? updated_records : updated_records[0]
  end
end

#deleteObject

Delete the current record using Sequel’s delete method, but clean up dependencies first.



224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
# File 'backend/app/model/ASModel_crud.rb', line 224

def delete
  object_graph = self.object_graph

  deleted_uris = []

  successfully_deleted_models = []
  last_error = nil
  while true
    progressed = false
    object_graph.each do |model, ids_to_delete|
      next if successfully_deleted_models.include?(model)

      begin
        model.handle_delete(ids_to_delete)
        successfully_deleted_models << model
        progressed = true
      rescue Sequel::DatabaseError
        last_error = $!
        next
      end

      if model.my_jsonmodel(true)
        ids_to_delete.each do |id|
          deleted_model = model.my_jsonmodel(true)

          deleted_uri = deleted_model.uri_for(id, :repo_id => model.active_repository)

          if deleted_uri
            deleted_uris << deleted_uri
          end
        end
      end
    end

    break if object_graph.models.length == successfully_deleted_models.length

    unless progressed
      if last_error && DB.is_retriable_exception(last_error)
        # Give us a chance to retry after a deadlock
        raise last_error
      end

      raise ConflictException.new("Record deletion failed: #{last_error}")
    end
  end


  deleted_uris.each do |uri|
    Tombstone.create(:uri => uri)
    DB.after_commit do
      RealtimeIndexing.record_delete(uri)
    end
  end
end

#eagerly_load!Object

Do whatever is necessary to eaglerly load this object from the database.

This is designed to give mixins the options of eagerly loading an entire record and its components.



48
49
50
# File 'backend/app/model/ASModel_crud.rb', line 48

def eagerly_load!
  # Do nothing by default
end

#map_validation_to_json_property(columns, property) ⇒ Object

When reporting a Sequel validation error against the set of ‘columns’, report it against the JSONModel ‘property’ instead.

For example, an identifier that must be unique to a repository might have a constraint against the columns [:repository, :identifier], but when we report this to the client we just want to tell them that the value for ‘identifier’ was incorrect.



287
288
289
290
291
292
293
294
295
296
297
298
299
# File 'backend/app/model/ASModel_crud.rb', line 287

def map_validation_to_json_property(columns, property)
  errors = self.errors.clone

  self.errors.clear

  errors.each do |error, msg|
    if error == columns
      self.errors[property] = msg
    else
      self.errors[error] = msg
    end
  end
end

#mark_as_system_modifiedObject



314
315
316
# File 'backend/app/model/ASModel_crud.rb', line 314

def mark_as_system_modified
  @system_modified = true
end

#remove_nested_recordsObject



156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
# File 'backend/app/model/ASModel_crud.rb', line 156

def remove_nested_records
  self.class.nested_records.each do |nested_record_defn|
    if [:one_to_one, :one_to_many].include?(nested_record_defn[:association][:type])
      # If the current record "owns" its nested record, delete the nested record.
      model = Kernel.const_get(nested_record_defn[:association][:class_name])

      # Tell the nested record to clear its own nested records
      Array(self.send(nested_record_defn[:association][:name])).each do |nested_record|
        nested_record.delete
      end
    elsif nested_record_defn[:association][:type] === :many_to_many
      # Just remove the links
      self.send("remove_all_#{nested_record_defn[:association][:name]}".intern)
    elsif nested_record_defn[:association][:type] === :many_to_one
      # Just remove the link
      self.send("#{nested_record_defn[:association][:name].intern}=", nil)
    end
  end
end

#system_modified?Boolean

that their local copy of the record includes the system-generated data too.

Returns:

  • (Boolean)


309
310
311
# File 'backend/app/model/ASModel_crud.rb', line 309

def system_modified?
  @system_modified
end

#update_from_json(json, extra_values = {}, apply_nested_records = true) ⇒ Object



177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
# File 'backend/app/model/ASModel_crud.rb', line 177

def update_from_json(json, extra_values = {}, apply_nested_records = true)
  if self.values.has_key?(:suppressed)
    if self[:suppressed] == 1
      raise ReadOnlyException.new("Can't update an object that has been suppressed")
    end

    # No funny business.  If you want to set this you need to do it via the
    # dedicated controller.
    json["suppressed"] = false
  end


  schema_defined_properties = json.class.schema["properties"].map {|prop, defn|
    prop if !defn['readonly']
  }.compact

  # Start by assuming all existing properties were nil, then overlay the
  # updates plus any extra attributes.
  #
  # This has the effect of unsetting (or setting to NULL) any properties that
  # were removed by this update.
  updated = Hash[schema_defined_properties.map {|property| [property, nil]}].
    merge(json.to_hash).
    merge(ASUtils.keys_as_strings(extra_values))

  if updated.has_key?('lock_version') && !updated['lock_version']
    raise ConflictException.new("You must provide a lock_version in your request")
  end

  self.class.strict_param_setting = false

  self.update(self.class.prepare_for_db(json.class, updated).
              merge(:user_mtime => Time.now,
                    :last_modified_by => RequestContext.get(:current_username)))

  if apply_nested_records
    self.apply_nested_records(json)
  end

  self.class.fire_update(json, self)

  self
end

#validateObject



31
32
33
34
35
36
37
38
39
40
41
# File 'backend/app/model/ASModel_crud.rb', line 31

def validate
  # Check uniqueness constraints
  self.class.repo_unique_constraints.each do |constraint|
    validates_unique([:repo_id, constraint[:property]],
                     :message => constraint[:message])
    map_validation_to_json_property([:repo_id, constraint[:property]],
                                     constraint[:json_property])
  end

  super
end