Module: Waxx::View

Defined in:
waxx/view.rb

Overview

A Waxx::View is like a database view. You define the primary object and related tables and fields on the view and it handles some routine processes for you. You can also just use it as a container for specialized business logic or complicated queries.

Example usage:

“` module App::Usr::List

extend Waxx::View
extend self

# Define what layouts to allow
as :json

# Define what fields are in the view
has(
  # Fields that are in the 'usr' table
  :id,
  :usr_name,
  :last_login_date,
  :last_login_host,
  :failed_login_count,
  # Fields that are in the 'person' table. Relationships are defined in App::Usr.has(...)
  "person_id: person.id",  # This column is accessible as 'person_id' on this view
  "person.first_name",
  "person.last_name",
)

end “`

This view definition will provide you with the following functionality:

“` App::Usr::List.get(x) # Executes the following SQL: SELECT usr.id, usr.usr_name, usr.last_login_date, usr.last_login_host, usr.failed_login_count,

person.id AS person_id, person.first_name, person.last_name

FROM usr LEFT JOIN person ON usr.id = person.id # And returns a PG::Result (If you are using the PG database connector) “`

Instance Attribute Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#columnsObject (readonly)

A hash of columns (See Waxx::Pg.has)



52
53
54
# File 'waxx/view.rb', line 52

def columns
  @columns
end

#joinsObject (readonly)

A hash of name: join_sql. Normally set automatically when the columns are parsed.



54
55
56
# File 'waxx/view.rb', line 54

def joins
  @joins
end

#matchesObject (readonly)

How to search the view by specific field



58
59
60
# File 'waxx/view.rb', line 58

def matches
  @matches
end

#objectObject (readonly)

The parent (primary) object. For example in App::Usr::List, App::Usr is the @object



48
49
50
# File 'waxx/view.rb', line 48

def object
  @object
end

#order_byObject (readonly)

The default order of the results



62
63
64
# File 'waxx/view.rb', line 62

def order_by
  @order_by
end

#ordersObject (readonly)

A hash of how you can sort this view



64
65
66
# File 'waxx/view.rb', line 64

def orders
  @orders
end

#relationsObject (readonly)

A hash of related tables. Normally set automatically when the columns are parsed.



56
57
58
# File 'waxx/view.rb', line 56

def relations
  @relations
end

#searchesObject (readonly)

How to search the view by the “q” parameter



60
61
62
# File 'waxx/view.rb', line 60

def searches
  @searches
end

#tableObject (readonly)

The table name of the primary object



50
51
52
# File 'waxx/view.rb', line 50

def table
  @table
end

Instance Method Details

#[](c) ⇒ Object

Get a column on the view

“` App::Usr::Record => :label=>“User Name”, :table=>:usr, :column=>:usr_name, :views=>[App::Usr::Record, App::Usr::List, App::Usr::Signup] “`



91
92
93
# File 'waxx/view.rb', line 91

def [](c)
  @columns[c.to_sym]
end

#as(*views) ⇒ Object

Autogenerate the modules to do standard layouts like Json, Csv, or Tab

“` as :json “`

This will generate the following code and allow the output of json formatted data for the view

“` module App::Usr::List::Json

extend Waxx::Json
extend self

end “`



233
234
235
236
237
238
239
240
241
242
# File 'waxx/view.rb', line 233

