module Scaffold

Public Instance Methods

copy_template(name, details) click to toggle source
# File lib/surikat/scaffold.rb, line 491
def copy_template name, details
  destination_path = details[:path]
  vars             = details[:vars] || {}
  file             = "#{__dir__}/templates/#{name}.tmpl"
  dest             = "#{destination_path}/#{details[:new_name] || name}"

  text = File.open(file).read

  File.open(dest, 'w') {|f| f.write(text % vars)}
end
destroy_queries(model_name) click to toggle source
# File lib/surikat/scaffold.rb, line 242
def destroy_queries(model_name)
  file_name = ActiveSupport::Inflector.singularize(model_name.underscore) + '_queries.rb'
  file_path = "queries/#{file_name}"

  print "Deleting queries file: #{file_path}... "

  begin
    File.unlink FileUtils.pwd + '/app/' + file_path
  rescue Exception => e
    puts "fail: #{e.message}"
    false
  else
    puts "ok"
    true
  end
end
destroy_routes(model_name) click to toggle source
# File lib/surikat/scaffold.rb, line 126
def destroy_routes(model_name)
  class_name        = ActiveSupport::Inflector.camelize(model_name)
  class_name_plural = ActiveSupport::Inflector.pluralize(class_name)

  routes = Routes.new

  print "Destroying query and mutation routes... "

  begin
    routes.delete_query class_name
    routes.delete_query class_name_plural
    routes.delete_mutation class_name
    routes.delete_mutation "Update#{class_name}"
    routes.delete_mutation "Delete#{class_name}"
  rescue Exception => e
    puts "fail: #{e.message}, #{e.backtrace.first}"
    false
  else
    puts "ok"
    true
  end
end
destroy_types(model_name) click to toggle source
# File lib/surikat/scaffold.rb, line 335
def destroy_types(model_name)
  print "Deleting types... "

  begin
    type_name = ActiveSupport::Inflector.camelize(model_name)
    types     = Types.new

    types.delete(type_name)
    types.delete("#{type_name}Input")
  rescue Exception => e
    puts "fail: #{e.message}, #{e.backtrace.first}"
    false
  else
    puts "ok"
    true
  end
end
generate_aaa() click to toggle source
# File lib/surikat/scaffold.rb, line 474
def generate_aaa
  model_name = 'user'
  arguments  = ['email:string', 'hashed_password:string', 'roleids:string']

  if generate_model(model_name, arguments, is_aaa: true) &&
      generate_types(model_name, arguments, is_aaa: true) &&
      generate_queries(model_name) &&
      generate_aaa_queries &&
      generate_routes(model_name, is_aaa: true)
    generate_tests(model_name, arguments, is_aaa: true)

    puts "Done."
  else
    puts "Partially done."
  end
end
generate_aaa_queries() click to toggle source
# File lib/surikat/scaffold.rb, line 219
def generate_aaa_queries
  file_name          = 'aaa_queries.rb'
  file_path          = "app/queries/#{file_name}"
  absolute_path      = FileUtils.pwd + '/app/queries/'
  file_absolute_path = absolute_path + file_path

  vars = {
      time: Time.now.to_s
  }

  print "Creating AAA queries file: #{file_name}... "

  if File.exists?(file_absolute_path)
    puts "exists"
    return true
  end

  copy_template file_name, {path: 'app/queries', vars: vars}

  puts "ok"
  true
end
generate_model(model_name, arguments, is_aaa: false) click to toggle source
# File lib/surikat/scaffold.rb, line 370
def generate_model(model_name, arguments, is_aaa: false)
  class_name         = ActiveSupport::Inflector.camelize(model_name)
  file_name          = ActiveSupport::Inflector.singularize(model_name.underscore) + '.rb'
  file_path          = "models/#{file_name}"
  absolute_path      = FileUtils.pwd + '/app/'
  file_absolute_path = absolute_path + file_path

  return false unless make_create_migration("create_#{model_name}", arguments)

  FileUtils.mkdir_p absolute_path + 'models'

  print "Creating model file: #{file_name}... "

  if File.exists?(file_absolute_path)
    puts "exists"
    return true
  end

  template = is_aaa ? 'base_aaa_model.rb' : 'base_model.rb'

  copy_template template, {new_name: file_name, path: 'app/models', vars: {class_name: class_name}}

  puts "ok"
  return true
