#!/usr/bin/env ruby # encoding: utf-8 # ruby: 3.0.2 =begin server to query part database to install sinatra sudo pacman -S ruby-sinatra ruby-webrick pikaur -S ruby-mysql2 =end require 'set' require 'mysql2' require 'json' require 'sinatra' # allow dumping crashes in browser DEBUG = true # maximum number of parts returned PARTS_LIMIT = 100 # credentials for database CREDENTIALS = "credentials.json" # folder name for served pages PUBLIC = "public" # folder name for part attachments (in PUBLIC) ATTACHMENTS = "attachments" raise "database information #{CREDENTIALS} do not exist" unless File.file? CREDENTIALS # open server configure do if DEBUG then set :show_exceptions, true set :logging, true else set :show_exceptions, false set :environment, :production set :logging, false end set :protection, :except => :json_csrf set :bind, 'localhost' set :port, 4244 set :public_folder, "public" set :static, true end before do response.headers["Access-Control-Allow-Origin"] = "*" response.headers["Access-Control-Allow-Headers"] = "Content-Type" if request.request_method == 'OPTIONS' response.headers["Access-Control-Allow-Methods"] = "GET,POST" halt 200 end # all replies are only JSON content_type 'application/json' # open database credentials = {} JSON.parse(IO.read(CREDENTIALS)).each {|key,value| credentials[key.to_sym] = value} Mysql2::Client.default_query_options.merge!(:as => :hash) @db = Mysql2::Client.new(credentials) end after do response.headers["Access-Control-Allow-Origin"] = "*" response.headers["Access-Control-Allow-Headers"] = "Content-Type" end get '/' do redirect to('/index.html') end def get_part_by_id(id) return nil unless id statement = @db.prepare("SELECT part.id, part.name, part.description, part.details, part.datasheet, manufacturer.name AS manufacturer, part.mpn AS mpn, package.name AS package, part.pincount AS pincount, part.page AS page, part.family AS parent, p2.name AS family FROM part LEFT JOIN package ON package.id = part.package LEFT JOIN manufacturer ON manufacturer.id = part.manufacturer LEFT JOIN part AS p2 ON p2.id = part.family WHERE part.id = ?") part = statement.execute(id).to_a[0] return nil unless part parent = get_part_by_id(part["parent"]) # merge parent info if parent then part.each do |k,v| part[k] ||= parent[k] end end # add all distributors distributors = @db.query("SELECT * FROM distributor").to_a statement = @db.prepare("SELECT * FROM distribution WHERE part = ?") distributions = statement.execute(id).to_a distributors.each do |distributor| distributions.each do |distribution| if distribution["distributor"] == distributor["id"] then distributor["sku"] = distribution["sku"] distributor["url"] = distributor["product_page"].gsub("%s", distribution["sku"]) end end distributor.delete("id") distributor.delete("homepage") distributor.delete("product_page") end part["distributors"] = distributors # add inventory statement = @db.prepare("SELECT location.name AS location, inventory.quantity AS stock FROM inventory LEFT JOIN location ON location.id = inventory.location WHERE inventory.part = ? ORDER BY inventory.quantity DESC LIMIT 1") inventory = statement.execute(id).to_a[0] if inventory then part["location"] = inventory["location"] part["stock"] = inventory["stock"] end # add properties part["properties"] = {} statement = @db.prepare("SELECT property.name AS name, property_value.value AS value FROM property_value JOIN property ON property.id = property_value.property WHERE property_value.part = ?") statement.execute(id).each do |row| part["properties"][row["name"]] ||= [] part["properties"][row["name"]] << row["value"] end if parent then parent["properties"].each do |k,v| part["properties"][k] ||= v end end # add attachments part["attachments"] = [] dir = PUBLIC + "/" + ATTACHMENTS + "/" + part["name"] if File.directory?(dir) then Dir.entries(dir).each do |file| path = dir + "/" + file next unless File.file? path part["attachments"] << ATTACHMENTS + "/" + part["name"] + "/" + file end end if parent then part["attachments"] += parent["attachments"] end # clean up delete = ["parent"] delete.each do |k| part.delete k end return part end def get_part_by_name(name) statement = @db.prepare("SELECT id FROM part WHERE part.name = ?") id = statement.execute(name).to_a[0] if id then return get_part_by_id(id["id"]) else return nil end end get '/part/:name' do part = get_part_by_name(params['name']) halt 404 unless part part.to_json end get '/search/:terms' do terms = params['terms'].split(" ") terms.keep_if {|term| term.length >= 3} halt 400 if terms.empty? # search in names, description, and category statements = [] statements << @db.prepare("SELECT id FROM part WHERE name LIKE ?") statements << @db.prepare("SELECT id FROM part WHERE mpn LIKE ?") statements << @db.prepare("SELECT id FROM part WHERE description LIKE ?") statements << @db.prepare("SELECT property_value.part AS id FROM property_value JOIN property ON property.id = property_value.property WHERE property.name = 'category' AND property_value.value LIKE ?") term_ids = [] terms.each do |term| ids = Set.new # OR term location statements.each do |statement| statement.execute("%#{term}%").each do |row| ids << row["id"] end end term_ids << ids end # get all children statement = @db.prepare("SELECT id FROM part WHERE family IN (?)") term_ids.each do |term_id| statement.execute(term_id.to_a * ",").each do |row| term_id << row["id"] end end # AND terms ids = term_ids.shift term_ids.each do |term_id| ids &= term_id end parts = ids.collect {|id| get_part_by_id(id)} parts.compact! parts = parts[0, PARTS_LIMIT] parts.sort! {|x,y| x["name"] <=> y["name"]} parts.to_json end def delete_part(id) # first delete all children statement = @db.prepare("SELECT id FROM part WHERE family = ?") statement.execute(id).each do |row| puts "child: #{row['id']}" delete_part(row['id']) puts "deleted" end # delete all fields statements = [] statements << @db.prepare("DELETE FROM property_value WHERE part = ?") statements << @db.prepare("DELETE FROM assembly WHERE assembled = ?") statements << @db.prepare("DELETE FROM assembly WHERE component = ?") statements << @db.prepare("DELETE FROM distribution WHERE part = ?") statements << @db.prepare("DELETE FROM property_value WHERE part = ?") statements << @db.prepare("DELETE FROM inventory WHERE part = ?") statements << @db.prepare("DELETE FROM part WHERE id = ?") statements.each do |statement| statement.execute(id) end end get '/delete/:id' do statement = @db.prepare("SELECT id FROM part WHERE id = ?") result = statement.execute(params['id']) halt 400 if result.to_a.empty? delete_part(params['id']) return 200 end post '/part' do request.body.rewind begin part = JSON.parse(request.body.read) rescue halt 401, "not json" end puts part if DEBUG if part["id"] then # ensure part to update exists statement = @db.prepare("SELECT id FROM part WHERE id = ?") halt(401, "id not valid") if statement.execute(part["id"]).to_a.empty? else # add new part halt(401, "name required") unless part["name"] and part["name"].length > 0 statement = @db.prepare("SELECT id FROM part WHERE name = ?") halt(401, "name already existing") unless statement.execute(part["name"]).to_a.empty? insert = @db.prepare("INSERT INTO part (name) VALUES (?)"); insert.execute(part["name"]) part["id"] = statement.execute(part["name"]).to_a[0]["id"] end # update family family = nil field = "family" part[field] = nil if part[field] and 0 == part[field].length if part[field] then statement = @db.prepare("SELECT id FROM part WHERE name = ?") family = statement.execute(part[field]).to_a halt(401, "family not existing") if family.empty? update = @db.prepare("UPDATE part SET #{field} = ? WHERE id = ?") update.execute(family[0]["id"], part["id"]) family = get_part_by_id(family[0]["id"]) end # update fields fields_txt = ["name", "description", "details", "mpn", "pincount", "datasheet", "page"]; fields_txt.each do |field| next unless part[field] part[field] = nil if part[field].kind_of?(String) and 0 == part[field].length part[field] = part[field].to_i if part[field] and "pincount" == field next if family and family[field] == part[field] update = @db.prepare("UPDATE part SET #{field} = ? WHERE id = ?") update.execute(part[field], part["id"]) end # update manufacturer and package field_ref = ["manufacturer", "package"] field_ref.each do |field| part[field] = nil if part[field] and 0 == part[field].length next if family and family[field] == part[field] if part[field] then statement = @db.prepare("SELECT id FROM #{field} WHERE LOWER(name) = ?") ref = statement.execute(part[field].downcase).to_a[0] unless ref then insert = @db.prepare("INSERT INTO #{field} (name) VALUES (?)"); insert.execute(part[field]) end ref = statement.execute(part[field].downcase).to_a[0] update = @db.prepare("UPDATE part SET #{field} = ? WHERE id = ?") update.execute(ref["id"], part["id"]) else update = @db.prepare("UPDATE part SET #{field} = NULL WHERE id = ?") update.execute(part["id"]) end end # update inventory field = "location" part[field] = nil if part[field] and 0 == part[field].length part["location"] = nil if part["stock"] and 0 == part["stock"].length if part[field] then statement = @db.prepare("SELECT id FROM #{field} WHERE LOWER(name) = ?") ref = statement.execute(part[field].downcase).to_a[0] unless ref then insert = @db.prepare("INSERT INTO #{field} (name) VALUES (?)"); insert.execute(part[field]) end ref = statement.execute(part[field].downcase).to_a[0] statement = @db.prepare("SELECT id FROM inventory WHERE part = ? AND location = ?") ref_inv = statement.execute(part["id"], ref["id"]).to_a[0] unless ref_inv then insert = @db.prepare("INSERT INTO inventory (part, location, quantity) VALUES (?,?,?)"); insert.execute(part["id"], ref["id"], part["stock"].to_i) end ref_inv = statement.execute(part["id"], ref["id"]).to_a[0] update = @db.prepare("UPDATE inventory SET quantity = ? WHERE id = ?") update.execute(part["stock"].to_i, ref_inv["id"]) else delete = @db.prepare("DELETE FROM inventory WHERE part = ?") delete.execute(part["id"]) end # update distributors field = "distributors" part[field] = nil if part[field] and 0 == part[field].length delete = @db.prepare("DELETE FROM distribution WHERE part = ?") delete.execute(part["id"]) if part[field] then part[field].each do |distributor,sku| next unless sku and !sku.empty? statement = @db.prepare("SELECT id FROM distributor WHERE LOWER(name) = ?") ref = statement.execute(distributor.downcase).to_a[0] halt(401, "distributor unknown") unless ref insert = @db.prepare("INSERT INTO distribution (distributor,part,sku) VALUES (?,?,?)"); insert.execute(ref["id"], part["id"], sku) end end # update properties field = "properties" part[field] = nil if part[field] and 0 == part[field].length delete = @db.prepare("DELETE FROM property_value WHERE part = ?") delete.execute(part["id"]) if part[field] then part[field].each do |name,values| next unless values and !values.empty? statement = @db.prepare("SELECT id FROM property WHERE LOWER(name) = ?") ref = statement.execute(name.downcase).to_a[0] unless ref then insert = @db.prepare("INSERT INTO property (name) VALUES (?)"); insert.execute(name) end ref = statement.execute(name.downcase).to_a[0] insert = @db.prepare("INSERT INTO property_value (property,part,value) VALUES (?,?,?)"); values.each do |value| next if family and family["properties"] and family["properties"][name].include?(value) insert.execute(ref["id"], part["id"], value) end end end return 200 end