def as(*views)
  views.each{|v|
    eval("
      module #{name}::#{v.to_s.capitalize} 
        extend Waxx::#{v.to_s.capitalize}
        extend self
      end
    ")
  }
end

#build_where(x, args: {}, matches: @matches, searches: @searches) ⇒ Object

Automatically build the where clause of SQL based on the parameters passed in and the definition of matches and searches.



302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
# File 'waxx/view.rb', line 302

def build_where(x, args: {}, matches: @matches, searches: @searches)
  return nil if args.nil? or args.empty? or (matches.nil? and searches.nil?)
  w_str = ""
  w_args = []
  q = args/:q || x['q']
  if q and searches
    w_str += "("
    searches.each_with_index{|c, i|
      w_str += " OR " if i > 0
      w_str += "LOWER(#{c}) like $1"
    }
    w_args << "%#{q.downcase}%"
    w_str += ")"
  end
  if matches
    matches.each_with_index{|c, i|
      next if (x/c).to_s == "" and (args/c).to_s == ""
      w_str += " AND " if w_str != ""
      col = self[c.to_sym]
      w_str += "#{c} #{col[:match] || "="} $#{w_args.size + 1}"
      w_args << (args/c || x/c)
    }
  end
  [w_str, w_args]
end

#debug(str, level = 3) ⇒ Object

A shorcut to Waxx.debug



386
387
388
# File 'waxx/view.rb', line 386

def debug(str, level=3)
  Waxx.debug(str, level)
end

#default_order(ord) ⇒ Object

Set the default order. Order is a key of the field name. Use _name to sort descending.



258
259
260
# File 'waxx/view.rb', line 258

def default_order(ord)
  @order_by = ord
end

#delete(x, id) ⇒ Object

Delete a record by ID (primary key of the primary object)



363
364
365
# File 'waxx/view.rb', line 363

def delete(x, id)
  @object.delete(x, id)
end

#get(x, where: nil, having: nil, order: nil, limit: nil, offset: nil, args: {}, &blk) ⇒ Object

Override this method in a view to change params



330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
# File 'waxx/view.rb', line 330

def get(x, where:nil, having:nil, order:nil, limit:nil, offset:nil, args:{}, &blk)
  where  ||= build_where(x, args: args)
  order  ||= args/:order || @order_by
  limit  ||= args/:limit
  offset ||= args/:offset
  @object.get(x, 
    view: self, 
    where: where, 
    joins: joins_to_sql(),
    having: having,
    order: order, 
    limit: limit, 
    offset: offset
  )
end

#get_by_id(x, id) ⇒ Object Also known as: by_id

Get a single record of the view based on the primary key of the primary object



348
349
350
# File 'waxx/view.rb', line 348

def get_by_id(x, id)
  @object.get_by_id(x, id, view: self)
end

#has(*cols) ⇒ Object

Columnas on a view can be defined in multiple ways:

“` has(

:id,                         # A field in the parent object
:name,                       # Another field in the parent object
"company_name:company.name", # name:rel_name.col_name  "name" is the name of the col in the query, rel_name is the join table as defined in object, col_name is the column in the foreign table 
[:creator, {table: "person", sql_select: "first_name || ' ' || last_name", label: "Creator"}]  # Array: [name, column (Hash)] 
{modifier: {table: "person", sql_select: "first_name || ' ' || last_name", label: "Creator"}}

“`



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
# File 'waxx/view.rb', line 107

def has(*cols)
  return @columns if cols.empty?
  init if @object.nil?
  #@joins = {}
  @columns = {}
  cols.each{|c|
    n = col = nil
    case c
    # Get the col from the object
    when Symbol
      n = c
      col = @object[c]
    # A related col (must be defined in the related object)
    when String
      n, col = string_to_col(c)
    # A custom col [name, col] col is a Hash
    when Array
      n, col = c
    # A custom col {name: col}, col is a Hash
    when Hash
      n, col = c.to_a[0]
    end
    if col.nil?
      Waxx.debug "Column #{c} not defined in #{@object}."
      #raise "Column #{c} not defined in #{@object}."
    end
    #Waxx.debug @relations.inspect
    #TODO: Deal with relations that have different names than the tables
    col[:views] << self rescue col[:views] = [self]
    @columns[n.to_sym] = col
  }
  @joins ||= Hash[@relations.map{|n, r| [n, %(#{r/:join} JOIN #{r/:foreign_table} AS #{n} ON #{r/:table}.#{r/:col} = #{n}.#{r/:foreign_col})] }]
end

#init(tbl: nil, cols: nil, layouts: nil) ⇒ Object

Initialize a view. This is normally done automatically when calling `has`.

Call init if the table, object, or layouts are non-standard. You can also set the attrs directly like `@table = 'usr'`

“` tbl: The name of the table cols: Same as has layouts: The layouts to auto-generate using waxx defaults: json, csv, tab, etc. “`



76
77
78
79
80
81
82
83
# File 'waxx/view.rb', line 76

def init(tbl: nil, cols: nil, layouts: nil)
  @table = (tbl || App.table_from_class(name)).to_sym
  @object = App.get_const(App, @table)
  @relations = {}
  @orders = {}
  has(*cols) if cols
  as(layouts) if layouts
end

#joins_to_sqlObject

Turn the @joins attribute into SQL for the JOIN clause



213
214
215
216
# File 'waxx/view.rb', line 213

def joins_to_sql()
  return nil if @joins.nil? or @joins.empty?
  @joins.map{|n,v| v}.join(" ")
end

#match_in(*cols) ⇒ Object

An array of columns to match in when passed in as params



246
247
248
# File 'waxx/view.rb', line 246

def match_in(*cols)
  @matches = cols.flatten
end

#not_found(x, data: {}, message: {type: "NotFound", message:"The record you requested was not found."}, as: x.ext) ⇒ Object

Send a not found message back to the client using the appropriate layout (json, csv, etc)



380
381
382
# File 'waxx/view.rb', line 380

def not_found(x, data:{}, message: {type: "NotFound", message:"The record you requested was not found."}, as:x.ext)
  self.const_get(as.to_s.capitalize).not_found(x, data:{}, message: message)
end

#parse_col(str) ⇒ Object

Parse a column (internal method used by col)



187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
# File 'waxx/view.rb', line 187

def parse_col(str)
  nam = rel = col = nil
  parts = str.split(/[:\.]/)
  case str
  # alias:relationship.column
  when /^\w+:\s*\w+\.\w+$/
    nam, rel, col = str.split(/[:\.]/).map{|part| part.strip}
  # relationship.column
  when /^\w+\.\w+$/
    rel, col = str.split(".")
  # alias:column (from primary object/table)
  when /^\w+:\w+$/
    nam, col = str.split(":")
  # column (from primary object/table)
  when /^\w+$/
    col = str
  else
    raise "Could not parse column definition in Waxx::View.parse_col (#{name}). Unknown match: #{str}."
  end             
  nam = col if nam.nil? 
  rel = @table if rel.nil?
  [nam, rel, col]
end

#put_post(x, id, data, args: nil, returning: nil) ⇒ Object Also known as: post, put

Save data



355
356
357
# File 'waxx/view.rb', line 355

def put_post(x, id, data, args:nil, returning: nil)
  @object.put_post(x, id, data, view: self, returning: returning)
end

#render(x, data, message: {}, as: x.ext, meth: x.meth) ⇒ Object

Render the view using the layout for meth and as

`render(x, data, as: 'json', meth: 'get')`

Uses logical defaults based on x.req



373
374
375
376
# File 'waxx/view.rb', line 373

def render(x, data, message: {}, as:x.ext, meth:x.meth)
  return App.not_found(x) unless const_defined?(as.to_s.capitalize)
  const_get(as.to_s.capitalize).send(meth, x, data, message: message)
end

#run(x, id: nil, data: nil, where: nil, having: nil, order: nil, limit: nil, offset: nil, message: {}, as: x.ext, meth: x.meth, args: {}) ⇒ Object Also known as: view

Gets the data for the view and displays it. This is just a shortcut method.

This is normally called from the handler method defined in Object

“` App::Usr::List.run(x) # Given a get request with the json extention, the above is a shortcut to: data = App::Usr::List.get(x) App::Usr::List::Json.get(x, data) “`



273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
# File 'waxx/view.rb', line 273

def run(x, id:nil, data:nil, where:nil, having:nil, order:nil, limit:nil, offset:nil, message:{}, as:x.ext, meth:x.meth, args:{})
  case meth.to_sym
  when :get, :head
    if data.nil? or data.empty?
      if id
        data = get_by_id(x, id)
      else
        data = get(x, where:where, having:having, order:(order||x['order']), limit:(limit||x['limit']), offset:(offset||x['offset']), args:args)
      end
    end
  when :put, :post, :patch
    data = put_post(x, id, data, args:args)
  when :delete
    delete(x, id, args:args)
  else
    raise "Unknown request method in Waxx::View.run(#{name})"
  end
  layout = const_get(as.to_s.capitalize) rescue nil
  return App.not_found(x, message:"No layout defined for #{as}") if not layout
  if layout.respond_to? meth
    render(x, data, message: message, as: as, meth: meth) 
  else
    render(x, data, message: message, as: as, meth: "get") 
  end
end

#search_in(*cols) ⇒ Object

Any array of columns to automatically search in using the “q” parameter



252
253
254
# File 'waxx/view.rb', line 252

def search_in(*cols)
  @searches = cols.flatten
end

#string_to_col(str) ⇒ Object

Column defined as a string in the format: name:foreign_table.foreign_col Converted to SQL: foreign_table.foreign_col AS name Also adds entries in the @relations hash. @relations drive the SQL join statement Joins are defined in the primary object of this view.



146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
# File 'waxx/view.rb', line 146

def string_to_col(str)
  n, rel_name, col_name = parse_col(str)
  # Look in the primary and related objects for relations
  j = @object.joins/rel_name || @relations.values.map{|foreign_rel| 
    #Waxx.debug "REL: #{foreign_rel}"
    o = App.get_const(App, foreign_rel/:foreign_table)
    #Waxx.debug o
    #Waxx.debug o.joins.inspect
    o.joins/rel_name
  }.compact.first
  #Waxx.debug "j:#{j.inspect}, n: #{n}, rel: #{rel_name}, col: #{col_name}"
  begin
    col = (App.get_const(App, j/:foreign_table)/col_name).dup
    col[:table] = rel_name
  rescue NoMethodError => e
    Waxx.debug "ERROR: NoMethodError: #{rel_name} does not define col: #{col_name}"
    raise e
  rescue NameError, TypeError => e
    Waxx.debug "ERROR: Name or Type Error: #{rel_name} does not define col: #{col_name}"
    raise e
  end
  begin
    @relations[rel_name] ||= j #(App.get_const(App, j/:table)).joins
    @orders[n] = App.get_const(App, j/:foreign_table).orders/n
    @orders["_#{n}"] = App.get_const(App, j/:foreign_table).orders/"_#{n}"
    #col[:table] = rel_name 
  rescue NoMethodError => e
    if col.nil?
      Waxx.debug "col is nil"
    else
      Waxx.debug "ERROR: App[#{col[:table]}] has no joins in View.has"
    end
    raise e
  end
  #Waxx.debug n
  #Waxx.debug col
  [n, col]
end