end
generate_queries(model_name) click to toggle source
# File lib/surikat/scaffold.rb, line 149
def generate_queries(model_name)
  var_name           = ActiveSupport::Inflector.singularize(model_name.underscore)
  class_name         = ActiveSupport::Inflector.camelize(model_name)
  class_name_plural  = ActiveSupport::Inflector.pluralize(class_name)
  file_name          = ActiveSupport::Inflector.singularize(model_name.underscore) + '_queries.rb'
  file_path          = "queries/#{file_name}"
  absolute_path      = FileUtils.pwd + '/app/'
  file_absolute_path = absolute_path + file_path

  FileUtils.mkdir_p absolute_path + 'models'

  print "Generating query file #{file_path}... "

  if File.exists?(file_absolute_path)
    puts "exists"
    return true
  end

  all_types = Types.new.all

  input_type_detailed = all_types["#{class_name}Input"]['arguments'].map do |field, type|
    "   '#{field}' => #{type}"
  end.join("\n")

  input_type_detailed_no_id = all_types["#{class_name}Input"]['arguments'].map do |field, type|
    next if field == 'id'
    "   '#{field}' => #{type}"
  end.compact.join("\n")

  fields = all_types[class_name]['fields'].keys

  examples = {
      get:    "{\n  #{class_name}(id: 123) {\n" + fields.map {|f| "    #{f}"}.join("\n") + "\n  }\n}",
      list:   "{\n  #{class_name_plural}(q: \"id_lt=100\") {\n" + fields.map {|f| "    #{f}"}.join("\n") + "\n  }\n}",
      create: "mutation #{class_name}($#{var_name}: #{class_name}Input) {\n  #{class_name}(#{var_name}: $#{var_name}) {\n" + fields.map {|f| "    #{f}"}.join("\n") + "\n  }\n}",
      update: "mutation Update#{class_name}($#{var_name}: #{class_name}Input) {\n  Update#{class_name}(#{var_name}: $#{var_name}) {\n" + fields.map {|f| "    #{f}"}.join("\n") + "\n  }\n}",
      delete: "mutation Delete#{class_name}($id: ID) {\n  Delete#{class_name}(id: $id)\n}"
  }

  update_vars = {var_name => {}}
  all_types["#{class_name}Input"]['arguments'].each {|a, t| update_vars[var_name][a] = random_values(t)}

  create_vars = {var_name => {}}
  all_types["#{class_name}Input"]['arguments'].each {|a, t| create_vars[var_name][a] = random_values(t)}
  create_vars[var_name].delete 'id'

  vars = {
      examples:                  examples,
      time:                      Time.now.to_s,
      class_name_downcase:       class_name.underscore,
      class_name:                class_name,
      class_name_plural:         class_name_plural,
      input_type_detailed:       input_type_detailed,
      pretty_update_vars:        JSON.pretty_generate(update_vars),
      pretty_create_vars:        JSON.pretty_generate(create_vars),
      pretty_random_id:          JSON.pretty_generate({'id' => random_values('ID')}),
      input_type_detailed_no_id: input_type_detailed_no_id,
      examples_get:              examples[:get],
      examples_list:             examples[:list],
      examples_create:           examples[:create],
      examples_update:           examples[:update],
      examples_delete:           examples[:delete]
  }

  copy_template 'crud_queries.rb', {new_name: file_name, path: 'app/queries', vars: vars}

  puts "ok"
  true
