359 lines
12 KiB
Ruby
Executable File
359 lines
12 KiB
Ruby
Executable File
#!/usr/bin/env ruby
|
|
# encoding: utf-8
|
|
# ruby: 3.0.2
|
|
=begin
|
|
backend to query part database
|
|
|
|
Copyright (C) 2023 King Kévin <kingkevin@cuvoodoo.info>
|
|
SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
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 = false
|
|
# 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"
|
|
# port for this service
|
|
PORT = 4245
|
|
|
|
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, PORT
|
|
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
|