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:22:47 +01:00
statement = @db.prepare("SELECT part.id, part.name, part.description, part.details, part.datasheet, manufacturer.name AS manufacturer, part.mpn AS mpn, part.package, part.page AS page, part.family AS parent, p2.name AS family FROM part 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
begin
add_part(part)
rescue StandardError => e
halt 401, e.message
end
i = 0
(part["attachments"] + [part["datasheet"]]).each do |attachment|
next unless attachment
file = attachment.split("/")[-1]
2023-01-29 09:56:49 +01:00
dir = PUBLIC + "/" + ATTACHMENTS + "/" + part["name"]
path = "#{dir}/#{i}_#{file}"
i += 1
unless File.file?(path) then
uri = URI(attachment)
2023-01-29 09:56:49 +01:00
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, "#{part['name']} added"
2023-01-29 09:56:49 +01:00
end