end
generate_routes(model_name, is_aaa: false) click to toggle source
# File lib/surikat/scaffold.rb, line 5
def generate_routes(model_name, is_aaa: false)
  print "Generating query and migration routes... "

  routes = Routes.new

  class_name = ActiveSupport::Inflector.camelize(model_name)

  begin
    # get one query
    routes.merge_query({class_name => {
        'class'       => "#{class_name}Queries",
        'method'      => 'get',
        'output_type' => "#{class_name}",
        'arguments'   => {'id' => 'ID'}
    }})

    # get all query
    class_name_plural = ActiveSupport::Inflector.pluralize(class_name)
    routes.merge_query({class_name_plural => {
        'class'       => "#{class_name}Queries",
        'method'      => 'all',
        'output_type' => "[#{class_name}]",
        'arguments'   => {'q' => 'String'}
    }})

    # create mutation
    routes.merge_mutation({class_name => {
        'class'       => "#{class_name}Queries",
        'method'      => 'create',
        'output_type' => "#{class_name}",
        'arguments'   => {class_name => "#{class_name}Input"}
    }})

    # update mutation
    routes.merge_mutation({"Update#{class_name}" => {
        'class'       => "#{class_name}Queries",
        'method'      => 'update',
        'output_type' => "Boolean",
        'arguments'   => {class_name => "#{class_name}Input"}
    }})

    # delete mutation
    routes.merge_mutation({"Delete#{class_name}" => {
        'class'       => "#{class_name}Queries",
        'method'      => 'delete',
        'output_type' => "Boolean",
        'arguments'   => {'id' => 'ID'}
    }})

    if is_aaa # add extra routes for AAA
      routes.merge_query({'Authenticate' => {
          "class"       => "AAAQueries",
          "method"      => "authenticate",
          "output_type" => "Boolean",
          "arguments"   => {
              "email"    => "String",
              "password" => "String"
          }
      }})

      routes.merge_query({'Logout' => {
          "class"           => "AAAQueries",
          "method"          => "logout",
          "output_type"     => "Boolean",
          "permitted_roles" => "any"
      }})

      routes.merge_query({'CurrentUser' => {
          "class"           => "AAAQueries",
          "method"          => "current_user",
          "output_type"     => "User",
          "permitted_roles" => "any"
      }})

      routes.merge_query({'LoginAs' => {
          "class"           => "AAAQueries",
          "method"          => "login_as",
          "output_type"     => "Boolean",
          "permitted_roles" => ["superadmin"],
          "arguments"       => {
              "user_id" => "ID"
          }
      }})

      routes.merge_query({'BackFromLoginAs' => {
          "class"           => "AAAQueries",
          "method"          => "back_from_login_as",
          "output_type"     => "Boolean",
          "permitted_roles" => "any"
      }})

      routes.merge_query({'DemoOne' => {
          "class"           => "AAAQueries",
          "method"          => "demo_one",
          "output_type"     => "String",
          "permitted_roles" => "any"
      }})

      routes.merge_query({'DemoTwo' => {
          "class"           => "AAAQueries",
          "method"          => "demo_two",
          "output_type"     => "String",
          "permitted_roles" => ["hotdog", "hamburger"]
      }})

      routes.merge_query({'DemoThree' => {
          "class"           => "AAAQueries",
          "method"          => "demo_three",
          "output_type"     => "String",
          "permitted_roles" => ["worker"]
      }})
    end # if is_aaa
  rescue Exception => e
    puts "fail: #{e.message}, #{e.backtrace.first}"
    false
  else
    puts "ok"
    true
  end
