partdb/server.rb

354 lines
12 KiB
Ruby
Raw Normal View History

#!/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'
2023-01-26 11:55:56 +01:00
# allow dumping crashes in browser
DEBUG = true
2023-01-26 11:55:56 +01:00
# maximum number of parts returned
PARTS_LIMIT = 100
2023-01-26 11:55:56 +01:00
# credentials for database
CREDENTIALS = "credentials.json"
2023-01-26 11:55:56 +01:00
# 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)
2023-01-26 07:17:20 +01:00
return nil unless id
2023-01-28 05:12:22 +01:00
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 = ?")
2023-01-25 07:48:36 +01:00
part = statement.execute(id).to_a[0]
return nil unless part
2023-01-26 07:17:20 +01:00
parent = get_part_by_id(part["parent"])
# merge parent info
if parent then
part.each do |k,v|
part[k] ||= parent[k]
end
end
2023-01-25 07:47:48 +01:00
# 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
2023-01-25 08:46:21 +01:00
# 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
2023-01-25 11:22:26 +01:00
# 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 = ?")
2023-01-26 07:17:20 +01:00
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
2023-01-25 11:22:26 +01:00
end
end
2023-01-26 11:55:56 +01:00
# add attachments
part["attachments"] = []
dir = PUBLIC + "/" + ATTACHMENTS + "/" + part["name"]
2023-01-28 02:41:32 +01:00
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
2023-01-26 11:55:56 +01:00
end
if parent then
part["attachments"] += parent["attachments"]
end
# clean up
2023-01-25 05:15:15 +01:00
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}
2023-01-26 00:42:54 +01:00
halt 400 if terms.empty?
2023-01-25 11:58:17 +01:00
# search in names, description, and category
statements = []
statements << @db.prepare("SELECT id FROM part WHERE name LIKE ?")
2023-01-28 00:20:29 +01:00
statements << @db.prepare("SELECT id FROM part WHERE mpn LIKE ?")
2023-01-25 11:58:17 +01:00
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
2023-01-25 11:58:17 +01:00
# 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
2023-01-26 00:42:54 +01:00
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
2023-01-27 02:04:03 +01:00
post '/part' do
request.body.rewind
begin
part = JSON.parse(request.body.read)
rescue
halt 401, "not json"
end
2023-01-28 00:34:12 +01:00
puts part if DEBUG
2023-01-27 02:04:03 +01:00
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 = ?")
2023-01-28 00:06:38 +01:00
halt(401, "name already existing") unless statement.execute(part["name"]).to_a.empty?
2023-01-27 02:04:03 +01:00
insert = @db.prepare("INSERT INTO part (name) VALUES (?)");
insert.execute(part["name"])
part["id"] = statement.execute(part["name"]).to_a[0]["id"]
end
2023-01-28 00:06:38 +01:00
# 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?
2023-01-28 00:19:33 +01:00
update = @db.prepare("UPDATE part SET #{field} = ? WHERE id = ?")
update.execute(family[0]["id"], part["id"])
2023-01-28 00:06:38 +01:00
family = get_part_by_id(family[0]["id"])
end
2023-01-27 02:04:03 +01:00
# update fields
2023-01-28 00:19:33 +01:00
fields_txt = ["name", "description", "details", "mpn", "pincount", "datasheet", "page"];
2023-01-27 02:04:03 +01:00
fields_txt.each do |field|
2023-01-28 00:06:38 +01:00
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
2023-01-28 02:41:32 +01:00
next if family and family[field] == part[field]
2023-01-27 02:04:03 +01:00
update = @db.prepare("UPDATE part SET #{field} = ? WHERE id = ?")
2023-01-28 00:19:33 +01:00
update.execute(part[field], part["id"])
2023-01-27 02:04:03 +01:00
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
2023-01-28 02:41:32 +01:00
next if family and family[field] == part[field]
2023-01-27 02:04:03 +01:00
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
2023-01-27 23:15:12 +01:00
ref = statement.execute(part[field].downcase).to_a[0]
2023-01-27 02:04:03 +01:00
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
2023-01-27 23:15:12 +01:00
ref = statement.execute(part[field].downcase).to_a[0]
2023-01-27 02:04:03 +01:00
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
2023-01-28 02:41:32 +01:00
insert = @db.prepare("INSERT INTO inventory (part, location, quantity) VALUES (?,?,?)");
insert.execute(part["id"], ref["id"], part["stock"].to_i)
2023-01-27 02:04:03 +01:00
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
2023-01-28 00:34:12 +01:00
# 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) = ?")
2023-01-28 05:00:38 +01:00
ref = statement.execute(distributor.downcase).to_a[0]
2023-01-28 00:34:12 +01:00
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
2023-01-27 23:15:41 +01:00
# 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|
2023-01-28 02:41:32 +01:00
next unless values and !values.empty?
2023-01-27 23:15:41 +01:00
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|
2023-01-28 02:41:32 +01:00
next if family and family["properties"] and family["properties"][name].include?(value)
2023-01-27 23:15:41 +01:00
insert.execute(ref["id"], part["id"], value)
end
end
end
return 200
2023-01-27 02:04:03 +01:00
end