partdb/server.rb

416 lines
14 KiB
Ruby
Raw Normal View History

#!/usr/bin/env ruby
# encoding: utf-8
# ruby: 3.0.2
=begin
2023-01-28 05:56:17 +01:00
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'
2023-01-29 09:56:49 +01:00
require 'uri'
require 'net/http'
2023-01-26 11:55:56 +01:00
# allow dumping crashes in browser
2023-01-28 07:00:44 +01:00
DEBUG = false
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"
2023-01-28 07:00:07 +01:00
# port for this service
2023-01-28 10:15:36 +01:00
PORT = 4245
2023-01-26 11:55:56 +01:00
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'
2023-01-28 07:00:07 +01:00
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)
2023-01-26 07:17:20 +01:00
return nil unless id
2023-01-29 10:09:42 +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.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
2023-01-28 10:16:51 +01:00
part["attachments"].sort!
2023-01-26 11:55:56 +01:00
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
def add_part(part)
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 = ?")
raise StandardError.new("id not valid") if statement.execute(part["id"]).to_a.empty?
2023-01-27 02:04:03 +01:00
else
# add new part
raise StandardError.new("name required") unless part["name"] and part["name"].length > 0
2023-01-27 02:04:03 +01:00
statement = @db.prepare("SELECT id FROM part WHERE name = ?")
raise StandardError.new("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
raise StandardError.new("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-29 10:09:42 +01:00
fields_txt = ["name", "description", "details", "mpn", "package", "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
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
2023-01-29 10:09:42 +01:00
field_ref = ["manufacturer"]
2023-01-27 02:04:03 +01:00
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]
raise StandardError.new("distributor unknown") unless ref
2023-01-28 00:34:12 +01:00
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
end
post '/part' do
request.body.rewind
begin
part = JSON.parse(request.body.read)
rescue
halt 401, "not json"
end
puts part if DEBUG
begin
add_part(part)
rescue StandardError => e
halt 401, e.message
end
2023-01-27 23:15:41 +01:00
return 200
2023-01-27 02:04:03 +01:00
end
2023-01-29 09:56:49 +01:00
get '/import/lcsc/:lcsc' do
halt 401 unless params['lcsc'] and params['lcsc'] =~ /^C\d+$/i
uri = URI("https://wmsc.lcsc.com/wmsc/product/detail?productCode=#{params['lcsc']}")
res = Net::HTTP.get_response(uri)
halt 401, "could not get part" unless res.is_a?(Net::HTTPSuccess)
json = JSON.parse(res.body)
#puts json
halt 401, "part not found" unless 200 == json["code"] and json["result"]
result = json["result"]
part = {}
part["name"] = result["productModel"]
part["mpn"] = result["productModel"]
part["description"] = result["productDescEn"]
part["details"] = result["productIntroEn"]
part["manufacturer"] = result["brandNameEn"]
2023-01-29 10:09:42 +01:00
part["package"] = result["encapStandard"]
2023-01-29 09:56:49 +01:00
part["distributors"] = {"LCSC" => result["productCode"]}
part["attachments"] = result["productImages"]
part["datasheet"] = result["pdfUrl"]
existing = get_part_by_name(part["name"])
halt 401, "part name already exists" if existing
puts part
begin
add_part(part)
rescue StandardError => e
halt 401, e.message
end
i = 0
(part["attachments"] + [part["datasheet"]]).each do |attachement|
file = attachement.split("/")[-1]
dir = PUBLIC + "/" + ATTACHMENTS + "/" + part["name"]
path = "#{dir}/#{i}_#{file}"
i += 1
unless File.file?(path) then
uri = URI(attachement)
res = Net::HTTP.get_response(uri)
if (res.is_a?(Net::HTTPSuccess)) then
Dir.mkdir(dir) unless File.directory?(dir)
File.open(path, "wb") do |f|
f.write res.body
end
end
end
end
return 200
end