end
generate_scaffold(arguments) click to toggle source
# File lib/surikat/scaffold.rb, line 437
def generate_scaffold arguments
  model_name = arguments.shift
  if model_name.to_s.empty?
    puts "Syntax: surikat generate scaffold model_name field1:type1 field2:type2..."
    return
  end

  unless model_name =~ /^[\p{L}_][\p{L}\p{N}@$#_]{0,127}$/
    puts "'#{model_name}' does not appear to be a valid."
    return
  end

  valid_types = %w(integer float string boolean date datetime decimal binary bigint primary_key references string text time timestamp)

  arguments.each do |arg|
    field_name, field_type = arg.split(':')
    unless field_name =~ /^[\p{L}_][\p{L}\p{N}@$#_]{0,127}$/
      puts "'#{field_name} does not appear to be valid'"
      return
    end
    unless valid_types.include?(field_type)
      puts "'#{field_type}' does not appear to be valid. Valid field types are: #{valid_types.join(', ')}"
    end
  end

  if generate_model(model_name, arguments) &&
      generate_types(model_name, arguments) &&
      generate_queries(model_name) &&
      generate_routes(model_name) &&
      generate_tests(model_name, arguments)
    puts "Done."
  else
    puts "Partially done."
  end

end
generate_tests(model_name, arguments, is_aaa: false) click to toggle source
# File lib/surikat/scaffold.rb, line 396
def generate_tests(model_name, arguments, is_aaa: false)

  class_name         = ActiveSupport::Inflector.camelize(model_name)
  class_name_plural  = ActiveSupport::Inflector.pluralize(class_name)
  model_name_plural  = ActiveSupport::Inflector.pluralize(model_name.underscore)
  file_name          = ActiveSupport::Inflector.singularize(model_name.underscore) + '_spec.rb'
  file_path          = "spec/#{file_name}"
  absolute_path      = FileUtils.pwd + '/app/'
  file_absolute_path = absolute_path + file_path
  columns            = (arguments.to_a.map {|a| a.split(':').first} + %w(id created_at updated_at)).uniq

  columns            -= ['hashed_password'] if is_aaa

  print "Creating rspec tests: #{file_name}... "

  if File.exists?(file_absolute_path)
    puts "exists"
    return true
  end

  copy_template 'base_spec.rb', {
      new_name: file_name,
      path:     'spec',
      vars:     {class_name:       class_name, class_name_plural: class_name_plural,
                 model_name:       model_name.underscore, model_name_plural: model_name_plural,
                 columns_new_line: columns.join("\n           "),
                 columns_space:    columns.map {|c| "'#{c}'"}.join(', ')
      }
  }

  puts "ok"

  if is_aaa
    print "Creating AAA rspec tests: spec/aaa_spec.rb... "
    copy_template 'aaa_spec.rb', {path: 'spec'}
    puts "ok"
  end

  true
end
generate_types(model_name, arguments, is_aaa: false) click to toggle source
# File lib/surikat/scaffold.rb, line 259
def generate_types(model_name, arguments, is_aaa: false)
  type_name = ActiveSupport::Inflector.camelize(model_name)
  types     = Types.new

  fields = {}
  (arguments + ['id:ID']).each do |arg|
    field_name, field_type = arg.split(':').map(&:strip)

    next if field_name.in?(%w(password hashed_password)) && is_aaa # never expose the hashed password

    field_type = 'ID' if field_name[-3, 3] == '_id'

    graphql_type = case field_type
                   when 'ID'
                     'ID'
                   when 'integer'
                     'Int'
                   when 'float'
                     'Float'
                   when 'string', 'text'
                     'String'
                   when 'boolean'
                     'Boolean'
                   else
                     'String'
                   end

    fields[field_name] = graphql_type
  end

  print "Generating output type... "

  if types.all.keys.include?(type_name)
    puts "exists"
  else
    begin
      types.merge type_name => {
          'type'   => 'Output',
          'fields' => fields.clone
      }
    rescue Exception => e
      puts "fail: #{e.message}, #{e.backtrace.first}"
      return false
    else
      puts "ok"
    end
  end

  print "Generating input type... "

  if types.all.keys.include?("#{type_name}Input")
    puts "exists"
    true
  else
    input_fields = fields.clone

    if is_aaa
      input_fields.merge!({'password' => 'String'})
      input_fields.delete('hashed_password')
    end

    begin
      types.merge "#{type_name}Input" => {
          'type'      => 'Input',
          'arguments' => input_fields
      }
    rescue Exception => e
      puts "fail: #{e.message}, #{e.backtrace.first}"
      false
    else
      puts "ok"
      true
    end
  end
end
make_create_migration(migration_name, arguments) click to toggle source
# File lib/surikat/scaffold.rb, line 353
def make_create_migration(migration_name, arguments)
  print "Creating migration #{migration_name}... "

  arguments << 'created_at:datetime'
  arguments << 'updated_at:datetime'

  begin
    StandaloneMigrations::Generator.migration migration_name, arguments
  rescue Exception => e
    puts " error: #{e.message}"
    return false
  else
    puts "ok"
    true
  end
end