šŸŽ© You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP →
Sign In

@isoftdata/universal-object-htp-utility

Package Overview
Dependencies
Maintainers
11
Versions
70
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@isoftdata/universal-object-htp-utility - npm Package Compare versions

Comparing version
3.18.1
to
3.19.0
+52
category.ts
import { multiRowUpsertQuery, write } from './shared.js'
function convertCategory(categoryRow, productCode ) {
return {
productcode: productCode,
categoryid: categoryRow.categoryId,
inventorytypeid: categoryRow.inventoryTypeId,
category: categoryRow.category,
description: categoryRow.description,
partlistauthorityid: categoryRow.partListAuthorityId
}
}
export async function upsertCategoryRows(connection, categoryArr, productCode) {
const responses = []
try {
const convertedRows = categoryArr.map(row => convertCategory(row, productCode))
const multiRowUpsertResponse = await multiRowUpsertQuery(connection, 'category', convertedRows)
responses.push(`category affected rows: ${multiRowUpsertResponse.affectedRows}`)
return {
success: true,
data: responses
}
} catch (err) {
return {
success: false,
data: err
}
}
}
export async function deleteCategory(connection, productCode, categoryId) {
const responses = []
try {
const queryOptions = {
sql: `DELETE FROM category WHERE productcode = ? AND categoryid = ?`,
values: [productCode, categoryId],
}
const deleteResponse = await write(connection, queryOptions)
responses.push(`category delete affected rows: ${deleteResponse?.affectedRows}`)
return {
success: true,
data: responses
}
} catch (err) {
return {
success: false,
data: err
}
}
}
import { multiRowUpsertQuery, query, write } from './shared.js'
// This still expects companyinfo_replikwando to be setup ahead of time
function convertStore(row, productCode, companyInfoResults) {
const companyInfoMatch = companyInfoResults.find(element => element.store === row.id)
if (!companyInfoMatch) return
return {
address: row.physicalAddress?.street,
city: row.physicalAddress?.city,
companyname: row.name,
country: row.physicalAddress?.country,
faxnum: row.contact[0]?.fax,
//logoname: row.logoname,
phonenum: row.contact[0]?.phone,
productcode: productCode,
state: row.physicalAddress?.state,
store: row.id,
//system: row.system || "enterprise",
zip: row.physicalAddress?.zip,
// These were likely intended to come from Contact[]
contact: companyInfoMatch?.contact,
email: companyInfoMatch?.email,
latitude: companyInfoMatch?.latitude,
longitude: companyInfoMatch?.longitude,
website: companyInfoMatch?.website,
// Additional fields same as trigger and companyinfo_replikwando
accounttype: companyInfoMatch?.accounttype,
active: companyInfoMatch?.active,
admin_email: companyInfoMatch?.admin_email,
alt_phonenum: companyInfoMatch?.alt_phonenum,
buynow_active: companyInfoMatch?.buynow_active,
buynow_email: companyInfoMatch?.buynow_email,
buynow_message: companyInfoMatch?.buynow_message,
combined_site_public: companyInfoMatch?.combined_site_public,
ecommerce_portal: companyInfoMatch?.ecommerce_portal,
ERequest: companyInfoMatch?.ERequest,
fogbugz_case: companyInfoMatch?.fogbugz_case,
hosted_itrack: companyInfoMatch?.hosted_itrack,
htuserlogin: companyInfoMatch?.htuserlogin,
htpassword: companyInfoMatch?.htpassword,
image_source: companyInfoMatch?.image_source,
images: companyInfoMatch?.images,
invpartslimit: companyInfoMatch?.invpartslimit,
invsellvehicles: companyInfoMatch?.invsellvehicles,
linkDescription: companyInfoMatch?.linkDescription,
local_currency: companyInfoMatch?.local_currency,
local_host: companyInfoMatch?.local_host,
local_username: companyInfoMatch?.local_username,
local_password: companyInfoMatch?.local_password,
local_database: companyInfoMatch?.local_database,
local_port: companyInfoMatch?.local_port,
logoname: companyInfoMatch?.logoname,
no_price_message: companyInfoMatch?.no_price_message,
onlineinvactive: companyInfoMatch?.onlineinvactive,
Policies: companyInfoMatch?.Policies,
salvage_request_email: companyInfoMatch?.salvage_request_email,
schema_priceRange: companyInfoMatch?.schema_priceRange,
sellvehicles: companyInfoMatch?.sellvehicles,
signupdate: companyInfoMatch?.signupdate,
system: companyInfoMatch?.system,
system_factor: companyInfoMatch?.system_factor,
vendor_factor: companyInfoMatch?.vendor_factor,
vendorinfluence: companyInfoMatch?.vendorinfluence,
}
}
async function getCompanyInfo(connection, productCode) {
const queryOptions = {
sql: `SELECT * FROM companyinfo_replikwando WHERE productcode = ?`,
values: [productCode],
}
return await query(connection, queryOptions)
}
export async function upsertCompanyInfoRows(connection, storeArr, productCode) {
const responses = []
const companyInfoResults = await getCompanyInfo(connection, productCode)
if (!companyInfoResults.length) {
return {
success: false,
request: '',
data: `No companyinfo inserts because no rows found in companyinfo_replikwando for productcode ${productCode}`
}
}
try {
const convertedRows = storeArr.map(row => convertStore(row, productCode, companyInfoResults))
const filteredRows = convertedRows.filter(row => row)
if (filteredRows.length) {
const multiRowUpsertResponse = await multiRowUpsertQuery(connection, 'companyinfo', filteredRows)
responses.push(`companyinfo affected rows: ${multiRowUpsertResponse?.affectedRows}`)
} else {
responses.push(`companyinfo affected rows: 0, check companyinfo_replikwando`)
}
return {
success: true,
request: '',
data: responses
}
} catch (err) {
return {
success: false,
request: '',
data: err
}
}
}
export async function deleteCompanyInfo(connection, productCode, store) {
const responses = []
try {
const queryOptions = {
sql: `DELETE FROM companyinfo WHERE productcode = ? AND store = ?`,
values: [productCode, store],
}
const deleteResponse = await write(connection, queryOptions)
responses.push(`companyinfo affected rows: ${deleteResponse?.affectedRows}`)
return {
success: true,
request: '',
data: responses
}
} catch (err) {
return {
success: false,
request: '',
data: err
}
}
}
import { multiRowUpsertQuery, write } from './shared.js'
function convertCustomer(row, productCode ) {
return {
productcode: productCode,
phone: row.phone,
customerid: row.customerId,
storeid: row.storeId,
company: row.company,
name: row.name,
street: row.street,
city: row.city,
county: row.country,
state: row.state,
zip: row.zip,
country: row.country,
fax: row.fax,
email: row.email,
web: row.web,
comments: row.comments,
type: row.type,
defaulttermid: row.defaultTermId,
dateentered: row.dateEntered,
taxitemid: row.taxItemId,
defaultpricetype: row.defaultPriceType,
percentofprice: row.percentOfPrice,
porequired: row.poRequired,
blanketpurchaseorder: row.blanketPurchaseOrder,
blanketpurchaseorderexpiration: row.blanketPurchaseOrderExpiration,
webuserid: row.webUserId,
active: row.active === 'False' ? 'False' : 'True' // To cover Pro which doesn't have an active flag
}
}
export async function upsertCustomerRows(connection, customerArr, productCode) {
const responses = []
try {
const convertedRows = customerArr.map(row => convertCustomer(row, productCode))
const multiRowUpsertResponse = await multiRowUpsertQuery(connection, 'customer', convertedRows)
responses.push(`customer affected rows: ${multiRowUpsertResponse.affectedRows}`)
return {
success: true,
data: responses
}
} catch (err) {
return {
success: false,
data: err
}
}
}
export async function deleteCustomerRow(connection, productCode, customerId) {
const responses = []
try {
const queryOptions = {
sql: `DELETE FROM customer WHERE productcode = ? AND customerid = ?`,
values: [productCode, customerId],
}
const deleteResponse = await write(connection, queryOptions)
responses.push(`customer delete affected rows: ${deleteResponse?.affectedRows}`)
return {
success: true,
data: responses
}
} catch (err) {
return {
success: false,
data: err
}
}
}
import { multiRowUpsertQuery, write } from './shared.js'
function convertCustomerAddress(row, productCode ) {
return {
productcode: productCode,
...row
}
}
export async function upsertCustomerAddressRows(connection, customerAddressArr, productCode) {
const responses = []
try {
const convertedRows = customerAddressArr.map(row => convertCustomerAddress(row, productCode))
const multiRowUpsertResponse = await multiRowUpsertQuery(connection, 'customeraddress', convertedRows)
responses.push(`customeraddress affected rows: ${multiRowUpsertResponse.affectedRows}`)
return {
success: true,
data: responses
}
} catch (err) {
return {
success: false,
data: err
}
}
}
export async function deleteCustomerAddressRow(connection, productCode, customerAddressId) {
const responses = []
try {
const queryOptions = {
sql: `DELETE FROM customeraddress WHERE productcode = ? AND customeraddressid = ?`,
values: [productCode, customerAddressId],
}
const deleteResponse = await write(connection, queryOptions)
responses.push(`customeraddress delete affected rows: ${deleteResponse?.affectedRows}`)
return {
success: true,
data: responses
}
} catch (err) {
return {
success: false,
data: err
}
}
}
import { multiRowUpsertQuery, write } from './shared.js'
function convertCustomerOption(row, productCode ) {
return {
productcode: productCode,
customeroptionid: row.customerOptionId,
optiontype: row.optionType,
option: row.option,
showincustomerlist: row.showInCustomerList
}
}
export async function upsertCustomerOptionRows(connection, customerOptionArr, productCode) {
const responses = []
try {
const convertedRows = customerOptionArr.map(row => convertCustomerOption(row, productCode))
const multiRowUpsertResponse = await multiRowUpsertQuery(connection, 'customeroption', convertedRows)
responses.push(`customeroption affected rows: ${multiRowUpsertResponse.affectedRows}`)
return {
success: true,
data: responses
}
} catch (err) {
return {
success: false,
data: err
}
}
}
export async function deleteCustomerOptionRow(connection, productCode, customerOptionId) {
const responses = []
try {
const queryOptions = {
sql: `DELETE FROM customeroption WHERE productcode = ? AND customeroptionid = ?`,
values: [productCode, customerOptionId],
}
const deleteResponse = await write(connection, queryOptions)
responses.push(`customeroption delete affected rows: ${deleteResponse?.affectedRows}`)
return {
success: true,
data: responses
}
} catch (err) {
return {
success: false,
data: err
}
}
}
import { multiRowUpsertQuery, write } from './shared.js'
function convertCustomerOptionValue(row, productCode ) {
return {
productcode: productCode,
customeroptionvalueid: row.customerOptionValueId,
customeroptionid: row.customerOptionId,
value: row.value,
customerid: row.customerId
}
}
export async function upsertCustomerOptionValueRows(connection, customerOptionValueArr, productCode) {
const responses = []
try {
const convertedRows = customerOptionValueArr.map(row => convertCustomerOptionValue(row, productCode))
const multiRowUpsertResponse = await multiRowUpsertQuery(connection, 'customeroptionvalue', convertedRows)
responses.push(`customeroptionvalue affected rows: ${multiRowUpsertResponse.affectedRows}`)
return {
success: true,
data: responses
}
} catch (err) {
return {
success: false,
data: err
}
}
}
export async function deleteCustomerOptionValueRow(connection, productCode, customerOptionValueId) {
const responses = []
try {
const queryOptions = {
sql: `DELETE FROM customeroptionvalue WHERE productcode = ? AND customeroptionvalueid = ?`,
values: [productCode, customerOptionValueId],
}
const deleteResponse = await write(connection, queryOptions)
responses.push(`customeroptionvalue delete affected rows: ${deleteResponse?.affectedRows}`)
return {
success: true,
data: responses
}
} catch (err) {
return {
success: false,
data: err
}
}
}
import { multiRowUpsertQuery, write } from './shared.js'
function convertCustomerPriceContract(row, productCode ) {
return {
productcode: productCode,
customerpricecontractid: row.customerPriceContractId,
customerid: row.customerId,
inventoryid: row.inventoryId,
price: row.price,
startdate: row.startDate,
enddate: row.endDate
}
}
export async function upsertCustomerPriceContractRows(connection, customerPriceContractArr, productCode) {
const responses = []
try {
const convertedRows = customerPriceContractArr.map(row => convertCustomerPriceContract(row, productCode))
const multiRowUpsertResponse = await multiRowUpsertQuery(connection, 'customerpricecontract', convertedRows)
responses.push(`customerpricecontract affected rows: ${multiRowUpsertResponse.affectedRows}`)
return {
success: true,
data: responses
}
} catch (err) {
return {
success: false,
data: err
}
}
}
export async function deleteCustomerPriceContractRow(connection, productCode, customerPriceContractId) {
const responses = []
try {
const queryOptions = {
sql: `DELETE FROM customerpricecontract WHERE productcode = ? AND customerpricecontractid = ?`,
values: [productCode, customerPriceContractId],
}
const deleteResponse = await write(connection, queryOptions)
responses.push(`customerpricecontract delete affected rows: ${deleteResponse?.affectedRows}`)
return {
success: true,
data: responses
}
} catch (err) {
return {
success: false,
data: err
}
}
}
export declare function upsertCategoryRows(connection: any, categoryArr: any, productCode: any): Promise<{
success: boolean;
data: any;
}>;
export declare function deleteCategory(connection: any, productCode: any, categoryId: any): Promise<{
success: boolean;
data: any;
}>;
import { multiRowUpsertQuery, write } from './shared.js';
function convertCategory(categoryRow, productCode) {
return {
productcode: productCode,
categoryid: categoryRow.categoryId,
inventorytypeid: categoryRow.inventoryTypeId,
category: categoryRow.category,
description: categoryRow.description,
partlistauthorityid: categoryRow.partListAuthorityId
};
}
export async function upsertCategoryRows(connection, categoryArr, productCode) {
const responses = [];
try {
const convertedRows = categoryArr.map(row => convertCategory(row, productCode));
const multiRowUpsertResponse = await multiRowUpsertQuery(connection, 'category', convertedRows);
responses.push(`category affected rows: ${multiRowUpsertResponse.affectedRows}`);
return {
success: true,
data: responses
};
}
catch (err) {
return {
success: false,
data: err
};
}
}
export async function deleteCategory(connection, productCode, categoryId) {
const responses = [];
try {
const queryOptions = {
sql: `DELETE FROM category WHERE productcode = ? AND categoryid = ?`,
values: [productCode, categoryId],
};
const deleteResponse = await write(connection, queryOptions);
responses.push(`category delete affected rows: ${deleteResponse?.affectedRows}`);
return {
success: true,
data: responses
};
}
catch (err) {
return {
success: false,
data: err
};
}
}
export declare function upsertCompanyInfoRows(connection: any, storeArr: any, productCode: any): Promise<{
success: boolean;
request: string;
data: any;
}>;
export declare function deleteCompanyInfo(connection: any, productCode: any, store: any): Promise<{
success: boolean;
request: string;
data: any;
}>;
import { multiRowUpsertQuery, query, write } from './shared.js';
// This still expects companyinfo_replikwando to be setup ahead of time
function convertStore(row, productCode, companyInfoResults) {
const companyInfoMatch = companyInfoResults.find(element => element.store === row.id);
if (!companyInfoMatch)
return;
return {
address: row.physicalAddress?.street,
city: row.physicalAddress?.city,
companyname: row.name,
country: row.physicalAddress?.country,
faxnum: row.contact[0]?.fax,
//logoname: row.logoname,
phonenum: row.contact[0]?.phone,
productcode: productCode,
state: row.physicalAddress?.state,
store: row.id,
//system: row.system || "enterprise",
zip: row.physicalAddress?.zip,
// These were likely intended to come from Contact[]
contact: companyInfoMatch?.contact,
email: companyInfoMatch?.email,
latitude: companyInfoMatch?.latitude,
longitude: companyInfoMatch?.longitude,
website: companyInfoMatch?.website,
// Additional fields same as trigger and companyinfo_replikwando
accounttype: companyInfoMatch?.accounttype,
active: companyInfoMatch?.active,
admin_email: companyInfoMatch?.admin_email,
alt_phonenum: companyInfoMatch?.alt_phonenum,
buynow_active: companyInfoMatch?.buynow_active,
buynow_email: companyInfoMatch?.buynow_email,
buynow_message: companyInfoMatch?.buynow_message,
combined_site_public: companyInfoMatch?.combined_site_public,
ecommerce_portal: companyInfoMatch?.ecommerce_portal,
ERequest: companyInfoMatch?.ERequest,
fogbugz_case: companyInfoMatch?.fogbugz_case,
hosted_itrack: companyInfoMatch?.hosted_itrack,
htuserlogin: companyInfoMatch?.htuserlogin,
htpassword: companyInfoMatch?.htpassword,
image_source: companyInfoMatch?.image_source,
images: companyInfoMatch?.images,
invpartslimit: companyInfoMatch?.invpartslimit,
invsellvehicles: companyInfoMatch?.invsellvehicles,
linkDescription: companyInfoMatch?.linkDescription,
local_currency: companyInfoMatch?.local_currency,
local_host: companyInfoMatch?.local_host,
local_username: companyInfoMatch?.local_username,
local_password: companyInfoMatch?.local_password,
local_database: companyInfoMatch?.local_database,
local_port: companyInfoMatch?.local_port,
logoname: companyInfoMatch?.logoname,
no_price_message: companyInfoMatch?.no_price_message,
onlineinvactive: companyInfoMatch?.onlineinvactive,
Policies: companyInfoMatch?.Policies,
salvage_request_email: companyInfoMatch?.salvage_request_email,
schema_priceRange: companyInfoMatch?.schema_priceRange,
sellvehicles: companyInfoMatch?.sellvehicles,
signupdate: companyInfoMatch?.signupdate,
system: companyInfoMatch?.system,
system_factor: companyInfoMatch?.system_factor,
vendor_factor: companyInfoMatch?.vendor_factor,
vendorinfluence: companyInfoMatch?.vendorinfluence,
};
}
async function getCompanyInfo(connection, productCode) {
const queryOptions = {
sql: `SELECT * FROM companyinfo_replikwando WHERE productcode = ?`,
values: [productCode],
};
return await query(connection, queryOptions);
}
export async function upsertCompanyInfoRows(connection, storeArr, productCode) {
const responses = [];
const companyInfoResults = await getCompanyInfo(connection, productCode);
if (!companyInfoResults.length) {
return {
success: false,
request: '',
data: `No companyinfo inserts because no rows found in companyinfo_replikwando for productcode ${productCode}`
};
}
try {
const convertedRows = storeArr.map(row => convertStore(row, productCode, companyInfoResults));
const filteredRows = convertedRows.filter(row => row);
if (filteredRows.length) {
const multiRowUpsertResponse = await multiRowUpsertQuery(connection, 'companyinfo', filteredRows);
responses.push(`companyinfo affected rows: ${multiRowUpsertResponse?.affectedRows}`);
}
else {
responses.push(`companyinfo affected rows: 0, check companyinfo_replikwando`);
}
return {
success: true,
request: '',
data: responses
};
}
catch (err) {
return {
success: false,
request: '',
data: err
};
}
}
export async function deleteCompanyInfo(connection, productCode, store) {
const responses = [];
try {
const queryOptions = {
sql: `DELETE FROM companyinfo WHERE productcode = ? AND store = ?`,
values: [productCode, store],
};
const deleteResponse = await write(connection, queryOptions);
responses.push(`companyinfo affected rows: ${deleteResponse?.affectedRows}`);
return {
success: true,
request: '',
data: responses
};
}
catch (err) {
return {
success: false,
request: '',
data: err
};
}
}
export declare function upsertCustomerRows(connection: any, customerArr: any, productCode: any): Promise<{
success: boolean;
data: any;
}>;
export declare function deleteCustomerRow(connection: any, productCode: any, customerId: any): Promise<{
success: boolean;
data: any;
}>;
import { multiRowUpsertQuery, write } from './shared.js';
function convertCustomer(row, productCode) {
return {
productcode: productCode,
phone: row.phone,
customerid: row.customerId,
storeid: row.storeId,
company: row.company,
name: row.name,
street: row.street,
city: row.city,
county: row.country,
state: row.state,
zip: row.zip,
country: row.country,
fax: row.fax,
email: row.email,
web: row.web,
comments: row.comments,
type: row.type,
defaulttermid: row.defaultTermId,
dateentered: row.dateEntered,
taxitemid: row.taxItemId,
defaultpricetype: row.defaultPriceType,
percentofprice: row.percentOfPrice,
porequired: row.poRequired,
blanketpurchaseorder: row.blanketPurchaseOrder,
blanketpurchaseorderexpiration: row.blanketPurchaseOrderExpiration,
webuserid: row.webUserId,
active: row.active === 'False' ? 'False' : 'True' // To cover Pro which doesn't have an active flag
};
}
export async function upsertCustomerRows(connection, customerArr, productCode) {
const responses = [];
try {
const convertedRows = customerArr.map(row => convertCustomer(row, productCode));
const multiRowUpsertResponse = await multiRowUpsertQuery(connection, 'customer', convertedRows);
responses.push(`customer affected rows: ${multiRowUpsertResponse.affectedRows}`);
return {
success: true,
data: responses
};
}
catch (err) {
return {
success: false,
data: err
};
}
}
export async function deleteCustomerRow(connection, productCode, customerId) {
const responses = [];
try {
const queryOptions = {
sql: `DELETE FROM customer WHERE productcode = ? AND customerid = ?`,
values: [productCode, customerId],
};
const deleteResponse = await write(connection, queryOptions);
responses.push(`customer delete affected rows: ${deleteResponse?.affectedRows}`);
return {
success: true,
data: responses
};
}
catch (err) {
return {
success: false,
data: err
};
}
}
export declare function upsertCustomerAddressRows(connection: any, customerAddressArr: any, productCode: any): Promise<{
success: boolean;
data: any;
}>;
export declare function deleteCustomerAddressRow(connection: any, productCode: any, customerAddressId: any): Promise<{
success: boolean;
data: any;
}>;
import { multiRowUpsertQuery, write } from './shared.js';
function convertCustomerAddress(row, productCode) {
return {
productcode: productCode,
...row
};
}
export async function upsertCustomerAddressRows(connection, customerAddressArr, productCode) {
const responses = [];
try {
const convertedRows = customerAddressArr.map(row => convertCustomerAddress(row, productCode));
const multiRowUpsertResponse = await multiRowUpsertQuery(connection, 'customeraddress', convertedRows);
responses.push(`customeraddress affected rows: ${multiRowUpsertResponse.affectedRows}`);
return {
success: true,
data: responses
};
}
catch (err) {
return {
success: false,
data: err
};
}
}
export async function deleteCustomerAddressRow(connection, productCode, customerAddressId) {
const responses = [];
try {
const queryOptions = {
sql: `DELETE FROM customeraddress WHERE productcode = ? AND customeraddressid = ?`,
values: [productCode, customerAddressId],
};
const deleteResponse = await write(connection, queryOptions);
responses.push(`customeraddress delete affected rows: ${deleteResponse?.affectedRows}`);
return {
success: true,
data: responses
};
}
catch (err) {
return {
success: false,
data: err
};
}
}
export declare function upsertCustomerOptionRows(connection: any, customerOptionArr: any, productCode: any): Promise<{
success: boolean;
data: any;
}>;
export declare function deleteCustomerOptionRow(connection: any, productCode: any, customerOptionId: any): Promise<{
success: boolean;
data: any;
}>;
import { multiRowUpsertQuery, write } from './shared.js';
function convertCustomerOption(row, productCode) {
return {
productcode: productCode,
customeroptionid: row.customerOptionId,
optiontype: row.optionType,
option: row.option,
showincustomerlist: row.showInCustomerList
};
}
export async function upsertCustomerOptionRows(connection, customerOptionArr, productCode) {
const responses = [];
try {
const convertedRows = customerOptionArr.map(row => convertCustomerOption(row, productCode));
const multiRowUpsertResponse = await multiRowUpsertQuery(connection, 'customeroption', convertedRows);
responses.push(`customeroption affected rows: ${multiRowUpsertResponse.affectedRows}`);
return {
success: true,
data: responses
};
}
catch (err) {
return {
success: false,
data: err
};
}
}
export async function deleteCustomerOptionRow(connection, productCode, customerOptionId) {
const responses = [];
try {
const queryOptions = {
sql: `DELETE FROM customeroption WHERE productcode = ? AND customeroptionid = ?`,
values: [productCode, customerOptionId],
};
const deleteResponse = await write(connection, queryOptions);
responses.push(`customeroption delete affected rows: ${deleteResponse?.affectedRows}`);
return {
success: true,
data: responses
};
}
catch (err) {
return {
success: false,
data: err
};
}
}
export declare function upsertCustomerOptionValueRows(connection: any, customerOptionValueArr: any, productCode: any): Promise<{
success: boolean;
data: any;
}>;
export declare function deleteCustomerOptionValueRow(connection: any, productCode: any, customerOptionValueId: any): Promise<{
success: boolean;
data: any;
}>;
import { multiRowUpsertQuery, write } from './shared.js';
function convertCustomerOptionValue(row, productCode) {
return {
productcode: productCode,
customeroptionvalueid: row.customerOptionValueId,
customeroptionid: row.customerOptionId,
value: row.value,
customerid: row.customerId
};
}
export async function upsertCustomerOptionValueRows(connection, customerOptionValueArr, productCode) {
const responses = [];
try {
const convertedRows = customerOptionValueArr.map(row => convertCustomerOptionValue(row, productCode));
const multiRowUpsertResponse = await multiRowUpsertQuery(connection, 'customeroptionvalue', convertedRows);
responses.push(`customeroptionvalue affected rows: ${multiRowUpsertResponse.affectedRows}`);
return {
success: true,
data: responses
};
}
catch (err) {
return {
success: false,
data: err
};
}
}
export async function deleteCustomerOptionValueRow(connection, productCode, customerOptionValueId) {
const responses = [];
try {
const queryOptions = {
sql: `DELETE FROM customeroptionvalue WHERE productcode = ? AND customeroptionvalueid = ?`,
values: [productCode, customerOptionValueId],
};
const deleteResponse = await write(connection, queryOptions);
responses.push(`customeroptionvalue delete affected rows: ${deleteResponse?.affectedRows}`);
return {
success: true,
data: responses
};
}
catch (err) {
return {
success: false,
data: err
};
}
}
export declare function upsertCustomerPriceContractRows(connection: any, customerPriceContractArr: any, productCode: any): Promise<{
success: boolean;
data: any;
}>;
export declare function deleteCustomerPriceContractRow(connection: any, productCode: any, customerPriceContractId: any): Promise<{
success: boolean;
data: any;
}>;
import { multiRowUpsertQuery, write } from './shared.js';
function convertCustomerPriceContract(row, productCode) {
return {
productcode: productCode,
customerpricecontractid: row.customerPriceContractId,
customerid: row.customerId,
inventoryid: row.inventoryId,
price: row.price,
startdate: row.startDate,
enddate: row.endDate
};
}
export async function upsertCustomerPriceContractRows(connection, customerPriceContractArr, productCode) {
const responses = [];
try {
const convertedRows = customerPriceContractArr.map(row => convertCustomerPriceContract(row, productCode));
const multiRowUpsertResponse = await multiRowUpsertQuery(connection, 'customerpricecontract', convertedRows);
responses.push(`customerpricecontract affected rows: ${multiRowUpsertResponse.affectedRows}`);
return {
success: true,
data: responses
};
}
catch (err) {
return {
success: false,
data: err
};
}
}
export async function deleteCustomerPriceContractRow(connection, productCode, customerPriceContractId) {
const responses = [];
try {
const queryOptions = {
sql: `DELETE FROM customerpricecontract WHERE productcode = ? AND customerpricecontractid = ?`,
values: [productCode, customerPriceContractId],
};
const deleteResponse = await write(connection, queryOptions);
responses.push(`customerpricecontract delete affected rows: ${deleteResponse?.affectedRows}`);
return {
success: true,
data: responses
};
}
catch (err) {
return {
success: false,
data: err
};
}
}
import { deleteCategory, upsertCategoryRows } from './category.js';
import { deleteCompanyInfo, upsertCompanyInfoRows } from './companyInfo.js';
import { deleteCustomerRow, upsertCustomerRows } from './customer.js';
import { deleteCustomerAddressRow, upsertCustomerAddressRows } from './customerAddress.js';
import { deleteCustomerOptionRow, upsertCustomerOptionRows } from './customerOption.js';
import { deleteCustomerOptionValueRow, upsertCustomerOptionValueRows } from './customerOptionValue.js';
import { deleteCustomerPriceContractRow, upsertCustomerPriceContractRows } from './customerPriceContract.js';
import { deleteInventoryData, insertInventoryData, upsertInventoryRows } from './inventory.js';
import { insertInventoryFileData, upsertInventoryFileData, deleteInventoryFileData, deleteInventoryFileRow } from './inventoryFile/inventoryFile.js';
import { insertInventoryOptionRows, upsertInventoryOptionRows, deleteInventoryOptionRows, deleteInventoryOptionRow } from './inventoryOption.js';
import { deleteInventoryOptionListRow, getPartUseInfoQuery, insertInventoryOptionListRows, upsertInventoryOptionListRows } from './inventoryOptionList.js';
import { deleteInventorySourceData, deleteInventorySourceRow, upsertInventorySourceRows } from './inventorySource.js';
import { deleteVehicle, upsertVehicleRows } from './invmaster.js';
import { insertMnfcrmodModelRows, upsertMnfcrmodModelRows, deleteMnfcrmodModelRow } from './mnfcrmod.js';
import { deletePartUseRow, upsertPartUseRows } from './partUse.js';
import { deleteSellPriceClassRow, upsertSellPriceClassRows } from './sellPriceClass.js';
import { upsertPaymentRows, deletePaymentRow } from './payment.js';
import { upsertPaymentLineRows, deletePaymentLineRow } from './paymentLine.js';
import { upsertSalesOrderRows, deleteSalesOrderRow } from './salesOrder.js';
import { upsertSalesOrderAdjustmentRows, deleteSalesOrderAdjustmentRow } from './salesOrderAdjustment.js';
import { upsertSalesOrderLineRows, deleteSalesOrderLineRow } from './salesOrderLineRow.js';
export { deleteCategory, deleteCompanyInfo, deleteCustomerAddressRow, deleteCustomerOptionRow, deleteCustomerOptionValueRow, deleteCustomerPriceContractRow, deleteCustomerRow, deleteInventoryData, deleteInventoryFileData, deleteInventoryFileRow, deleteInventoryOptionListRow, deleteInventoryOptionRow, deleteInventoryOptionRows, deleteInventorySourceData, deleteInventorySourceRow, deleteMnfcrmodModelRow, deletePartUseRow, deletePaymentRow, deletePaymentLineRow, deleteSalesOrderAdjustmentRow, deleteSalesOrderLineRow, deleteSalesOrderRow, deleteSellPriceClassRow, deleteVehicle, getPartUseInfoQuery, insertInventoryData, insertInventoryFileData, insertInventoryOptionListRows, insertInventoryOptionRows, insertMnfcrmodModelRows, upsertCategoryRows, upsertCompanyInfoRows, upsertCustomerAddressRows, upsertCustomerOptionRows, upsertCustomerOptionValueRows, upsertCustomerPriceContractRows, upsertCustomerRows, upsertInventoryFileData, upsertInventoryOptionListRows, upsertInventoryOptionRows, upsertInventoryRows, upsertInventorySourceRows, upsertMnfcrmodModelRows, upsertPartUseRows, upsertPaymentRows, upsertPaymentLineRows, upsertSalesOrderAdjustmentRows, upsertSalesOrderLineRows, upsertSalesOrderRows, upsertSellPriceClassRows, upsertVehicleRows };
export type { HtpPaymentRow, HtpPaymentLineRow, HtpSalesOrderAdjustmentRow, HtpSalesOrderLineRow, HtpSalesOrderRow, PaymentRow, PaymentLineRow, SalesOrderAdjustmentRow, SalesOrderLineRow, SalesOrderRow, } from './types.js';
import { deleteCategory, upsertCategoryRows } from './category.js';
import { deleteCompanyInfo, upsertCompanyInfoRows } from './companyInfo.js';
import { deleteCustomerRow, upsertCustomerRows } from './customer.js';
import { deleteCustomerAddressRow, upsertCustomerAddressRows } from './customerAddress.js';
import { deleteCustomerOptionRow, upsertCustomerOptionRows } from './customerOption.js';
import { deleteCustomerOptionValueRow, upsertCustomerOptionValueRows } from './customerOptionValue.js';
import { deleteCustomerPriceContractRow, upsertCustomerPriceContractRows } from './customerPriceContract.js';
import { deleteInventoryData, insertInventoryData, upsertInventoryRows } from './inventory.js';
import { insertInventoryFileData, upsertInventoryFileData, deleteInventoryFileData, deleteInventoryFileRow } from './inventoryFile/inventoryFile.js';
import { insertInventoryOptionRows, upsertInventoryOptionRows, deleteInventoryOptionRows, deleteInventoryOptionRow } from './inventoryOption.js';
import { deleteInventoryOptionListRow, getPartUseInfoQuery, insertInventoryOptionListRows, upsertInventoryOptionListRows, } from './inventoryOptionList.js';
import { deleteInventorySourceData, deleteInventorySourceRow, upsertInventorySourceRows } from './inventorySource.js';
import { deleteVehicle, upsertVehicleRows } from './invmaster.js';
import { insertMnfcrmodModelRows, upsertMnfcrmodModelRows, deleteMnfcrmodModelRow } from './mnfcrmod.js';
import { deletePartUseRow, upsertPartUseRows } from './partUse.js';
import { deleteSellPriceClassRow, upsertSellPriceClassRows } from './sellPriceClass.js';
import { upsertPaymentRows, deletePaymentRow } from './payment.js';
import { upsertPaymentLineRows, deletePaymentLineRow } from './paymentLine.js';
import { upsertSalesOrderRows, deleteSalesOrderRow } from './salesOrder.js';
import { upsertSalesOrderAdjustmentRows, deleteSalesOrderAdjustmentRow } from './salesOrderAdjustment.js';
import { upsertSalesOrderLineRows, deleteSalesOrderLineRow } from './salesOrderLineRow.js';
export { deleteCategory, deleteCompanyInfo, deleteCustomerAddressRow, deleteCustomerOptionRow, deleteCustomerOptionValueRow, deleteCustomerPriceContractRow, deleteCustomerRow, deleteInventoryData, deleteInventoryFileData, deleteInventoryFileRow, deleteInventoryOptionListRow, deleteInventoryOptionRow, deleteInventoryOptionRows, deleteInventorySourceData, deleteInventorySourceRow, deleteMnfcrmodModelRow, deletePartUseRow, deletePaymentRow, deletePaymentLineRow, deleteSalesOrderAdjustmentRow, deleteSalesOrderLineRow, deleteSalesOrderRow, deleteSellPriceClassRow, deleteVehicle, getPartUseInfoQuery, insertInventoryData, insertInventoryFileData, insertInventoryOptionListRows, insertInventoryOptionRows, insertMnfcrmodModelRows, upsertCategoryRows, upsertCompanyInfoRows, upsertCustomerAddressRows, upsertCustomerOptionRows, upsertCustomerOptionValueRows, upsertCustomerPriceContractRows, upsertCustomerRows, upsertInventoryFileData, upsertInventoryOptionListRows, upsertInventoryOptionRows, upsertInventoryRows, upsertInventorySourceRows, upsertMnfcrmodModelRows, upsertPartUseRows, upsertPaymentRows, upsertPaymentLineRows, upsertSalesOrderAdjustmentRows, upsertSalesOrderLineRows, upsertSalesOrderRows, upsertSellPriceClassRows, upsertVehicleRows };
type InventorySettings = {
useSellableQuantity?: boolean;
synchSource?: boolean;
};
export declare function upsertInventoryRows(htpConnection: any, inventoryArr: any, productCode: any, settings?: InventorySettings): Promise<{
success: boolean;
request: string;
data: any;
}>;
export declare function deleteInventoryData(htpConnection: any, productCode: any, partNum: any, storeId: any): Promise<{
success: boolean;
request: string;
data: any;
}>;
export declare function insertInventoryData(htpConnection: any, inventoryArr: any, productCode: any, settings?: InventorySettings): Promise<{
success: boolean;
request: string;
data: any;
}>;
export {};
import mysql from 'mysql2/promise';
import pProps from 'p-props';
import { multiRowInsertQuery, multiRowUpsertQuery, query, write } from './shared.js';
import { insertInventoryOptionListRows, upsertInventoryOptionListRows } from './inventoryOptionList.js';
import { insertInventoryOptionRows, upsertInventoryOptionRows, deleteInventoryOptionRows } from './inventoryOption.js';
import { deleteInventoryFileData, insertInventoryFileData, upsertInventoryFileData } from './inventoryFile/inventoryFile.js';
import { upsertInventorySourceRows, deleteInventorySourceData } from './inventorySource.js';
// Get partuse data for all of the rows being handled
async function getPartUseRows(htpConnection, inventoryArr, productCode) {
// Create an array of all the inventorytype names, remove null and duplicates
const uniquePartUseRows = Array.from(new Set(inventoryArr.map(row => row.inventoryType?.id).filter(element => element !== null)));
if (!uniquePartUseRows.length) {
return;
}
// Need to do this to escape correctly
const queryData = [uniquePartUseRows];
const queryOptions = {
sql: `SELECT partuse.typenum, partuse.part, partlistauthoritymap.partlistauthorityid
FROM partuse
LEFT JOIN partlistauthoritymap
ON partuse.productcode = partlistauthoritymap.productcode
AND partuse.typenum = partlistauthoritymap.sourceid
WHERE partuse.productcode = ? AND typenum IN ? `,
values: [productCode, queryData],
};
const response = await query(htpConnection, queryOptions);
return response.map(row => ({
typenum: row.typenum,
part: row.part,
partlistauthorityid: row.partlistauthorityid,
}));
}
// Create a Map from partUseData for efficient lookups
function createPartUseMap(partUseData) {
if (!partUseData) {
return null;
}
return new Map(partUseData.map(row => [row.typenum, row]));
}
// TODO: Need to return the error instead of logging separately
// TODO: Needs revisited but handles empty partUseData
function addPartUse(partUseMap, inventoryObjectRow) {
// If partUseMap is empty, log and return inventoryObjectRow as-is
if (!partUseMap) {
console.info("No part use data for inventory partnum: ", inventoryObjectRow.inventoryId);
return;
}
// If there is partUseData but no match, log and return nothing as there is likely a data issue
//const partUseMatch = partUseData.find(element => inventoryObjectRow.inventoryType.id === element.typenum)
const inventoryTypeId = inventoryObjectRow.inventoryType.id;
if (!inventoryTypeId) {
console.info("No inventorytypeid given for this inventory partnum: ", inventoryObjectRow.inventoryId);
return;
}
const partUseMatch = partUseMap.get(inventoryTypeId);
if (!partUseMatch) {
console.info("No insert because no matching partuse entry", "inventory", "partnum", inventoryObjectRow.inventoryId);
return;
}
// Otherwise return an updated inventory object row
const { typenum, partlistauthorityid } = partUseMatch;
inventoryObjectRow.typeNum = typenum;
inventoryObjectRow.partListAuthorityId = partlistauthorityid;
return inventoryObjectRow;
}
// Converts universal inventory object to an HTP row
// Settings can have modifiers that allow different value mappings without additional queries, currently only used for available vs sellable quantities
function convertInventory(inventoryObjectRow, productCode, settings) {
const row = {
partnum: inventoryObjectRow.inventoryId,
store: inventoryObjectRow.storeId,
typenum: inventoryObjectRow.partListAuthorityId || inventoryObjectRow.typeNum,
sourcetypenum: inventoryObjectRow.typeNum,
stocknum: inventoryObjectRow.vehicle.id,
vinnum: inventoryObjectRow.vehicle.vin || '',
year: inventoryObjectRow.vehicle.year || inventoryObjectRow.year,
description: inventoryObjectRow.description || '',
interchangenumber: inventoryObjectRow.interchange.number,
subinterchangenumber: inventoryObjectRow.interchange.subNumber?.substring(0, 4) || '',
status: inventoryObjectRow.status,
suggestedprice: inventoryObjectRow.retailPrice || 0.00,
bottomprice: inventoryObjectRow.wholesalePrice,
replenish: inventoryObjectRow.replenish,
deplete: inventoryObjectRow.deplete,
quantity: settings.useSellableQuantity ? inventoryObjectRow.sellableQuantity : inventoryObjectRow.availableQuantity,
//dateentered: inventoryObjectRow.dateEntered !== '0000-00-00 00:00:00' ? row.dateentered = inventoryObjectRow.dateEntered : row.dateentered = null,
// datemodified: inventoryObjectRow.dateModified,
// datesold: inventoryObjectRow.dateSold,
label1: null,
label2: null,
label3: null,
label4: null,
data1: null,
data2: null,
data3: null,
data4: null,
cost: inventoryObjectRow.cost,
// Vehicle Make
make: inventoryObjectRow.vehicle?.make?.substring(0, 24) || '',
// Vehicle Model
model: inventoryObjectRow.vehicle?.model?.substring(0, 49) || '',
pmanufacturer: inventoryObjectRow.parentManufacturer.name?.substring(0, 24), //matches trigger
pmodel: inventoryObjectRow.parentModel.name?.substring(0, 49), //matches trigger
taxable: inventoryObjectRow.taxable,
oemnum: inventoryObjectRow.oemNumber,
condition: inventoryObjectRow.condition,
side: inventoryObjectRow.side,
// TODO: confirm this is desired or make schema change
tagnum: inventoryObjectRow.tagNumber?.substring(0, 19),
upc: inventoryObjectRow.upc || '',
category: inventoryObjectRow.category,
weight: inventoryObjectRow.shippingWeight?.value,
productcode: productCode,
lastupdate: mysql.raw('NOW()'),
worldviewable: inventoryObjectRow.public,
displayonline: inventoryObjectRow.displayOnline,
notes: inventoryObjectRow.notes || '',
shippinglength: inventoryObjectRow.shippingDimensions?.length,
shippingwidth: inventoryObjectRow.shippingDimensions?.width,
shippingheight: inventoryObjectRow.shippingDimensions?.height,
core: inventoryObjectRow.retailCore,
location: inventoryObjectRow.location,
//buynow: inventoryObjectRow.webSaleClass ? inventoryObjectRow.webSaleClass.replace(/s$/,'') : 'None',
inventoryvendorid: inventoryObjectRow.vendorId,
jobberprice: inventoryObjectRow.jobberPrice,
distributorprice: inventoryObjectRow.distributorPrice,
averagecost: inventoryObjectRow.averageCost,
sellpriceclassid: inventoryObjectRow.sellPriceClassId,
wholesalecore: inventoryObjectRow.wholeSaleCore ? inventoryObjectRow.wholeSaleCore : null,
jobbercore: inventoryObjectRow.jobberCore ? inventoryObjectRow.jobberCore : null,
distributorcore: inventoryObjectRow.distributorCore ? inventoryObjectRow.distributorCore : null,
// The following return ER_TRUNCATED_WRONG_VALUE if left as '0000-00-00 00:00:00'
dateentered: inventoryObjectRow.dateEntered !== '0000-00-00 00:00:00' ? inventoryObjectRow.dateEntered : undefined,
datemodified: inventoryObjectRow.dateModified !== '0000-00-00 00:00:00' ? inventoryObjectRow.dateModified : undefined,
datesold: inventoryObjectRow.dateSold !== '0000-00-00 00:00:00' ? inventoryObjectRow.dateSold : undefined,
// Buy now might optionally come from a function (LKQ at least)
buynow: inventoryObjectRow.buyNowSla ?? (inventoryObjectRow.webSaleClass?.replace(/s$/, '') ?? 'None'),
};
return row;
}
/*
This is the primary function called by the queue and synch functions
The outer try/catch should return the same object as the other main functions
The nested try/catch is for transaction handling
*/
export async function upsertInventoryRows(htpConnection, inventoryArr, productCode, settings = {}) {
const responses = [];
try {
//
// Get partuse data for later steps
const partUseData = await getPartUseRows(htpConnection, inventoryArr, productCode);
// Create a Map from partUseData for efficient lookups (do this once)
const partUseMap = createPartUseMap(partUseData);
// Add partUseData to inventoryArr
const inventoryPartUseRows = inventoryArr.map(row => addPartUse(partUseMap, row));
// Remove undefines, these rows will be used for their tags
const filteredInventoryPartUseRows = inventoryPartUseRows.filter(row => row !== undefined);
// Convert changed product rows to HTP rows
const convertedRows = filteredInventoryPartUseRows.map(row => convertInventory(row, productCode, settings));
// Need to filter undefined rows due to missing partuse data
const filteredRows = convertedRows.filter(row => row !== undefined);
if (filteredRows.length) {
// Get a connection from the pool for transaction
const connection = await htpConnection.getConnection();
try {
// Start transaction
await connection.beginTransaction();
// Insert inventory rows first, return should be??
const multiRowUpsertResponse = await multiRowUpsertQuery(connection, 'inventory', filteredRows);
// call inventory option list stuff
const multiRowInventoryOptionListUpsertResponse = await upsertInventoryOptionListRows(connection, filteredInventoryPartUseRows, productCode);
const multiRowInventoryOptionUpsertResponse = await upsertInventoryOptionRows(connection, filteredInventoryPartUseRows, productCode);
// call inventory file and image stuff
const multiRowInventoryFileUpsertResponse = await upsertInventoryFileData(connection, filteredInventoryPartUseRows, productCode);
// call inventory source stuff
let multiRowInventorySourceUpsertResponse;
if (settings.synchSource) {
multiRowInventorySourceUpsertResponse = await upsertInventorySourceRows(connection, filteredInventoryPartUseRows, productCode);
}
// Commit transaction
await connection.commit();
responses.push(`affected rows: inventory ${multiRowUpsertResponse?.affectedRows}, ${multiRowInventoryOptionListUpsertResponse}, ${multiRowInventoryOptionUpsertResponse}, ${[...multiRowInventoryFileUpsertResponse]}, ${multiRowInventorySourceUpsertResponse ? multiRowInventorySourceUpsertResponse : 'inventorysource affected rows: none'}`);
}
catch (err) {
// Rollback transaction on error
await connection.rollback();
throw err;
}
finally {
// Always release connection back to pool
connection.release();
}
}
else {
responses.push(`affected rows: inventory none`);
}
return {
success: true,
request: '',
data: responses
};
}
catch (err) {
return {
success: false,
request: '',
data: err
};
}
}
async function deleteInventoryQuery(htpConnection, productCode, partNum, storeId) {
const queryOptions = {
sql: "DELETE FROM inventory WHERE partnum = ? AND store = ? AND productcode = ?",
values: [partNum, storeId, productCode],
};
return await write(htpConnection, queryOptions);
}
export async function deleteInventoryData(htpConnection, productCode, partNum, storeId) {
const responses = [];
try {
const deleteResponses = await pProps({
inventoryDeleteResponse: deleteInventoryQuery(htpConnection, productCode, partNum, storeId),
inventoryOptionDeleteResponse: deleteInventoryOptionRows(htpConnection, productCode, storeId, partNum),
inventoryFileDeleteResponse: deleteInventoryFileData(htpConnection, productCode, partNum),
inventorySourceDeleteResponse: deleteInventorySourceData(htpConnection, productCode, partNum)
});
const fileDeleteResponse = deleteResponses.inventoryFileDeleteResponse.data;
responses.push(`inventory delete affected rows: ${deleteResponses.inventoryDeleteResponse?.affectedRows}, ${deleteResponses.inventoryOptionDeleteResponse}, ${fileDeleteResponse}, ${deleteResponses.inventorySourceDeleteResponse}`);
return {
success: true,
request: '',
data: responses
};
}
catch (err) {
return {
success: false,
request: '',
data: err
};
}
}
// Multi-row insert query
async function insertInventoryRows(htpConnection, rows) {
const columns = Object.keys(rows[0]);
const values = rows.map(obj => Object.values(obj));
const result = await multiRowInsertQuery(htpConnection, "inventory", columns, values);
return result;
}
// Multi-row insert function for repush
// TODO: get partUseData outside of this function
export async function insertInventoryData(htpConnection, inventoryArr, productCode, settings = {}) {
const responses = [];
try {
//
// Get partuse data for later steps
const partUseData = await getPartUseRows(htpConnection, inventoryArr, productCode);
// Create a Map from partUseData for efficient lookups (do this once)
const partUseMap = createPartUseMap(partUseData);
// Add partUseData to inventoryArr
const inventoryPartUseRows = inventoryArr.map(row => addPartUse(partUseMap, row));
// Remove undefines, these rows will be used for their tags
const filteredInventoryPartUseRows = inventoryPartUseRows.filter(row => row !== undefined);
// Convert changed product rows to HTP rows
const convertedRows = filteredInventoryPartUseRows.map(row => convertInventory(row, productCode, settings));
// Need to filter undefined rows due to missing partuse data
const filteredRows = convertedRows.filter(row => row !== undefined);
if (filteredRows.length) {
// Get a connection from the pool for transaction
const connection = await htpConnection.getConnection();
try {
// Start transaction
await connection.beginTransaction();
// Insert inventory rows first
const initialResponse = await pProps({
inventoryInsertResponse: insertInventoryRows(connection, filteredRows),
inventoryFileResponse: insertInventoryFileData(connection, filteredInventoryPartUseRows, productCode),
inventoryOptionListResponse: insertInventoryOptionListRows(connection, filteredInventoryPartUseRows, productCode),
inventorySourceUpsertResponse: settings.synchSource ? upsertInventorySourceRows(connection, filteredInventoryPartUseRows, productCode) : Promise.resolve(null)
});
// This one has to be run after inventoryoptionlist since it needs to query for those rows
const inventoryOptionResponse = await insertInventoryOptionRows(connection, filteredInventoryPartUseRows, productCode);
// Commit transaction
await connection.commit();
responses.push(`affected rows: inventory ${initialResponse.inventoryInsertResponse?.affectedRows}, ${initialResponse.inventoryOptionListResponse}, ${inventoryOptionResponse}, ${initialResponse.inventoryFileResponse ? [...initialResponse.inventoryFileResponse] : ''}, ${initialResponse.inventorySourceUpsertResponse ? initialResponse.inventorySourceUpsertResponse : 'inventorysource affected rows: none'}`);
// responses.push(`affected rows: inventory ${inventoryInsertResponse.affectedRows}, ${inventoryOptionListResponse.affectedRows}, ${inventoryOptionResponse?.affectedRows}, ${inventoryFileResponse?.affectedRows}`)
}
catch (err) {
// Rollback transaction on error
await connection.rollback();
throw err;
}
finally {
// Always release connection back to pool
connection.release();
}
}
return {
success: true,
request: '',
data: responses
};
}
catch (err) {
//
return {
success: false,
request: '',
data: err
};
}
}
type FluffedAttachmentBase = {
id: number;
public: 'False' | 'True';
rank: number;
file: number;
inventoryid: number;
productcode: number;
storeid: number;
};
type ImageAttachment = FluffedAttachmentBase & {
type: 'Image';
};
type FileAttachment = FluffedAttachmentBase & {
type: 'PDF' | 'Text';
};
type VideoAttachment = FluffedAttachmentBase & {
type: 'YOUTUBE';
video: {
id: number;
url: string;
};
};
export type FluffedAttachmentRow = ImageAttachment | FileAttachment | VideoAttachment;
export declare function fluffAttachmentRows(productInventoryRow: any, productCode: any): (FluffedAttachmentRow | undefined)[];
export declare function convertManyInventoryImageRows(row: any): {
productcode: any;
inventoryid: any;
fileid: any;
imagelocation: string;
rank: any;
};
export declare function convertManyInventoryFileRows(row: any): {
productcode: any;
associatedid: any;
associatedidtype: string;
fileid: any;
filetype: any;
};
export declare function convertManyPartVideoListRows(row: any): {
productcode: any;
store: any;
partnum: any;
videotype: any;
videoid: any;
url: any;
dirty: string;
};
export declare function filterExistingFileListRows(newRows: any, existingRows: any): any;
type ConvertedImageListRow = {
productcode: number;
inventoryid: number;
fileid: number;
imagelocation: string;
rank: number;
};
export declare function partitionImageListRows<TExisting extends {
productcode: number;
inventoryid: number;
fileid: number | null;
}>(incomingRows: ConvertedImageListRow[], existingRows: TExisting[]): {
toInsert: ConvertedImageListRow[];
toDelete: TExisting[];
};
type ConvertedVideoListRow = {
productcode: number;
store: number;
partnum: number;
videotype: string;
videoid: string;
url: string;
dirty: 'False' | 'True';
};
export declare function partitionVideoListRows<TExisting extends {
productcode: number;
store: number;
partnum: number;
videoid: string;
}>(incomingRows: ConvertedVideoListRow[], existingRows: TExisting[]): {
toInsert: ConvertedVideoListRow[];
toDelete: TExisting[];
};
type ConvertedFileListRow = {
productcode: number;
associatedid: number;
associatedidtype: string;
fileid: number;
filetype: string;
};
export declare function partitionFileListRows<TExisting extends {
productcode: number | string;
associatedid: number;
associatedidtype: string;
fileid: number;
}>(incomingRows: ConvertedFileListRow[], existingRows: TExisting[]): {
toInsert: ConvertedFileListRow[];
toDelete: TExisting[];
};
export {};
// Only adds inventoryid and productcode to attachment rows
export function fluffAttachmentRows(productInventoryRow, productCode) {
const inventoryId = productInventoryRow.inventoryId;
const storeId = productInventoryRow.storeId;
const attachmentRows = productInventoryRow.attachments.map(function (attachment) {
if (attachment.id) {
return { ...attachment,
inventoryid: inventoryId,
productcode: productCode,
storeid: storeId
};
}
});
return attachmentRows;
}
export function convertManyInventoryImageRows(row) {
const imageLocation = `&productcode=${row.productcode}&id=${row.file}`;
return {
productcode: row.productcode,
inventoryid: row.inventoryid,
fileid: row.file,
imagelocation: imageLocation,
rank: row.rank,
};
}
export function convertManyInventoryFileRows(row) {
return {
productcode: row.productcode,
associatedid: row.inventoryid,
associatedidtype: 'inventoryid',
fileid: row.file,
filetype: row.type
};
}
export function convertManyPartVideoListRows(row) {
return {
productcode: row.productcode,
store: row.storeid,
partnum: row.inventoryid,
videotype: row.type,
videoid: row.video.id,
url: row.video.url,
dirty: "False"
};
}
export function filterExistingFileListRows(newRows, existingRows) {
const uniqueRows = newRows.filter(function (newRow) {
return !existingRows.find(function (existingRow) {
return existingRow.associatedid === newRow.associatedid && existingRow.fileid === newRow.fileid && newRow.associatedidtype === existingRow.associatedidtype;
});
});
return uniqueRows;
}
// Compares incoming attachment rows (source of truth) against what already exists in the DB.
// Returns two lists in one pass:
// toInsert — rows in incoming that are missing from the DB (need to be written)
// toDelete — rows in the DB that are missing from incoming (no longer wanted, need to be removed)
//
// Identity is determined by a composite key string built by the caller, e.g. "1|100|inventoryid|10".
// We build a Set from each side first so every membership check is O(1) instead of scanning the array.
function partitionAttachmentRows(incomingRows, existingRows, buildIncomingKey, buildExistingKey) {
// Index each side by its composite key for fast lookups below
const incomingKeySet = new Set(incomingRows.map(buildIncomingKey));
const existingKeySet = new Set(existingRows.map(buildExistingKey));
// An incoming row is new if its key is not found in the DB
const toInsert = incomingRows.filter(row => !existingKeySet.has(buildIncomingKey(row)));
// An existing DB row is stale if its key is not found in incoming
const toDelete = existingRows.filter(row => !incomingKeySet.has(buildExistingKey(row)));
return { toInsert, toDelete };
}
// Wrapper for partimagelist rows.
// The composite key is (productcode, inventoryid, fileid)
// so fileid is sufficient to identify a unique image on a part.
export function partitionImageListRows(incomingRows, existingRows) {
return partitionAttachmentRows(incomingRows, existingRows, row => `${row.productcode}|${row.inventoryid}|${row.fileid}`, row => `${row.productcode}|${row.inventoryid}|${row.fileid}`);
}
// Wrapper for partvideolist rows.
// The composite key is (productcode, store, partnum, videoid)
// the same part can have different videos per store
// so a video for store 2 must not be treated as stale just because store 1 doesn't have it.
export function partitionVideoListRows(incomingRows, existingRows) {
return partitionAttachmentRows(incomingRows, existingRows, row => `${row.productcode}|${row.store}|${row.partnum}|${row.videoid}`, row => `${row.productcode}|${row.store}|${row.partnum}|${row.videoid}`);
}
// Wrapper for partfilelist rows.
// The composite key is (productcode, associatedid, associatedidtype, fileid)
// associatedid alone is not enough: the same fileid can exist under different inventory rows.
export function partitionFileListRows(incomingRows, existingRows) {
return partitionAttachmentRows(incomingRows, existingRows, row => `${row.productcode}|${row.associatedid}|${row.associatedidtype}|${row.fileid}`, row => `${row.productcode}|${row.associatedid}|${row.associatedidtype}|${row.fileid}`);
}
export declare function upsertInventoryFileData(htpConnection: any, productInventoryRows: any, productCode: any): Promise<any[]>;
export declare function deleteInventoryFileRow(htpConnection: any, productCode: any, inventoryId: any, fileId: any, type: any): Promise<{
success: boolean;
request: string;
data: any;
}>;
export declare function deleteInventoryFileData(htpConnection: any, productCode: any, inventoryId: any): Promise<{
success: boolean;
request: string;
data: any;
}>;
export declare function insertInventoryFileData(htpConnection: any, productInventoryRows: any, productCode: any): Promise<any[]>;
import pProps from 'p-props';
import _ from 'lodash';
import { fluffAttachmentRows } from './helpers.js';
import { deletePartFileListData, deletePartFileListRow, deletePartImageListData, deletePartImageListRow, insertPartFileListRows, insertPartImageListRows, insertPartVideoList, upsertPartFileList, upsertPartImageList, upsertPartVideoList } from './queries.js';
// Main exported functions for inventoryFile/Attachments
// Main function for partimagelist and partfilelist upserts
export async function upsertInventoryFileData(htpConnection, productInventoryRows, productCode) {
const responses = [];
// Filter just the inventory rows with attachments
const inventoryRowsWithAttachments = productInventoryRows.filter(row => row.attachments?.length > 0);
// Add inventoryid and storeid to all of the attachment rows for later
const modifiedAttachmentRows = inventoryRowsWithAttachments.map(row => fluffAttachmentRows(row, productCode)).flat().filter(Boolean);
// Separate image and file rows since they're handled differently
const imageAttachmentRows = modifiedAttachmentRows.filter(row => row.type === 'Image');
const fileAttachmentRows = modifiedAttachmentRows.filter(row => row.type === 'PDF' || row.type === 'Text');
const videoAttachmentRows = modifiedAttachmentRows.filter(row => row.type === 'YOUTUBE');
// Call the individual upsert functions in parallel using pProps
// Prepare the filtered rows and create promises object
const filteredImageAttachmentRows = imageAttachmentRows.length ?
_.uniqWith(imageAttachmentRows, _.isEqual) : [];
const filteredFileAttachmentRows = fileAttachmentRows.length ?
_.uniqWith(fileAttachmentRows, _.isEqual) : [];
const filteredYoutubeAttachmentRows = videoAttachmentRows.length ?
_.uniqWith(videoAttachmentRows, _.isEqual) : [];
// Create an object with promises to run in parallel
const promisesObj = {};
if (filteredImageAttachmentRows.length) {
promisesObj.imageResponse = upsertPartImageList(htpConnection, filteredImageAttachmentRows, productCode);
}
if (filteredFileAttachmentRows.length) {
promisesObj.fileResponse = upsertPartFileList(htpConnection, filteredFileAttachmentRows, productCode);
}
if (filteredYoutubeAttachmentRows.length) {
promisesObj.videoResponse = upsertPartVideoList(htpConnection, filteredYoutubeAttachmentRows, productCode);
}
// Run promises in parallel if any exist
if (Object.keys(promisesObj).length > 0) {
const results = await pProps(promisesObj);
// Add results to responses
if (results.imageResponse) {
responses.push(results.imageResponse);
}
if (results.fileResponse) {
responses.push(results.fileResponse);
}
if (results.videoResponse) {
responses.push(results.videoResponse);
}
}
return responses;
}
// Called when an inventoryfile row is deleted
export async function deleteInventoryFileRow(htpConnection, productCode, inventoryId, fileId, type) {
const responses = [];
try {
// Run delete operations in parallel using pProps
const deleteResults = await pProps({
imageListResult: deletePartImageListRow(htpConnection, productCode, inventoryId, fileId),
fileListResult: deletePartFileListRow(htpConnection, productCode, inventoryId, fileId, type)
});
responses.push(`${deleteResults.imageListResult}, ${deleteResults.fileListResult}`);
return {
success: true,
request: '',
data: responses
};
}
catch (err) {
return {
success: false,
request: '',
data: err
};
}
}
// Called as part of an inventory row delete, fileid isn't known from changeDoc
export async function deleteInventoryFileData(htpConnection, productCode, inventoryId) {
// Two delete types: when an inventory row goes, and when inventoryfile goes (though they're likely related)
// Can't join a deleted row to file for the type so we'll have to do it with inventoryid, fileid, and productcode
const responses = [];
try {
// Run delete operations in parallel using pProps
const deleteResults = await pProps({
imageListResult: deletePartImageListData(htpConnection, productCode, inventoryId),
fileListResult: deletePartFileListData(htpConnection, productCode, inventoryId)
});
responses.push(`${deleteResults.imageListResult}, ${deleteResults.fileListResult}`);
return {
success: true,
request: '',
data: responses
};
}
catch (err) {
return {
success: false,
request: '',
data: err
};
}
}
// Bulk insert function for partimagelist and partfilelist
export async function insertInventoryFileData(htpConnection, productInventoryRows, productCode) {
const responses = [];
// Filter just the inventory rows with attachments
const attachmentRows = productInventoryRows.filter(row => row.attachments?.length > 0);
// Add inventoryid to all of the attachment rows for later
const modifiedAttachmentRows = attachmentRows.map(row => fluffAttachmentRows(row, productCode)).flat();
// Separate image and file rows since they're handled differently
const imageAttachmentRows = modifiedAttachmentRows.filter(row => row.type === 'Image');
const fileAttachmentRows = modifiedAttachmentRows.filter(row => row.type === 'PDF' || row.type === 'Text');
const videoAttachmentRows = modifiedAttachmentRows.filter(row => row.type === 'YOUTUBE');
// Call the individual insert functions in parallel using pProps
// Prepare the filtered rows and create promises object
const filteredImageAttachmentRows = imageAttachmentRows.length ?
_.uniqWith(imageAttachmentRows, _.isEqual) : [];
const filteredFileAttachmentRows = fileAttachmentRows.length ?
_.uniqWith(fileAttachmentRows, _.isEqual) : [];
const filteredYoutubeAttachmentRows = videoAttachmentRows.length ?
_.uniqWith(videoAttachmentRows, _.isEqual) : [];
// Create an object with promises to run in parallel
const promisesObj = {};
if (filteredImageAttachmentRows.length) {
promisesObj.imageResponse = insertPartImageListRows(htpConnection, filteredImageAttachmentRows, productCode);
}
if (filteredFileAttachmentRows.length) {
promisesObj.fileResponse = insertPartFileListRows(htpConnection, filteredFileAttachmentRows, productCode);
}
if (filteredYoutubeAttachmentRows.length) {
promisesObj.videoResponse = insertPartVideoList(htpConnection, filteredYoutubeAttachmentRows, productCode);
}
// Run promises in parallel if any exist
if (Object.keys(promisesObj).length > 0) {
const results = await pProps(promisesObj);
// Add results to responses
if (results.imageResponse) {
responses.push(results.imageResponse);
}
if (results.fileResponse) {
responses.push(results.fileResponse);
}
if (results.videoResponse) {
responses.push(results.videoResponse);
}
}
return responses;
}
export declare function deletePartImageListData(connection: any, productCode: any, partNum: any): Promise<string>;
export declare function deletePartFileListData(connection: any, productCode: any, partNum: any): Promise<string>;
export declare function deletePartImageListRows(htpConnection: any, deleteRows: any): Promise<string>;
export declare function deletePartFileListRows(htpConnection: any, deleteRows: any): Promise<string>;
export declare function deletePartVideoListRows(htpConnection: any, deleteRows: any): Promise<string>;
export declare function deletePartImageListRow(connection: any, productCode: any, partNum: any, fileId: any): Promise<string>;
export declare function deletePartFileListRow(connection: any, productCode: any, partNum: any, fileId: any, type: any): Promise<string>;
export declare function insertPartVideoList(htpConnection: any, inventoryVideoRows: any, productCode: any): Promise<any[]>;
export declare function insertPartImageListRows(htpConnection: any, inventoryImageRows: any, productCode: any): Promise<any[]>;
export declare function insertPartFileListRows(htpConnection: any, inventoryFileRows: any, productCode: any): Promise<any[]>;
export declare function partImageListMultiRowUpsertQuery(htpConnection: any, objectArray: any): Promise<import("mysql2").ResultSetHeader>;
export declare function partVideoListMultiRowUpsertQuery(htpConnection: any, objectArray: any): Promise<import("mysql2").ResultSetHeader>;
export declare function partFileListMultiRowUpsertQuery(htpConnection: any, objectArray: any): Promise<import("mysql2").ResultSetHeader>;
export declare function upsertPartImageList(htpConnection: any, inventoryImageRows: any, productCode: any): Promise<any[]>;
export declare function upsertPartVideoList(htpConnection: any, inventoryVideoRows: any, productCode: any): Promise<any[]>;
export declare function upsertPartFileList(htpConnection: any, inventoryFileRows: any, productCode: any): Promise<any[]>;
export type ExistingFileListRow = {
partfilelistid: number;
productcode: number;
associatedid: number;
associatedidtype: 'inventoryid' | 'vehicleid';
fileid: number;
filetype: 'Unknown' | 'Text' | 'PDF';
};
export type ExistingVideoListRow = {
partvideolistid: number;
productcode: number;
store: number;
partnum: number;
videotype: 'youtube';
videoid: string;
url: string | null;
dirty: 'False' | 'True';
};
export type ExistingImageListRow = {
partimagelistid: number;
productcode: number;
inventoryid: number;
fileid: number | null;
imagelocation: string | null;
rank: number | null;
dirty: 'False' | 'True';
};
export declare function queryExistingAttachmentRows<T>(connection: any, table: any, column: any, ids: any, productCode: any): Promise<T[]>;
import { query, multiRowInsertQuery, write } from '../shared.js';
import { convertManyInventoryFileRows, convertManyInventoryImageRows, convertManyPartVideoListRows, partitionFileListRows, partitionImageListRows, partitionVideoListRows } from './helpers.js';
// ==== Delete Query Functions ====
// Called when an inventory row is deleted
export async function deletePartImageListData(connection, productCode, partNum) {
try {
const queryOptions = {
sql: `DELETE FROM partimagelist WHERE productcode = ? AND inventoryid = ?`,
values: [productCode, partNum]
};
const deleteResponse = await write(connection, queryOptions);
return `partimagelist delete affected rows: ${deleteResponse?.affectedRows}`;
}
catch (err) {
return `error deleting partimagelist rows: ${err}`;
}
}
// Called when an inventory row is deleted
export async function deletePartFileListData(connection, productCode, partNum) {
try {
const queryOptions = {
sql: `DELETE FROM partfilelist WHERE productcode = ? AND associatedidtype = 'inventoryid' AND associatedid = ?`,
values: [productCode, partNum]
};
const deleteResponse = await write(connection, queryOptions);
return `partfilelist delete affected rows: ${deleteResponse?.affectedRows}`;
}
catch (err) {
return `error deleting partfilelist rows: ${err}`;
}
}
// Called in upsertPartImageList to cover non-public rows
export async function deletePartImageListRows(htpConnection, deleteRows) {
//const deleteRows = nonPublicRows.map(row => ([productCode, row.inventoryid, row.id]))
try {
const queryOptions = {
sql: `DELETE FROM partimagelist WHERE (productcode, inventoryid, fileid) in (?)`,
values: [deleteRows]
};
const deleteResponse = await write(htpConnection, queryOptions);
return `partimagelist delete affected rows: ${deleteResponse?.affectedRows}`;
}
catch (err) {
return `error deleting partimagelist rows: ${err}`;
}
// const deleteResponse = await deletePartImageListRows(htpConnection, deleteRows)
}
// Called in upsertPartImageList to cover possible non-public rows
export async function deletePartFileListRows(htpConnection, deleteRows) {
//const deleteRows = nonPublicRows.map(row => ([productCode, 'inventoryid', row.inventoryid, row.id]))
try {
const queryOptions = {
sql: `DELETE FROM partfilelist WHERE (productcode, associatedid, associatedidtype, fileid) in (?)`,
values: [deleteRows]
};
const deleteResponse = await write(htpConnection, queryOptions);
return `non-public partfilelist delete affected rows: ${deleteResponse?.affectedRows}`;
}
catch (err) {
return `error deleting partfilelist rows: ${err}`;
}
}
export async function deletePartVideoListRows(htpConnection, deleteRows) {
//const deleteRows = nonPublicRows.map(row => ([productCode, 'inventoryid', row.inventoryid, row.id]))
try {
const queryOptions = {
sql: `DELETE FROM partvideolist WHERE (productcode, store, partnum, videoid) in (?)`,
values: [deleteRows]
};
const deleteResponse = await write(htpConnection, queryOptions);
return `non-public partvideolist delete affected rows: ${deleteResponse?.affectedRows}`;
}
catch (err) {
return `error deleting partvideolist rows: ${err}`;
}
}
// Called in deleteInventoryFileData when an inventoryfile row is deleted
export async function deletePartImageListRow(connection, productCode, partNum, fileId) {
try {
const queryOptions = {
sql: `DELETE FROM partimagelist WHERE productcode = ? AND inventoryid = ? AND fileid = ?`,
values: [productCode, partNum, fileId]
};
const deleteResponse = await write(connection, queryOptions);
return `partimagelist delete affected rows: ${deleteResponse?.affectedRows}`;
}
catch (err) {
return `error deleting partimagelist rows: ${err}`;
}
}
// Called by deleteInventoryFileRow when an inventoryfile row is deleted
export async function deletePartFileListRow(connection, productCode, partNum, fileId, type) {
try {
const queryOptions = {
sql: `DELETE FROM partfilelist WHERE productcode = ? AND associatedid = ? AND associatedidtype = ? AND fileid = ?`,
values: [productCode, partNum, type, fileId]
};
const deleteResponse = await write(connection, queryOptions);
return `partfilelist delete affected rows: ${deleteResponse?.affectedRows}`;
}
catch (err) {
return `error deleting partfilelist rows: ${err}`;
}
}
// ==== Insert Query Functions ====
export async function insertPartVideoList(htpConnection, inventoryVideoRows, productCode) {
const responses = [];
const publicRows = inventoryVideoRows.filter(row => row.public === 'True');
if (publicRows.length) {
const convertedRows = publicRows.map(row => convertManyPartVideoListRows(row));
const columns = Object.keys(convertedRows[0]);
const values = convertedRows.map(obj => Object.values(obj));
const response = await multiRowInsertQuery(htpConnection, 'partvideolist', columns, values);
responses.push(`partvideolist affected rows: ${response?.affectedRows}`);
}
else {
responses.push(`partvideolist affected rows: none`);
}
return responses;
}
export async function insertPartImageListRows(htpConnection, inventoryImageRows, productCode) {
const responses = [];
// Filter for public rows and upsert
const publicRows = inventoryImageRows.filter(row => row.public === 'True');
if (publicRows.length) {
// Convert rows
const convertedRows = publicRows.map(row => convertManyInventoryImageRows(row));
//const upsertResponse = await partImageListInsertQuery(htpConnection, convertedRows)
const columns = Object.keys(convertedRows[0]);
const values = convertedRows.map(obj => Object.values(obj));
const response = await multiRowInsertQuery(htpConnection, 'partimagelist', columns, values);
responses.push(`partimagelist affected rows: ${response?.affectedRows}`);
}
else {
responses.push(`partimagelist affected rows: none`);
}
return responses;
}
export async function insertPartFileListRows(htpConnection, inventoryFileRows, productCode) {
const responses = [];
const publicRows = inventoryFileRows.filter(row => row.public === 'True');
if (publicRows.length) {
// Convert rows
const convertedRows = publicRows.map(row => convertManyInventoryFileRows(row));
// const response = await partFileListInsertQuery(htpConnection, convertedRows)
const columns = Object.keys(convertedRows[0]);
const values = convertedRows.map(obj => Object.values(obj));
const response = await multiRowInsertQuery(htpConnection, 'partfilelist', columns, values);
responses.push(`partfilelist affected rows: ${response?.affectedRows}`);
}
else {
responses.push(`partfilelist affected rows: none`);
}
return responses;
}
// ==== Upsert Query Functions ====
export async function partImageListMultiRowUpsertQuery(htpConnection, objectArray) {
// Get columns
const columns = Object.keys(objectArray[0]);
const values = objectArray.map(obj => Object.values(obj));
const queryOptions = {
sql: 'INSERT INTO partimagelist (??) VALUES ? ON DUPLICATE KEY UPDATE `rank` = values(`rank`)',
values: [columns, values]
};
return await write(htpConnection, queryOptions);
}
export async function partVideoListMultiRowUpsertQuery(htpConnection, objectArray) {
// Get columns
const columns = Object.keys(objectArray[0]);
const values = objectArray.map(obj => Object.values(obj));
const queryOptions = {
sql: 'INSERT INTO partvideolist (??) VALUES ? ON DUPLICATE KEY UPDATE `url` = values(`url`)',
values: [columns, values]
};
return await write(htpConnection, queryOptions);
}
export async function partFileListMultiRowUpsertQuery(htpConnection, objectArray) {
// Get columns
const columns = Object.keys(objectArray[0]);
const values = objectArray.map(obj => Object.values(obj));
const queryOptions = {
sql: `INSERT IGNORE INTO partfilelist (??) VALUES ?`,
values: [columns, values]
};
return await write(htpConnection, queryOptions);
}
export async function upsertPartImageList(htpConnection, inventoryImageRows, productCode) {
const responses = [];
// Get inventoryids and query existing rows up front — needed for both insert and stale-delete
const inventoryIds = inventoryImageRows.map(row => row.inventoryid);
const existingRows = await queryExistingAttachmentRows(htpConnection, 'partimagelist', 'inventoryid', inventoryIds, productCode);
// Convert all public incoming rows
const publicRows = inventoryImageRows.filter(row => row.public === 'True');
const convertedRows = publicRows.map(row => convertManyInventoryImageRows(row));
// Single pass: new rows to insert, stale rows to delete
const { toInsert, toDelete } = partitionImageListRows(convertedRows, existingRows);
if (toDelete.length) {
const deleteRows = toDelete.map(row => ([row.productcode, row.inventoryid, row.fileid]));
const deleteResponse = await deletePartImageListRows(htpConnection, deleteRows);
responses.push(deleteResponse);
}
// Also delete any non-public rows that exist in the DB
const nonPublicRows = inventoryImageRows.filter(row => row.public === 'False');
if (nonPublicRows.length) {
const deleteRows = nonPublicRows.map(row => ([productCode, row.inventoryid, row.file]));
const deleteResponse = await deletePartImageListRows(htpConnection, deleteRows);
responses.push(deleteResponse);
}
if (toInsert.length) {
const upsertResponse = await partImageListMultiRowUpsertQuery(htpConnection, toInsert);
responses.push(`partimagelist affected rows: ${upsertResponse?.affectedRows}`);
}
else {
responses.push(`partimagelist affected rows: none`);
}
return responses;
}
export async function upsertPartVideoList(htpConnection, inventoryVideoRows, productCode) {
const responses = [];
// Get inventoryids and query existing rows up front — needed for both insert and stale-delete
const inventoryIds = inventoryVideoRows.map(row => row.inventoryid);
const existingRows = await queryExistingAttachmentRows(htpConnection, 'partvideolist', 'partnum', inventoryIds, productCode);
// Convert all public incoming rows
const publicRows = inventoryVideoRows.filter(row => row.public === 'True');
const convertedRows = publicRows.map(row => convertManyPartVideoListRows(row));
// Single pass: new rows to insert, stale rows to delete
const { toInsert, toDelete } = partitionVideoListRows(convertedRows, existingRows);
if (toDelete.length) {
const deleteRows = toDelete.map(row => ([row.productcode, row.store, row.partnum, row.videoid]));
const deleteResponse = await deletePartVideoListRows(htpConnection, deleteRows);
responses.push(deleteResponse);
}
// Also delete any non-public rows that exist in the DB
const nonPublicRows = inventoryVideoRows.filter(row => row.public === 'False');
if (nonPublicRows.length) {
const deleteRows = nonPublicRows.map(row => ([productCode, row.storeid, row.inventoryid, row.video.id]));
const deleteResponse = await deletePartVideoListRows(htpConnection, deleteRows);
responses.push(deleteResponse);
}
if (toInsert.length) {
const upsertResponse = await partVideoListMultiRowUpsertQuery(htpConnection, toInsert);
responses.push(`partvideolist affected rows: ${upsertResponse?.affectedRows}`);
}
else {
responses.push(`partvideolist affected rows: none`);
}
return responses;
}
export async function upsertPartFileList(htpConnection, inventoryFileRows, productCode) {
const responses = [];
// Get inventoryids and query existing rows up front — needed for both insert and stale-delete
const inventoryIds = inventoryFileRows.map(row => row.inventoryid);
const existingRows = await queryExistingAttachmentRows(htpConnection, 'partfilelist', 'associatedid', inventoryIds, productCode);
// Convert all public incoming rows
const publicRows = inventoryFileRows.filter(row => row.public === 'True');
const convertedRows = publicRows.map(row => convertManyInventoryFileRows(row));
// Single pass: new rows to insert, stale rows to delete
const { toInsert, toDelete } = partitionFileListRows(convertedRows, existingRows);
if (toDelete.length) {
const deleteRows = toDelete.map(row => ([row.productcode, row.associatedid, row.associatedidtype, row.fileid]));
const deleteResponse = await deletePartFileListRows(htpConnection, deleteRows);
responses.push(deleteResponse);
}
// Also delete any non-public rows that exist in the DB
const nonPublicRows = inventoryFileRows.filter(row => row.public === 'False');
if (nonPublicRows.length) {
const deleteRows = nonPublicRows.map(row => ([productCode, 'inventoryid', row.inventoryid, row.file]));
const deleteResponse = await deletePartFileListRows(htpConnection, deleteRows);
responses.push(deleteResponse);
}
if (toInsert.length) {
const upsertResponse = await partFileListMultiRowUpsertQuery(htpConnection, toInsert);
responses.push(`partfilelist affected rows: ${upsertResponse?.affectedRows}`);
}
else {
responses.push(`partfilelist affected rows: none`);
}
return responses;
}
export async function queryExistingAttachmentRows(connection, table, column, ids, productCode) {
const queryOptions = {
sql: `SELECT * FROM ?? WHERE productcode = ? AND ?? IN (?)`,
values: [table, productCode, column, ids]
};
return await query(connection, queryOptions);
}
export declare function upsertInventoryOptionRows(connection: any, productInventoryPartUseRows: any, productCode: any): Promise<string>;
export declare function deleteInventoryOptionRow(connection: any, productCode: any, partNum: any, typeNum: any, option: any, value: any): Promise<{
success: boolean;
request: string;
data: string;
}>;
export declare function deleteInventoryOptionRows(connection: any, productCode: any, storeNum: any, partNum: any): Promise<string>;
export declare function insertInventoryOptionRows(connection: any, productInventoryPartUseRows: any, productCode: any): Promise<string>;
import mysql from 'mysql2/promise';
import { multiRowInsertQuery, query, queryFirst, write } from './shared.js';
import _ from 'lodash';
function convertManyInventoryOptionValue(inventoryOptionListData, productInventoryRow, productCode) {
const tags = productInventoryRow.tags;
const inventoryOptionRows = tags.map(function (tag) {
if (tag.value && tag.label) {
// Check inventoryoptionlist for match to use for building inventoryoption(value) row
const inventoryOptionListRow = inventoryOptionListData.find(element => tag.label === element.option && productInventoryRow.typeNum === element.sourcetypenum && (productInventoryRow.partListAuthorityId ? productInventoryRow.partListAuthorityId === element.typenum : productInventoryRow.typeNum === element.typenum));
if (inventoryOptionListRow) {
const { optionnum } = inventoryOptionListRow;
return {
productcode: productCode,
storenum: productInventoryRow.storeId,
partnum: productInventoryRow.inventoryId,
value: tag.value,
optionnum,
lastupdate: mysql.raw('NOW()'),
};
}
else {
console.debug(`No matching inventoryoptionlist row found for inventoryRow.tag.label ${tag.label} of inventory id ${productInventoryRow.inventoryId}`);
return;
}
}
});
return inventoryOptionRows.filter(row => row !== undefined);
}
async function inventoryOptionMultiRowUpsertQuery(htpConnection, objectArray) {
// Get columns
const columns = Object.keys(objectArray[0]);
const values = objectArray.map(obj => Object.values(obj));
const queryOptions = {
sql: `INSERT INTO inventoryoption (??) VALUES ? ON DUPLICATE KEY UPDATE ?? = values(??)`,
values: [columns, values, 'value', 'value']
};
return await write(htpConnection, queryOptions);
}
// Main multi-row upsert function
export async function upsertInventoryOptionRows(connection, productInventoryPartUseRows, productCode) {
// Get the relevant typenums to find existing optionlist rows
const allTypeNums = productInventoryPartUseRows.map(function (row) {
return row.typeNum;
});
const uniqueTypeNums = _.uniqWith(allTypeNums, _.isEqual);
// Get a list of inventoryoptionlist rows
const inventoryOptionListData = await getInventoryOptionList(connection, productCode, uniqueTypeNums);
const inventoryOptionRows = productInventoryPartUseRows.map(row => convertManyInventoryOptionValue(inventoryOptionListData, row, productCode)).flat();
// const inventoryOptionMultiRowUpsertResponse= await inventoryOptionMultiRowUpsertQuery(connection, inventoryOptionRows)
if (inventoryOptionRows.length) {
const inventoryOptionMultiRowUpsertResponse = await inventoryOptionMultiRowUpsertQuery(connection, inventoryOptionRows);
return (`affected rows: inventoryoption ${inventoryOptionMultiRowUpsertResponse?.affectedRows}`);
}
else {
return (`affected rows: inventoryoption none`);
}
}
async function queryInventoryOptionList(connection, productCode, typeNum, option) {
const queryOptions = {
sql: `SELECT * FROM inventoryoptionlist WHERE productcode = ? AND typenum = ? AND inventoryoptionlist.option = ?`,
values: [productCode, typeNum, option]
};
return await queryFirst(connection, queryOptions);
}
// Deletes a specific row
export async function deleteInventoryOptionRow(connection, productCode, partNum, typeNum, option, value) {
// Need inventoryoptionlist data to get optionnum from inventoryoptionlist for a delete
const queryResult = await queryInventoryOptionList(connection, productCode, typeNum, option);
if (queryResult?.optionnum) {
try {
const queryOptions = {
sql: `DELETE FROM inventoryoption WHERE productcode = ? AND partnum = ? AND optionnum = ? AND value = ?`,
values: [productCode, partNum, queryResult.optionnum, value]
};
const deleteResponse = await write(connection, queryOptions);
return {
success: true,
request: '',
data: `inventoryoption delete affected rows: ${deleteResponse?.affectedRows}`
};
}
catch (err) {
return {
success: false,
request: '',
data: `error deleting inventoryoption rows: ${err}`
};
}
}
else {
return {
success: false,
request: '',
data: `no matching inventoryoptionlist value for typenum: ${typeNum} option: ${option}`
};
}
}
// Deletes all rows for an inventory item
export async function deleteInventoryOptionRows(connection, productCode, storeNum, partNum) {
// Get columns
try {
const queryOptions = {
sql: `DELETE FROM inventoryoption WHERE productcode = ? AND storenum = ? AND partnum = ?`,
values: [productCode, storeNum, partNum]
};
const deleteResponse = await write(connection, queryOptions);
return `inventoryoption delete affected rows: ${deleteResponse?.affectedRows}`;
}
catch (err) {
return `error deleting inventoryoption rows: ${err}`;
}
}
// Bulk insert function
export async function insertInventoryOptionRows(connection, productInventoryPartUseRows, productCode) {
// grab all the relevant inventoryoptionlist values
// Get the relevant typenums to find existing optionlist rows
const allTypeNums = productInventoryPartUseRows.map(function (row) {
return row.typeNum;
});
const uniqueTypeNums = _.uniqWith(allTypeNums, _.isEqual);
// Get a list of inventoryoptionrows
const inventoryOptionListData = await getInventoryOptionList(connection, productCode, uniqueTypeNums);
const inventoryOptionRows = productInventoryPartUseRows.map(row => convertManyInventoryOptionValue(inventoryOptionListData, row, productCode)).flat();
if (inventoryOptionRows.length > 0) {
const columns = Object.keys(inventoryOptionRows[0]);
const values = inventoryOptionRows.map(obj => Object.values(obj));
const response = await multiRowInsertQuery(connection, 'inventoryoption', columns, values);
return (`affected rows: inventoryoption ${response?.affectedRows}`);
}
else {
return (`affected rows: inventoryoption none`);
}
}
async function getInventoryOptionList(connection, productCode, typeNums) {
const queryData = [typeNums];
const queryOptions = {
sql: "SELECT * FROM inventoryoptionlist WHERE productcode = ? AND sourcetypenum IN ?",
values: [productCode, queryData]
};
return await query(connection, queryOptions);
}
export declare function getPartUseInfoQuery(connection: any, inventoryObjectRow: any, productCode: any): Promise<unknown>;
export declare function upsertInventoryOptionListRows(connection: any, inventoryRows: any, productCode: any): Promise<any[]>;
export declare function deleteInventoryOptionListRow(connection: any, productCode: any, typeNum: any, option: any, rank: any): Promise<{
success: boolean;
request: string;
data: any;
}>;
export declare function insertInventoryOptionListRows(connection: any, productInventoryRows: any, productCode: any): Promise<any[]>;
import { multiRowInsertQuery, query, queryFirst, write } from './shared.js';
import _ from "lodash";
import mysql from 'mysql2/promise';
// Get partlistauthorityid and partuse from HTP using inventorytype name from universal inventory object
export async function getPartUseInfoQuery(connection, inventoryObjectRow, productCode) {
return await queryFirst(connection, {
sql: `SELECT partuse.*, partlistauthoritymap.partlistauthorityid
FROM partuse
LEFT JOIN partlistauthoritymap
ON partuse.productcode = partlistauthoritymap.productcode and
partuse.typenum = partlistauthoritymap.sourceid
WHERE partuse.productcode = ? AND part = ?`,
values: [productCode, inventoryObjectRow.inventoryType], //only inventorytype.name is provided
});
}
async function getExistingInventoryOptionList(connection, productCode, uniqueTypeNums) {
const queryData = [uniqueTypeNums];
// product.typeNum => htp.sourcetypenum
const queryOptions = {
sql: "SELECT optionnum, typenum, sourcetypenum, `option`, `rank`, public FROM inventoryoptionlist WHERE productcode = ? AND sourcetypenum IN ?",
values: [productCode, queryData],
};
const result = await query(connection, queryOptions);
// Create a list to compare against
const resultCache = result.map(function (row) {
return {
optionnum: row.optionnum,
typenum: row.typenum,
sourcetypenum: row.sourcetypenum,
option: row.option,
rank: row.rank,
productcode: productCode,
public: row.public // default is true so we should be safe
//lastupdate: mysql.raw('NOW()'),
};
});
return resultCache;
}
async function inventoryOptionListMultiRowUpsertQuery(htpConnection, optionListArray) {
const newOptionListRows = optionListArray.map(obj => ({ ...obj, lastupdate: mysql.raw('NOW()') }));
const columns = Object.keys(newOptionListRows[0]);
const values = optionListArray.map(obj => Object.values(obj));
const queryOptions = {
sql: 'INSERT INTO inventoryoptionlist (??) VALUES ? ON DUPLICATE KEY UPDATE `rank` = values(`rank`), lastupdate = values(lastupdate), public = values(public)',
values: [columns, values]
};
return await write(htpConnection, queryOptions);
}
function convertManyInventoryOptionList(productInventoryRow, productCode) {
//10 valid tags
const inventoryOptionListRows = productInventoryRow.tags.map(function (tag) {
// This only checks that both a value and label exist
if (tag.value && tag.label) {
return {
sourcetypenum: productInventoryRow.typeNum,
typenum: productInventoryRow.partListAuthorityId || productInventoryRow.typeNum,
option: tag.label,
rank: tag.rank,
productcode: productCode,
// TODO: Confirm this is correct or not
public: tag.public ? tag.public : 'True',
//lastupdate: mysql.raw('NOW()'),
};
}
});
// This gets rid of undefined rows, usually caused by empty strings
const filteredRows = inventoryOptionListRows.filter(row => row !== undefined);
return filteredRows;
}
// Only a 5x difference in speed compared to the previous filter + find method but always tests faster
function filterExistingOptionListRows(newRows, existingRows) {
// Create a Map for lookups
const existingRowsMap = new Map();
// Populate the Map with composite keys
existingRows.forEach(row => {
const key = `${row.typenum}|${row.option}|${row.sourcetypenum}`;
existingRowsMap.set(key, true);
});
// Filter newRows using the Map for lookups
return newRows.filter(newRow => {
const key = `${newRow.typenum}|${newRow.option}|${newRow.sourcetypenum}`;
return !existingRowsMap.has(key);
});
}
// Old method, keeping here in case there are any side effects of the new Map method
// that testing didn't catch.
function filterExistingOptionListRowsOld(newRows, existingRows) {
const uniqueRows = newRows.filter(function (newRow) {
return !existingRows.find(function (existingRow) {
return existingRow.typenum === newRow.typenum && existingRow.option === newRow.option && existingRow.sourcetypenum === newRow.sourcetypenum;
});
});
return uniqueRows;
}
function matchExistingFileListRows(newRows, existingRows) {
const updatedRows = [];
// Find matching rows, add optionnum and return updated object
//const testRows = newRows.map(row => addOptionNum(existingRows, row))
//or
for (const row of newRows) {
const existingRowMatch = existingRows.find(element => row.typenum === element.typenum && row.option === element.option);
if (existingRowMatch) {
const { optionnum } = existingRowMatch;
row.optionnum = optionnum;
updatedRows.push(row);
}
}
return updatedRows;
}
// New function that takes the entire array of inventory rows with partuse data
// Option, rank, and public are the possible changeable values
export async function upsertInventoryOptionListRows(connection, inventoryRows, productCode) {
const responses = [];
// Get the tags from all of the inventoryRows, there will be some duplicates
const optionListRows = inventoryRows.map(row => convertManyInventoryOptionList(row, productCode)).flat();
//Remove duplicates
const uniqueFilteredRows = _.uniqWith(optionListRows, _.isEqual);
// Get the relevant typenums to find existing optionlist rows
const allTypeNums = inventoryRows.map(function (row) {
return row.typeNum;
});
const uniqueTypeNums = _.uniqWith(allTypeNums, _.isEqual);
// Get existing optionlist rows
const existingOptionList = await getExistingInventoryOptionList(connection, productCode, uniqueTypeNums);
// Create list of new and unique items for upsert
const uniqueOptionListRows = filterExistingOptionListRows(uniqueFilteredRows, existingOptionList);
// Upsert probably won't work as optionnum + productcode is the primary key
// If option changes, it will add a new one
if (uniqueOptionListRows.length) {
// Add lastupdate
const newOptionListRows = uniqueOptionListRows.map(obj => ({ ...obj, lastupdate: mysql.raw('NOW()') }));
const columns = Object.keys(newOptionListRows[0]);
const values = newOptionListRows.map(obj => Object.values(obj));
const response = await multiRowInsertQuery(connection, 'inventoryoptionlist', columns, values);
responses.push(`affected rows: new inventoryoptionlist ${response?.affectedRows}`);
}
else {
responses.push(`affected rows: new inventoryoptionlist none`);
}
const rowsWithOptionNum = matchExistingFileListRows(uniqueFilteredRows, existingOptionList);
if (rowsWithOptionNum.length) {
const newOptionListRows = rowsWithOptionNum.map(obj => ({ ...obj, lastupdate: mysql.raw('NOW()') }));
// Need to do a custom upsert query
const upsertResponse = await inventoryOptionListMultiRowUpsertQuery(connection, newOptionListRows);
responses.push(`affected rows: existing inventoryoptionlist ${upsertResponse?.affectedRows}`);
}
return responses;
}
export async function deleteInventoryOptionListRow(connection, productCode, typeNum, option, rank) {
const queryOptions = {
sql: `DELETE FROM inventoryoptionlist WHERE productcode = ? AND typenum = ? AND inventoryoptionlist.option = ?`,
values: [productCode, typeNum, option]
};
try {
const result = await write(connection, queryOptions);
return {
success: true,
request: '',
data: `inventoryoptionlist delete affected rows: ${result.affectedRows}`
};
}
catch (err) {
return {
success: false,
request: '',
data: err
};
}
}
// Bulk synch insert
export async function insertInventoryOptionListRows(connection, productInventoryRows, productCode) {
const responses = [];
// Convert rows to insertable objects
const optionListRows = productInventoryRows.map(row => convertManyInventoryOptionList(row, productCode)).flat();
// Remove duplicates
const uniqueFilteredRows = _.uniqWith(optionListRows, _.isEqual);
// Get the relevant typenums to find existing optionlist rows
const allTypeNums = productInventoryRows.map(function (row) {
return row.typeNum;
});
const uniqueTypeNums = _.uniqWith(allTypeNums, _.isEqual);
// Get list of existing rows from db to compare to
const existingOptionListRows = await getExistingInventoryOptionList(connection, productCode, uniqueTypeNums);
// Create a list of new items to be inserted
const uniqueOptionListRows = filterExistingOptionListRows(uniqueFilteredRows, existingOptionListRows);
// Add lastupdate to rows
// TODO: this can be done as part of the conversion, its a leftover from a failed unique comparison func
if (uniqueOptionListRows.length) {
const newOptionListRows = uniqueOptionListRows.map(obj => ({ ...obj, lastupdate: mysql.raw('NOW()') }));
const columns = Object.keys(newOptionListRows[0]);
const values = newOptionListRows.map(obj => Object.values(obj));
const response = await multiRowInsertQuery(connection, 'inventoryoptionlist', columns, values);
responses.push(`affected rows: inventoryoptionlist ${response?.affectedRows}`);
}
else {
responses.push(`affected rows: inventoryoptionlist none`);
}
return responses;
}
export declare function deleteInventorySourceData(connection: any, productCode: any, partNum: any): Promise<string>;
export declare function deleteInventorySourceRow(connection: any, productCode: any, inventorySourceId: any): Promise<{
success: boolean;
data: string;
}>;
export declare function upsertInventorySourceRows(connection: any, productInventoryRows: any, productCode: any): Promise<any[]>;
import { multiRowUpsertQuery, multiRowInsertQuery, write } from './shared.js';
import pProps from 'p-props';
// TODO: remove insert functions...everything should work with upserts
// TODO: upserts may not be working correctly and instead inserting new rows every time
// Called by inventory row delete
export async function deleteInventorySourceData(connection, productCode, partNum) {
try {
const queryOptions = {
sql: `DELETE FROM inventorysource WHERE productcode = ? AND partnum = ?`,
values: [productCode, partNum]
};
const deleteResponse = await write(connection, queryOptions);
return `inventorysource delete affected rows: ${deleteResponse?.affectedRows}`;
}
catch (err) {
return `error deleting inventorysource row: ${err}`;
}
}
// Called when an inventory source row is deleted
export async function deleteInventorySourceRow(connection, productCode, inventorySourceId) {
try {
const queryOptions = {
sql: `DELETE FROM inventorysource WHERE productcode = ? AND inventorysourceid`,
values: [productCode, inventorySourceId]
};
const deleteResponse = await write(connection, queryOptions);
return {
success: true,
data: `inventorysource delete affected rows: ${deleteResponse?.affectedRows}`
};
}
catch (err) {
return {
success: false,
data: `error deleting inventorysource row: ${err}`
};
}
}
// #things that can change: quantity, quantityallocated, date? status?
// #Compare new and existing rows by
// #inventorystoreid, inventoryid, documenttype, documentid, documentstoreid, documentlineid
// #if any match, update those rows after grabbing the htpinventorysourceid
// #if any new rows don't match, insert those as new
// Finds existing rows to be updated
function filterExistingInventorySourceRows(newRows, existingRows) {
// For each new row, find the matching existing row
const updateRows = [];
for (const newRow of newRows) {
const matchingRow = existingRows.find(function (existingRow) {
return existingRow.inventorystoreid === newRow.inventorystoreid && existingRow.inventoryid === newRow.inventoryid
&& newRow.documenttype === existingRow.documenttype && newRow.documentid == existingRow.documentid &&
newRow.documentstoreid == existingRow.documentstoreid && newRow.documentlineid == existingRow.documentlineid;
});
// If exists, get the htpinventorysourceid and add it to updateRows
if (matchingRow) {
newRow.htpinventorysourceid = matchingRow.htpinventorysourceid;
updateRows.push(newRow);
}
}
return updateRows;
}
// Finds new rows for insert
function filterNewInventorySourceRows(newRows, existingRows) {
const newInventorySourceRows = newRows.filter(function (newRow) {
return !existingRows.find(function (existingRow) {
return existingRow.inventorystoreid === newRow.inventorystoreid && existingRow.inventoryid === newRow.inventoryid
&& newRow.documenttype === existingRow.documenttype && newRow.documentid == existingRow.documentid &&
newRow.documentstoreid == existingRow.documentstoreid && newRow.documentlineid == existingRow.documentlineid;
});
});
return newInventorySourceRows;
}
async function queryExistingInventorySourceRows(connection, inventoryIds, productCode) {
const queryOptions = {
sql: `SELECT * FROM inventorysource WHERE productcode = ? AND inventoryid IN (?)`,
values: [productCode, inventoryIds]
};
return await write(connection, queryOptions);
}
// Called by inventory upsert
export async function upsertInventorySourceRows(connection, productInventoryRows, productCode) {
const responses = [];
// Flatten the rows then create source map array, add productcode and partnum
const convertedInventorySourceRows = productInventoryRows.flatMap(inventoryRow => inventoryRow.openSources.map(sourceRow => ({
inventorystoreid: sourceRow.inventoryStoreId,
quantity: sourceRow.quantity,
quantityallocated: sourceRow.quantityAllocated,
documenttype: sourceRow.documentType,
documentstoreid: sourceRow.documentStoreId,
documentlineid: sourceRow.documentLineId,
date: sourceRow.date,
status: sourceRow.status,
productcode: productCode,
inventoryid: inventoryRow.inventoryId,
})));
// Filter out new rows for insert
if (convertedInventorySourceRows.length) {
// Get the inventoryids and query existing rows
const inventoryIds = productInventoryRows.map(row => row.inventoryId);
const existingInventorySourceRows = await queryExistingInventorySourceRows(connection, inventoryIds, productCode);
//things that can change: quantity, quantityallocated, date? status?
// Find new rows for insert
const newInventorySourceRows = filterNewInventorySourceRows(convertedInventorySourceRows, existingInventorySourceRows);
// Find existing rows for possible update
const updateInventorySourceRows = filterExistingInventorySourceRows(convertedInventorySourceRows, existingInventorySourceRows);
//const uniqueRows = filterExistingInventorySourceRows(convertedInventorySourceRows, existingInventorySourceRows)
let promisesObj = {};
if (newInventorySourceRows.length) {
const columns = Object.keys(newInventorySourceRows[0]);
const values = newInventorySourceRows.map(obj => Object.values(obj));
promisesObj.insertResponse = multiRowInsertQuery(connection, "inventorysource", columns, values);
}
if (updateInventorySourceRows.length) {
promisesObj.updateResponse = multiRowUpsertQuery(connection, 'inventorysource', updateInventorySourceRows);
}
if (Object.keys(promisesObj).length > 0) {
const results = await pProps(promisesObj);
if (results.insertResponse) {
responses.push(`new inventorysource affected rows: ${results.insertResponse?.affectedRows}`);
}
if (results.updateResponse) {
responses.push(`updated inventorysource affected rows: ${results.updateResponse?.affectedRows}`);
}
}
else {
responses.push(`inventorysource affected rows: none`);
}
}
else {
responses.push(`inventory source affected rows: none`);
}
return responses;
}
export declare function upsertVehicleRows(connection: any, vehicleArr: any, productCode: any): Promise<{
success: boolean;
request: string;
data: any;
}>;
export declare function deleteVehicle(connection: any, productCode: any, stockNum: any, store: any): Promise<{
success: boolean;
request: string;
data: any;
}>;
import { multiRowUpsertQuery, write } from './shared.js';
import mysql from 'mysql2/promise';
// TODO: Change naming to invmaster to match others
function convertVehicle(vehicleRow, productCode) {
return {
stocknum: vehicleRow.id,
// Original trigger doesn't use stocknumber
store: vehicleRow.storeId,
vinnum: vehicleRow.vin,
make: vehicleRow.make,
model: vehicleRow.model,
year: vehicleRow.year,
mileage: vehicleRow.mileage,
bodystyle: vehicleRow.bodyStyle,
extcolor: vehicleRow.colors[1].name,
extcolorcode: vehicleRow.colors[1].code,
intcolor: vehicleRow.colors[0].name,
intcolorcode: vehicleRow.colors[0].code,
status: vehicleRow.status,
dateentered: vehicleRow.dateEntered,
trackingnum: vehicleRow.tagNumber,
damage: vehicleRow.description,
productcode: productCode,
lastupdate: mysql.raw('NOW()'),
titlenum: vehicleRow.title?.number || '',
titlestatus: vehicleRow.title?.status || '',
titletype: vehicleRow.title?.type || ''
};
}
// Multiple row insert
export async function upsertVehicleRows(connection, vehicleArr, productCode) {
const responses = [];
try {
// Convert to HTP insertable object
const convertedRows = vehicleArr.map(row => convertVehicle(row, productCode));
const multiRowUpsertResponse = await multiRowUpsertQuery(connection, 'invmaster', convertedRows);
responses.push(`invmaster affected rows: ${multiRowUpsertResponse.affectedRows}`);
return {
success: true,
request: '',
data: responses
};
}
catch (err) {
return {
success: false,
request: '',
data: err
};
}
}
export async function deleteVehicle(connection, productCode, stockNum, store) {
const responses = [];
try {
const queryOptions = {
sql: `DELETE FROM invmaster WHERE productcode = ? AND stocknum = ? AND store = ?`,
values: [productCode, stockNum, store],
};
const deleteResponse = await write(connection, queryOptions);
responses.push(`invmaster delete affected rows: ${deleteResponse?.affectedRows}`);
return {
success: true,
request: '',
data: responses
};
}
catch (err) {
return {
success: false,
request: '',
data: err
};
}
}
export declare function upsertMnfcrmodModelRows(connection: any, modelArr: any, productCode: any): Promise<{
success: boolean;
request: string;
data: any;
}>;
export declare function deleteMnfcrmodModelRow(connection: any, typeNum: any, manufacturerName: any, modelName: any, firstYear: any, lastYear: any, vehicleUnit: any, productCode: any): Promise<{
success: boolean;
request: string;
data: any;
}>;
export declare function insertMnfcrmodModelRows(connection: any, modelArr: any, productCode: any): Promise<{
success: boolean;
request: string;
data: any;
}>;
import { multiRowInsertQuery, multiRowUpsertQuery, query, write } from './shared.js';
import mysql from 'mysql2/promise';
// Convert universal model object to HTP insertable row for mnfcrmod
function convertMnfcrmod(modelRow, productCode) {
return {
typenum: modelRow.inventoryTypeId,
pmanufacturer: modelRow.manufacturerName?.substring(0, 24) ?? '',
pmodel: modelRow.name,
firstyear: modelRow.firstYear,
lastyear: modelRow.lastYear,
//weight: ,
productcode: productCode,
active: modelRow.active,
// Following are required but not in object
sourcetypenum: modelRow.inventoryTypeId,
category: 'Used',
intcode: '',
lastupdate: mysql.raw('NOW()'),
};
}
// Convert universal model object to HTP insertable row for model
function convertModel(modelRow, productCode) {
return {
Model: modelRow.name?.substring(0, 29) ?? '',
Make: modelRow.manufacturerName?.substring(0, 24) ?? '',
FirstYear: modelRow.firstYear,
LastYear: modelRow.lastYear,
Series: modelRow.series,
Active: modelRow.active,
lastupdate: mysql.raw('NOW()'),
productcode: productCode,
};
}
async function queryExistingMnfcrmod(connection, typenums, pmanufacturers, pmodels, productCode) {
const queryOptions = {
sql: `SELECT * FROM mnfcrmod WHERE productcode = ? AND typenum IN (?) AND pmanufacturer IN (?) AND pmodel IN (?)`,
values: [productCode, typenums, pmanufacturers, pmodels]
};
return await query(connection, queryOptions);
}
/*
Looks like we can only update the first/last year and active columns
Leaving out mnfcrmod.name will find the nearest match of type and manufacturer but only the first of the .names gets added
If a name changes, it won't delete or change the existing row, only create a new one and there's no current way to fix that
*/
async function multiRowMnfcrModUpsertQuery(htpConnection, objectArray) {
// Get columns
const columns = Object.keys(objectArray[0]);
const values = objectArray.map(obj => Object.values(obj));
// Create query string addition for all of the columns to be updated
// const queryStringAdd = columns.map(row => `?? = VALUES(??)`)
const queryOptions = {
sql: 'INSERT INTO mnfcrmod (??) VALUES ? ON DUPLICATE KEY UPDATE firstyear = VALUES(firstyear), lastyear = VALUES(lastyear), `active` = VALUES(`active`)',
values: [columns, values],
};
return await write(htpConnection, queryOptions);
}
// Look for existing rows and add htpmnfcrmodid if exists
function filterMnfcrmodRows(mnfcrmodArr, existingRowsArr) {
const updateRows = [];
mnfcrmodArr.forEach(row => {
const matchingRow = existingRowsArr.find(element => element.typenum === row.typenum && element.sourcetypenum === row.typenum && element.pmanufacturer === row.pmanufacturer && row.pmodel === element.pmodel);
if (matchingRow) {
row.htpmnfcrmodid = matchingRow.htpmnfcrmodid;
updateRows.push(row);
}
else {
row.htpmnfcrmodid = undefined;
updateRows.push(row);
}
});
return updateRows;
}
/*
Unable to run a simple ON DUPLICATE KEY UPDATE on all rows as we get multiple copies
We'll have to check for existing mnfcrmod rows and add the primary key to the object then update
or insert if there's no match.
*/
async function upsertMnfcrmodRows(connection, mnfcrmodArr, productCode) {
// Convert to HTP rows
const convertedMnfcrmodRows = mnfcrmodArr.map(row => convertMnfcrmod(row, productCode));
// Create all the columns needed to build query to check for existing rows
const typenums = convertedMnfcrmodRows.map(row => row.typenum);
const pmanufacturers = convertedMnfcrmodRows.map(row => row.pmanufacturer);
const pmodels = convertedMnfcrmodRows.map(row => row.pmodel);
// Query for existing rows
const existingRowsResponse = await queryExistingMnfcrmod(connection, typenums, pmanufacturers, pmodels, productCode);
// Filter for matching rows and add key if exists
const updateRows = filterMnfcrmodRows(convertedMnfcrmodRows, existingRowsResponse);
// Run upsert query
const upsertResponse = await multiRowMnfcrModUpsertQuery(connection, updateRows);
return (`mnfcrmod affected rows: ${upsertResponse?.affectedRows}`);
}
export async function upsertMnfcrmodModelRows(connection, modelArr, productCode) {
const responses = [];
// Need to filter into mnfcrmod vs model rows
const nonVehicleUnitRows = modelArr.filter(row => row.vehicleUnit === 'False');
const vehicleUnitRows = modelArr.filter(row => row.vehicleUnit === 'True');
try {
if (nonVehicleUnitRows.length) {
// Convert
const mnfcrmodUpsertResponse = await upsertMnfcrmodRows(connection, nonVehicleUnitRows, productCode);
responses.push(mnfcrmodUpsertResponse);
}
if (vehicleUnitRows.length) {
// Convert rows to HTP
const convertedModelRows = vehicleUnitRows.map(row => convertModel(row, productCode));
const modelUpsertResponse = await multiRowUpsertQuery(connection, 'model', convertedModelRows);
responses.push(`model affected rows: ${modelUpsertResponse?.affectedRows}`);
}
return {
success: true,
request: '',
data: responses
};
}
catch (err) {
return {
success: false,
request: '',
data: err
};
}
}
async function deleteMnfcrmod(connection, typeNum, manufacturerName, modelName, firstYear, lastYear, productCode) {
const queryOptions = {
sql: `DELETE FROM mnfcrmod WHERE productcode = ? AND typenum = ? AND pmanufacturer = ? AND pmodel = ? AND firstyear = ? AND lastyear = ?`,
values: [productCode, typeNum, manufacturerName, modelName, firstYear, lastYear]
};
return await write(connection, queryOptions);
}
async function deleteModel(connection, manufacturerName, modelName, firstYear, lastYear, productCode) {
const queryOptions = {
sql: `DELETE FROM model WHERE productcode = ? AND Make = ? AND Model = ? AND FirstYear = ? AND LastYear = ?`,
values: [productCode, manufacturerName, modelName, firstYear, lastYear]
};
return await write(connection, queryOptions);
}
export async function deleteMnfcrmodModelRow(connection, typeNum, manufacturerName, modelName, firstYear, lastYear, vehicleUnit, productCode) {
const responses = [];
// Get manufacturer name
// Get vehicleunit true/false
try {
if (vehicleUnit === 'False') {
// delete from mnfcrmodel
const deleteResult = await deleteMnfcrmod(connection, typeNum, manufacturerName, modelName, firstYear, lastYear, productCode);
responses.push(`mnfcrmod delete affected rows: ${deleteResult?.affectedRows}`);
}
else if (vehicleUnit === 'True') {
// delete from Model
const deleteResult = await deleteModel(connection, manufacturerName, modelName, firstYear, lastYear, productCode);
responses.push(`Model delete affected rows: ${deleteResult?.affectedRows}`);
}
return {
success: true,
request: '',
data: responses
};
}
catch (err) {
return {
success: false,
request: '',
data: err
};
}
}
export async function insertMnfcrmodModelRows(connection, modelArr, productCode) {
const responses = [];
// Need to filter into mnfcrmod vs model rows
const nonVehicleUnitRows = modelArr.filter(row => row.vehicleUnit === 'False');
const vehicleUnitRows = modelArr.filter(row => row.vehicleUnit === 'True');
try {
if (nonVehicleUnitRows.length) {
// Convert rows to HTP mnfcrmod
const convertedNonVehicleRows = nonVehicleUnitRows.map(row => convertMnfcrmod(row, productCode));
const nonVehicleColumns = Object.keys(convertedNonVehicleRows[0]);
const nonVehicleValues = convertedNonVehicleRows.map(obj => Object.values(obj));
const mnfcrmodInsertResponse = await multiRowInsertQuery(connection, 'mnfcrmod', nonVehicleColumns, nonVehicleValues);
responses.push(`mnfcrmod affected rows: ${mnfcrmodInsertResponse?.affectedRows}`);
}
if (vehicleUnitRows.length) {
// Convert rows to HTP model
const convertedModelRows = vehicleUnitRows.map(row => convertModel(row, productCode));
const vehicleColumns = Object.keys(convertedModelRows[0]);
const vehicleValues = convertedModelRows.map(obj => Object.values(obj));
const modelInsertResponse = await multiRowInsertQuery(connection, 'model', vehicleColumns, vehicleValues);
responses.push(`model affected rows: ${modelInsertResponse?.affectedRows}`);
}
return {
success: true,
request: '',
data: responses
};
}
catch (err) {
return {
success: false,
request: '',
data: err
};
}
}
export declare function upsertPartListAuthorityMapRows(connection: any, inventoryTypeArr: any, productCode: any): Promise<{
success: boolean;
request: string;
data: any;
}>;
export declare function deletePartListAuthorityMapRow(connection: any, productCode: any, inventoryType: any, partListAuthorityId: any): Promise<string>;
import { multiRowUpsertQuery, write } from './shared.js';
function convertPartListAuthorityMap(inventoryTypeRow, productCode) {
return {
partlisttype: 'inventorytype',
productcode: productCode,
sourceid: inventoryTypeRow.inventoryTypeId,
partlistauthorityid: inventoryTypeRow.partListAuthorityId,
};
}
export async function upsertPartListAuthorityMapRows(connection, inventoryTypeArr, productCode) {
const partListRows = inventoryTypeArr.filter(row => row.partListAuthorityId);
if (partListRows.length) {
try {
const convertedRows = partListRows.map(row => convertPartListAuthorityMap(row, productCode));
const queryResponse = await multiRowUpsertQuery(connection, 'partlistauthoritymap', convertedRows);
return {
success: true,
request: '',
data: queryResponse.affectedRows || 'none'
};
}
catch (err) {
return {
success: false,
request: '',
data: err
};
}
}
else {
return {
success: true,
request: '',
data: 'none'
};
}
}
export async function deletePartListAuthorityMapRow(connection, productCode, inventoryType, partListAuthorityId) {
try {
const queryOptions = {
sql: `DELETE FROM partlistauthoritymap WHERE productcode = ? AND partlisttype = 'inventorytype' AND sourceid = ? AND partlistauthorityid = ?`,
values: [productCode, inventoryType, partListAuthorityId],
};
const deleteResponse = await write(connection, queryOptions);
//responses.push(`partlistauthoritymap affected rows: ${deleteResponse?.affectedRows}`)
return `partlistauthoritymap delete affected rows: ${deleteResponse?.affectedRows}`;
}
catch (err) {
return `error deleting partlistauthoritymap rows: ${err}`;
}
}
export declare function upsertPartUseRows(connection: any, inventoryTypeArr: any, productCode: any): Promise<{
success: boolean;
request: string;
data: any;
}>;
export declare function deletePartUseRow(connection: any, productCode: any, inventoryType: any, partListAuthorityId: any): Promise<{
success: boolean;
request: string;
data: any;
}>;
import mysql from 'mysql2/promise';
import pProps from 'p-props';
import { upsertPartListAuthorityMapRows, deletePartListAuthorityMapRow } from './partListAuthority.js';
import { multiRowUpsertQuery, write } from './shared.js';
function convertInventoryType(row, productCode) {
return {
typenum: row.inventoryTypeId,
part: row.name,
set: row.typeSetId,
used: row.active, //boolean
manmod: row.useManufacturer,
productcode: productCode,
lastupdate: mysql.raw('NOW()'),
// more fields are excluded as they're part of a unified tags object
};
}
// Multiple row upsert
export async function upsertPartUseRows(connection, inventoryTypeArr, productCode) {
const responses = [];
try {
// Convert to HTP insertable object
const convertedRows = inventoryTypeArr.map(row => convertInventoryType(row, productCode));
// Use pProps to run both database operations in parallel
const results = await pProps({
partUse: multiRowUpsertQuery(connection, 'partuse', convertedRows),
partListAuthorityMap: upsertPartListAuthorityMapRows(connection, inventoryTypeArr, productCode)
});
// Process results
responses.push(`partUse affected rows: ${results.partUse.affectedRows}`);
if (results.partListAuthorityMap.success) {
responses.push(`partlistauthoritymap affected rows: ${results.partListAuthorityMap.data}`);
}
else {
responses.push(`partlistauthoritymap error: ${results.partListAuthorityMap.data}`);
}
return ({
success: true,
request: '',
data: responses,
});
}
catch (err) {
return {
success: false,
request: '',
data: err,
};
}
}
export async function deletePartUseRow(connection, productCode, inventoryType, partListAuthorityId) {
const responses = [];
// Partuse delete query
const queryOptions = {
sql: `DELETE FROM partuse WHERE productcode = ? AND typenum = ?`,
values: [productCode, inventoryType]
};
try {
const partuseDeleteResult = await write(connection, queryOptions);
responses.push(`partuse delete affected rows: ${partuseDeleteResult?.affectedRows}`);
// Delete partlistauthoritymap row if exists
if (partListAuthorityId) {
const partListAuthorityDeleteResponse = await deletePartListAuthorityMapRow(connection, productCode, inventoryType, partListAuthorityId);
responses.push(partListAuthorityDeleteResponse);
}
return {
success: true,
request: '',
data: responses
};
}
catch (err) {
return {
success: false,
request: '',
data: err
};
}
}
export declare function upsertPaymentRows(connection: any, paymentArr: any, productCode: any): Promise<{
success: boolean;
data: any;
}>;
export declare function deletePaymentRow(connection: any, productCode: any, htpPaymentId: any): Promise<{
success: boolean;
data: any;
}>;
import { multiRowUpsertQuery, write } from './shared.js';
function convertPaymentRow(row, productCode) {
return {
productcode: productCode,
paymentid: row.paymentId,
storeid: row.storeId,
paymentmethodname: row.paymentMethodName,
documentnumber: row.documentNumber,
dateentered: row.dateEntered,
date: row.date,
void: row.void,
comments: row.comments
};
}
export async function upsertPaymentRows(connection, paymentArr, productCode) {
const responses = [];
try {
const convertedRows = paymentArr.map(row => convertPaymentRow(row, productCode));
const multiRowUpsertResponse = await multiRowUpsertQuery(connection, 'payment', convertedRows);
responses.push(`payment affected rows: ${multiRowUpsertResponse.affectedRows}`);
return {
success: true,
data: responses
};
}
catch (err) {
return {
success: false,
data: err
};
}
}
export async function deletePaymentRow(connection, productCode, htpPaymentId) {
const responses = [];
try {
const queryOptions = {
sql: `DELETE FROM payment WHERE productcode = ? AND htppaymentid = ?`,
values: [productCode, htpPaymentId],
};
const deleteResponse = await write(connection, queryOptions);
responses.push(`payment delete affected rows: ${deleteResponse?.affectedRows}`);
return {
success: true,
data: responses
};
}
catch (err) {
return {
success: false,
data: err
};
}
}
export declare function upsertPaymentLineRows(connection: any, paymentLineArr: any, productCode: any): Promise<{
success: boolean;
data: any;
}>;
export declare function deletePaymentLineRow(connection: any, productCode: any, htpPaymentLineId: any): Promise<{
success: boolean;
data: any;
}>;
import { multiRowUpsertQuery, write } from './shared.js';
function convertPaymentLineRow(row, productCode) {
return {
productcode: productCode,
paymentlineid: row.paymentLineId,
storeid: row.storeId,
paymentid: row.paymentId,
salesorderstoreid: row.salesOrderStoreId,
salesorderid: row.salesOrderId,
amount: row.amount,
};
}
export async function upsertPaymentLineRows(connection, paymentLineArr, productCode) {
const responses = [];
try {
const convertedRows = paymentLineArr.map(row => convertPaymentLineRow(row, productCode));
const multiRowUpsertResponse = await multiRowUpsertQuery(connection, 'paymentline', convertedRows);
responses.push(`paymentline affected rows: ${multiRowUpsertResponse.affectedRows}`);
return {
success: true,
data: responses
};
}
catch (err) {
return {
success: false,
data: err
};
}
}
export async function deletePaymentLineRow(connection, productCode, htpPaymentLineId) {
const responses = [];
try {
const queryOptions = {
sql: `DELETE FROM paymentline WHERE productcode = ? AND htppaymentlineid = ?`,
values: [productCode, htpPaymentLineId],
};
const deleteResponse = await write(connection, queryOptions);
responses.push(`paymentline delete affected rows: ${deleteResponse?.affectedRows}`);
return {
success: true,
data: responses
};
}
catch (err) {
return {
success: false,
data: err
};
}
}
export declare function upsertSalesOrderRows(connection: any, salesOrderArr: any, productCode: any): Promise<{
success: boolean;
data: any;
}>;
export declare function deleteSalesOrderRow(connection: any, productCode: any, htpSalesOrderId: any): Promise<{
success: boolean;
data: any;
}>;
import { multiRowUpsertQuery, write } from './shared.js';
function convertSalesOrderRow(row, productCode) {
return {
productcode: productCode,
storeid: row.storeId,
salesorderid: row.salesOrderId,
sequentialid: row.sequentialId,
parentsalesorderid: row.parentSalesOrderId,
date: row.date,
void: row.void,
salesorderdocumentname: row.salesOrderDocumentName,
closed: row.closed,
expirationdate: row.expirationDate,
dateentered: row.dateEntered,
dateclosed: row.dateClosed,
datemodified: row.dateModified,
termname: row.termName,
shippingmethodname: row.shippingMethodName,
shippingtrackingnumber: row.shippingTrackingNumber,
customerid: row.customerId,
tax: row.tax,
subtotal: row.subtotal,
internalcomments: row.internalComments,
externalcomments: row.externalComments,
billingcompany: row.billingCompany,
billingcontact: row.billingContact,
billingstreet: row.billingStreet,
billingcity: row.billingCity,
billingstate: row.billingState,
billingzip: row.billingZip,
billingcountry: row.billingCountry,
billingphone: row.billingPhone,
billingemail: row.billingEmail,
shipcompany: row.shipCompany,
shipcontact: row.shipContact,
shipstreet: row.shipStreet,
shipcity: row.shipCity,
shipstate: row.shipState,
shipzip: row.shipZip,
shipcountry: row.shipCountry,
shipphone: row.shipPhone,
shipemail: row.shipEmail,
shippingcustomeraddressid: row.shippingCustomerAddressId,
purchaseordernumber: row.purchaseOrderNumber,
trucknumber: row.truckNumber,
websaleid: row.webSaleId,
websaletype: row.webSaleType,
websaleorigin: row.webSaleOrigin,
vendorapprovalstatus: row.vendorApprovalStatus,
customerapprovalstatus: row.customerApprovalStatus,
};
}
export async function upsertSalesOrderRows(connection, salesOrderArr, productCode) {
const responses = [];
try {
const convertedRows = salesOrderArr.map(row => convertSalesOrderRow(row, productCode));
const multiRowUpsertResponse = await multiRowUpsertQuery(connection, 'salesorder', convertedRows);
responses.push(`salesorder affected rows: ${multiRowUpsertResponse.affectedRows}`);
return {
success: true,
data: responses
};
}
catch (err) {
return {
success: false,
data: err
};
}
}
export async function deleteSalesOrderRow(connection, productCode, htpSalesOrderId) {
const responses = [];
try {
const queryOptions = {
sql: `DELETE FROM salesorder WHERE productcode = ? AND htpsalesorderid = ?`,
values: [productCode, htpSalesOrderId],
};
const deleteResponse = await write(connection, queryOptions);
responses.push(`salesorder delete affected rows: ${deleteResponse?.affectedRows}`);
return {
success: true,
data: responses
};
}
catch (err) {
return {
success: false,
data: err
};
}
}
export declare function upsertSalesOrderAdjustmentRows(connection: any, salesOrderAdjustmentArr: any, productCode: any): Promise<{
success: boolean;
data: any;
}>;
export declare function deleteSalesOrderAdjustmentRow(connection: any, productCode: any, htpSalesOrderAdjustmentId: any): Promise<{
success: boolean;
data: any;
}>;
import { multiRowUpsertQuery, write } from './shared.js';
function convertSalesOrderAdjustmentRow(row, productCode) {
return {
productcode: productCode,
salesorderadjustmentid: row.salesOrderAdjustmentId,
salesorderid: row.salesOrderId,
salesorderlineid: row.salesOrderLineId,
salesorderstoreid: row.salesOrderStoreId,
amount: row.amount,
date: row.date,
adjustmenttypename: row.adjustmentTypeName,
description: row.description,
void: row.void,
taxable: row.taxable,
subtotaladjustment: row.subtotalAdjustment,
};
}
export async function upsertSalesOrderAdjustmentRows(connection, salesOrderAdjustmentArr, productCode) {
const responses = [];
try {
const convertedRows = salesOrderAdjustmentArr.map(row => convertSalesOrderAdjustmentRow(row, productCode));
const multiRowUpsertResponse = await multiRowUpsertQuery(connection, 'salesorderadjustment', convertedRows);
responses.push(`salesorderadjustment affected rows: ${multiRowUpsertResponse.affectedRows}`);
return {
success: true,
data: responses
};
}
catch (err) {
return {
success: false,
data: err
};
}
}
export async function deleteSalesOrderAdjustmentRow(connection, productCode, htpSalesOrderAdjustmentId) {
const responses = [];
try {
const queryOptions = {
sql: `DELETE FROM salesorderadjustment WHERE productcode = ? AND htpsalesorderadjustmentid = ?`,
values: [productCode, htpSalesOrderAdjustmentId],
};
const deleteResponse = await write(connection, queryOptions);
responses.push(`salesorderadjustment delete affected rows: ${deleteResponse?.affectedRows}`);
return {
success: true,
data: responses
};
}
catch (err) {
return {
success: false,
data: err
};
}
}
export declare function upsertSalesOrderLineRows(connection: any, salesOrderLineArr: any, productCode: any): Promise<{
success: boolean;
data: any;
}>;
export declare function deleteSalesOrderLineRow(connection: any, productCode: any, htpSalesOrderLineId: any): Promise<{
success: boolean;
data: any;
}>;
import { multiRowUpsertQuery, write } from './shared.js';
function convertSalesOrderLineRow(row, productCode) {
return {
productcode: productCode,
salesorderlineid: row.salesOrderLineId,
salesorderid: row.salesOrderId,
storeid: row.storeId,
rank: row.rank,
parentsalesorderlineid: row.parentSalesOrderLineId,
inventoryid: row.inventoryId,
inventorystoreid: row.inventoryStoreId,
inventorytypeid: row.inventoryTypeId,
type: row.type,
collectiontype: row.collectionType,
collectionquantity: row.collectionQuantity,
vin: row.vin,
make: row.make,
vehiclemodel: row.vehicleModel,
manufacturer: row.manufacturer,
model: row.model,
year: row.year,
bodystyle: row.bodyStyle,
side: row.side,
description: row.description,
interchangenumber: row.interchangeNumber,
subinterchangenumber: row.subInterchangeNumber,
vehicleid: row.vehicleId,
partnumber: row.partNumber,
vendorid: row.vendorId,
retailprice: row.retailPrice,
wholesaleprice: row.wholesalePrice,
jobberprice: row.jobberPrice,
distributorprice: row.distributorPrice,
customerprice: row.customerPrice,
cost: row.cost,
averagecost: row.averageCost,
coreclass: row.coreClass,
serialnumber: row.serialNumber,
core: row.core,
price: row.price,
quantity: row.quantity,
deliverystatus: row.deliveryStatus,
taxable: row.taxable,
creditissued: row.creditIssued,
creditstoreid: row.creditStoreId,
creditsalesorderid: row.creditSalesOrderId,
creditsalesorderlineid: row.creditSalesOrderLineId,
returnable: row.returnable,
daystoreturn: row.daysToReturn,
corerequired: row.coreRequired,
daystoreturncore: row.daysToReturnCore,
returncodename: row.returnCodeName,
deliverydate: row.deliveryDate,
dateentered: row.dateEntered,
websalelineid: row.webSaleLineId,
corestatus: row.coreStatus,
};
}
export async function upsertSalesOrderLineRows(connection, salesOrderLineArr, productCode) {
const responses = [];
try {
const convertedRows = salesOrderLineArr.map(row => convertSalesOrderLineRow(row, productCode));
const multiRowUpsertResponse = await multiRowUpsertQuery(connection, 'salesorderline', convertedRows);
responses.push(`salesorderline affected rows: ${multiRowUpsertResponse.affectedRows}`);
return {
success: true,
data: responses
};
}
catch (err) {
return {
success: false,
data: err
};
}
}
export async function deleteSalesOrderLineRow(connection, productCode, htpSalesOrderLineId) {
const responses = [];
try {
const queryOptions = {
sql: `DELETE FROM salesorderline WHERE productcode = ? AND htpsalesorderlineid = ?`,
values: [productCode, htpSalesOrderLineId],
};
const deleteResponse = await write(connection, queryOptions);
responses.push(`salesorderline delete affected rows: ${deleteResponse?.affectedRows}`);
return {
success: true,
data: responses
};
}
catch (err) {
return {
success: false,
data: err
};
}
}
export declare function upsertSellPriceClassRows(connection: any, sellPriceClassArr: any, productCode: any): Promise<{
success: boolean;
data: any;
}>;
export declare function deleteSellPriceClassRow(connection: any, productCode: any, sellPriceClassId: any): Promise<{
success: boolean;
data: any;
}>;
import { multiRowUpsertQuery, write } from './shared.js';
function convertSellPriceContract(row, productCode) {
return {
productcode: productCode,
sellpriceclassid: row.sellPriceClassId,
name: row.name,
parentsellpriceclassid: row.parentSellPriceClassId
};
}
export async function upsertSellPriceClassRows(connection, sellPriceClassArr, productCode) {
const responses = [];
try {
const convertedRows = sellPriceClassArr.map(row => convertSellPriceContract(row, productCode));
const multiRowUpsertResponse = await multiRowUpsertQuery(connection, 'sellpriceclass', convertedRows);
responses.push(`sellpriceclass affected rows: ${multiRowUpsertResponse.affectedRows}`);
return {
success: true,
data: responses
};
}
catch (err) {
return {
success: false,
data: err
};
}
}
// Do we need parentsellpriceclassid too?
export async function deleteSellPriceClassRow(connection, productCode, sellPriceClassId) {
const responses = [];
try {
const queryOptions = {
sql: `DELETE FROM sellpriceclass WHERE productcode = ? AND sellpriceclassid = ?`,
values: [productCode, sellPriceClassId],
};
const deleteResponse = await write(connection, queryOptions);
responses.push(`sellpriceclass delete affected rows: ${deleteResponse?.affectedRows}`);
return {
success: true,
data: responses
};
}
catch (err) {
return {
success: false,
data: err
};
}
}
import { Pool, Connection, QueryOptions, ResultSetHeader } from 'mysql2/promise';
/**
* Execute a query and return results
* Works similar to utility-db's query function
*
*/
export declare function query<T = unknown>(connection: Pool | Connection, queryOptions: QueryOptions | string): Promise<T[]>;
/**
* Execute a query and return the first result
* Works similar to utility-db's readFirst function
*
*/
export declare function queryFirst<T = unknown>(connection: Pool | Connection, queryOptions: QueryOptions | string): Promise<T | null>;
export declare function write(connection: Pool | Connection, queryOptions: QueryOptions | string): Promise<ResultSetHeader>;
export declare function upsertQuery(connection: Pool | Connection, table: string, row: object): Promise<ResultSetHeader>;
export declare function multiRowUpsertQuery(htpConnection: Pool | Connection, table: string, objectArray: object[]): Promise<ResultSetHeader>;
export declare function multiRowInsertQuery(connection: Pool | Connection, table: string, columns: string[], columnValues: unknown[][]): Promise<ResultSetHeader>;
/**
* Execute a query and return results
* Works similar to utility-db's query function
*
*/
export async function query(connection, queryOptions) {
// mysql2/promise returns [rows, fields]
// Both Pool and Connection have .query() method
// Pool.query() automatically manages connection get/release
const [rows] = await connection.query(queryOptions);
return rows;
}
/**
* Execute a query and return the first result
* Works similar to utility-db's readFirst function
*
*/
export async function queryFirst(connection, queryOptions) {
// mysql2/promise returns [rows, fields]
// Both Pool and Connection have .query() method
// Pool.query() automatically manages connection get/release
const [rows] = await connection.query(queryOptions);
return (rows[0]) ?? null;
}
export async function write(connection, queryOptions) {
// mysql2/promise returns [ResultSetHeader, fields] for write operations
// ResultSetHeader contains: affectedRows, insertId, warningStatus, etc.
// Both Pool and Connection have .query() method
// Pool.query() automatically manages connection get/release
const [result] = await connection.query(queryOptions);
return result;
}
export async function upsertQuery(connection, table, row) {
const queryOptions = {
sql: "INSERT INTO ?? SET ? ON DUPLICATE KEY UPDATE ?",
values: [table, row, row],
};
return await write(connection, queryOptions);
}
export async function multiRowUpsertQuery(htpConnection, table, objectArray) {
// Get columns
const columns = Object.keys(objectArray[0]);
const values = objectArray.map(obj => Object.values(obj));
// Duplicate the columns so it can be passed in query values
const doubleColumns = columns.flatMap(i => [i, i]);
// Create query string addition for all of the columns to be updated
const queryStringAdd = columns.map(() => `?? = VALUES(??)`);
const queryOptions = {
sql: `INSERT INTO ?? (??) VALUES ? ON DUPLICATE KEY UPDATE${queryStringAdd}`,
values: [table, columns, values, ...doubleColumns],
};
return await write(htpConnection, queryOptions);
}
export async function multiRowInsertQuery(connection, table, columns, columnValues) {
const queryOptions = {
sql: `INSERT IGNORE INTO ?? (??) VALUES ?`,
values: [table, columns, columnValues],
};
return await write(connection, queryOptions);
}
type LowercaseKeys<T> = {
[K in keyof T as Lowercase<string & K>]: T[K];
};
/**
* HTP types for sales order related rows
*/
export interface PaymentRow {
paymentId: number;
storeId: number;
paymentMethodName: string;
documentNumber: string;
dateEntered: string;
date: string;
void: 'False' | 'True';
comments: string | null;
}
export type HtpPaymentRow = LowercaseKeys<PaymentRow> & {
productcode: number;
};
export interface PaymentLineRow {
paymentLineId: number;
storeId: number;
paymentId: number;
salesOrderStoreId: number | null;
salesOrderId: number | null;
amount: number;
}
export type HtpPaymentLineRow = LowercaseKeys<PaymentLineRow> & {
productcode: number;
};
export interface SalesOrderRow {
storeId: number;
salesOrderId: number;
sequentialId: number | null;
parentSalesOrderId: number | null;
date: string;
void: 'False' | 'True';
salesOrderDocumentName: string;
closed: 'False' | 'True';
expirationDate: string | null;
dateEntered: string;
dateClosed: string | null;
dateModified: string;
termName: string | null;
shippingMethodName: string | null;
shippingTrackingNumber: string;
customerId: number | null;
tax: number;
subtotal: number;
internalComments: string | null;
externalComments: string | null;
billingCompany: string;
billingContact: string;
billingStreet: string;
billingCity: string;
billingState: string;
billingZip: string;
billingCountry: string;
billingPhone: string;
billingEmail: string | null;
shipCompany: string;
shipContact: string;
shipStreet: string;
shipCity: string;
shipState: string;
shipZip: string;
shipCountry: string;
shipPhone: string;
shipEmail: string | null;
shippingCustomerAddressId: number | null;
purchaseOrderNumber: string;
truckNumber: string;
webSaleId: number | null;
webSaleType: 'None' | 'HeavyTruckParts.net' | 'eBay';
webSaleOrigin: string | null;
vendorApprovalStatus: 'Pending' | 'Rejected' | 'Approved' | 'Expired';
customerApprovalStatus: 'Pending' | 'Rejected' | 'Approved' | 'Expired';
}
export type HtpSalesOrderRow = LowercaseKeys<SalesOrderRow> & {
productcode: number;
};
export interface SalesOrderAdjustmentRow {
salesOrderAdjustmentId: number;
salesOrderId: number;
salesOrderLineId: number | null;
salesOrderStoreId: number;
amount: number;
date: string;
adjustmentTypeName: string;
description: string | null;
void: 'False' | 'True';
taxable: 'False' | 'True';
subtotalAdjustment: 'False' | 'True';
}
export type HtpSalesOrderAdjustmentRow = LowercaseKeys<SalesOrderAdjustmentRow> & {
productcode: number;
};
export interface SalesOrderLineRow {
salesOrderLineId: number;
salesOrderId: number;
storeId: number;
rank: number;
parentSalesOrderLineId: number | null;
inventoryId: number | null;
inventoryStoreId: number | null;
inventoryTypeId: number | null;
type: 'Inherent Core' | 'Inventory' | 'Misc' | 'Dirty Core' | 'Job';
collectionType: 'None' | 'Kit' | 'Ordered Kit' | 'Template' | 'Breakdown';
collectionQuantity: number;
vin: string;
make: string;
vehicleModel: string;
manufacturer: string | null;
model: string | null;
year: number;
bodyStyle: string;
side: 'Left' | 'Right' | 'Both' | 'N/A' | null;
description: string | null;
interchangeNumber: string;
subInterchangeNumber: string;
vehicleId: number | null;
partNumber: string;
vendorId: number | null;
retailPrice: number;
wholesalePrice: number;
jobberPrice: number;
distributorPrice: number;
customerPrice: number;
cost: number;
averageCost: number;
coreClass: string;
serialNumber: string;
core: 'False' | 'True';
price: number;
quantity: number;
deliveryStatus: 'Open' | 'Rejected' | 'In Progress' | 'Delivered';
taxable: 'False' | 'True';
creditIssued: 'False' | 'True';
creditStoreId: number | null;
creditSalesOrderId: number | null;
creditSalesOrderLineId: number | null;
returnable: 'False' | 'True';
daysToReturn: number;
coreRequired: 'False' | 'True';
daysToReturnCore: number;
returnCodeName: string;
deliveryDate: string | null;
dateEntered: string;
webSaleLineId: number | null;
coreStatus: 'N/A' | 'Rejected' | 'Available' | 'Processed';
}
export type HtpSalesOrderLineRow = LowercaseKeys<SalesOrderLineRow> & {
productcode: number;
};
export {};
export {};
import { deleteCategory, upsertCategoryRows } from './category.js'
import { deleteCompanyInfo, upsertCompanyInfoRows } from './companyInfo.js'
import { deleteCustomerRow, upsertCustomerRows } from './customer.js'
import { deleteCustomerAddressRow, upsertCustomerAddressRows } from './customerAddress.js'
import { deleteCustomerOptionRow, upsertCustomerOptionRows } from './customerOption.js'
import { deleteCustomerOptionValueRow, upsertCustomerOptionValueRows } from './customerOptionValue.js'
import { deleteCustomerPriceContractRow, upsertCustomerPriceContractRows } from './customerPriceContract.js'
import { deleteInventoryData, insertInventoryData, upsertInventoryRows } from './inventory.js'
import {
insertInventoryFileData,
upsertInventoryFileData,
deleteInventoryFileData,
deleteInventoryFileRow
} from './inventoryFile/inventoryFile.js'
import {
insertInventoryOptionRows,
upsertInventoryOptionRows,
deleteInventoryOptionRows,
deleteInventoryOptionRow
} from './inventoryOption.js'
import {
deleteInventoryOptionListRow,
getPartUseInfoQuery,
insertInventoryOptionListRows,
upsertInventoryOptionListRows,
} from './inventoryOptionList.js'
import { deleteInventorySourceData, deleteInventorySourceRow, upsertInventorySourceRows } from './inventorySource.js'
import { deleteVehicle, upsertVehicleRows } from './invmaster.js'
import { insertMnfcrmodModelRows, upsertMnfcrmodModelRows, deleteMnfcrmodModelRow } from './mnfcrmod.js'
import { deletePartUseRow, upsertPartUseRows } from './partUse.js'
import { deleteSellPriceClassRow, upsertSellPriceClassRows } from './sellPriceClass.js'
import { upsertPaymentRows, deletePaymentRow } from './payment.js'
import { upsertPaymentLineRows, deletePaymentLineRow } from './paymentLine.js'
import { upsertSalesOrderRows, deleteSalesOrderRow } from './salesOrder.js'
import { upsertSalesOrderAdjustmentRows, deleteSalesOrderAdjustmentRow } from './salesOrderAdjustment.js'
import { upsertSalesOrderLineRows, deleteSalesOrderLineRow } from './salesOrderLineRow.js'
export {
deleteCategory,
deleteCompanyInfo,
deleteCustomerAddressRow,
deleteCustomerOptionRow,
deleteCustomerOptionValueRow,
deleteCustomerPriceContractRow,
deleteCustomerRow,
deleteInventoryData,
deleteInventoryFileData,
deleteInventoryFileRow,
deleteInventoryOptionListRow,
deleteInventoryOptionRow,
deleteInventoryOptionRows,
deleteInventorySourceData,
deleteInventorySourceRow,
deleteMnfcrmodModelRow,
deletePartUseRow,
deletePaymentRow,
deletePaymentLineRow,
deleteSalesOrderAdjustmentRow,
deleteSalesOrderLineRow,
deleteSalesOrderRow,
deleteSellPriceClassRow,
deleteVehicle,
getPartUseInfoQuery,
insertInventoryData,
insertInventoryFileData,
insertInventoryOptionListRows,
insertInventoryOptionRows,
insertMnfcrmodModelRows,
upsertCategoryRows,
upsertCompanyInfoRows,
upsertCustomerAddressRows,
upsertCustomerOptionRows,
upsertCustomerOptionValueRows,
upsertCustomerPriceContractRows,
upsertCustomerRows,
upsertInventoryFileData,
upsertInventoryOptionListRows,
upsertInventoryOptionRows,
upsertInventoryRows,
upsertInventorySourceRows,
upsertMnfcrmodModelRows,
upsertPartUseRows,
upsertPaymentRows,
upsertPaymentLineRows,
upsertSalesOrderAdjustmentRows,
upsertSalesOrderLineRows,
upsertSalesOrderRows,
upsertSellPriceClassRows,
upsertVehicleRows
}
export type {
HtpPaymentRow,
HtpPaymentLineRow,
HtpSalesOrderAdjustmentRow,
HtpSalesOrderLineRow,
HtpSalesOrderRow,
PaymentRow,
PaymentLineRow,
SalesOrderAdjustmentRow,
SalesOrderLineRow,
SalesOrderRow,
} from './types.js'
import _ from "lodash"
type InventorySettings = {
useSellableQuantity?: boolean
synchSource?: boolean
}
import mysql from 'mysql2/promise'
import pProps from 'p-props'
import { multiRowInsertQuery, multiRowUpsertQuery, query, write } from './shared.js'
import { insertInventoryOptionListRows, upsertInventoryOptionListRows } from './inventoryOptionList.js'
import { insertInventoryOptionRows, upsertInventoryOptionRows, deleteInventoryOptionRows } from './inventoryOption.js'
import { deleteInventoryFileData, insertInventoryFileData, upsertInventoryFileData } from './inventoryFile/inventoryFile.js'
import { upsertInventorySourceRows, deleteInventorySourceData } from './inventorySource.js'
// Get partuse data for all of the rows being handled
async function getPartUseRows(htpConnection, inventoryArr, productCode) {
// Create an array of all the inventorytype names, remove null and duplicates
const uniquePartUseRows = Array.from(new Set(inventoryArr.map(row => row.inventoryType?.id).filter(element => element !== null)))
if (!uniquePartUseRows.length) {
return
}
// Need to do this to escape correctly
const queryData = [uniquePartUseRows]
const queryOptions = {
sql: `SELECT partuse.typenum, partuse.part, partlistauthoritymap.partlistauthorityid
FROM partuse
LEFT JOIN partlistauthoritymap
ON partuse.productcode = partlistauthoritymap.productcode
AND partuse.typenum = partlistauthoritymap.sourceid
WHERE partuse.productcode = ? AND typenum IN ? `,
values: [productCode, queryData],
}
const response = await query<{ typenum: number, part: string, partlistauthorityid: number }>(htpConnection, queryOptions)
return response.map(row => (
{
typenum: row.typenum,
part: row.part,
partlistauthorityid: row.partlistauthorityid,
}
))
}
// Create a Map from partUseData for efficient lookups
function createPartUseMap(partUseData) {
if (!partUseData) {
return null
}
return new Map(partUseData.map(row => [row.typenum, row]))
}
// TODO: Need to return the error instead of logging separately
// TODO: Needs revisited but handles empty partUseData
function addPartUse(partUseMap, inventoryObjectRow) {
// If partUseMap is empty, log and return inventoryObjectRow as-is
if (!partUseMap) {
console.info("No part use data for inventory partnum: ", inventoryObjectRow.inventoryId)
return
}
// If there is partUseData but no match, log and return nothing as there is likely a data issue
//const partUseMatch = partUseData.find(element => inventoryObjectRow.inventoryType.id === element.typenum)
const inventoryTypeId = inventoryObjectRow.inventoryType.id
if (!inventoryTypeId) {
console.info("No inventorytypeid given for this inventory partnum: ", inventoryObjectRow.inventoryId)
return
}
const partUseMatch = partUseMap.get(inventoryTypeId)
if (!partUseMatch) {
console.info("No insert because no matching partuse entry", "inventory", "partnum", inventoryObjectRow.inventoryId)
return
}
// Otherwise return an updated inventory object row
const { typenum, partlistauthorityid } = partUseMatch
inventoryObjectRow.typeNum = typenum
inventoryObjectRow.partListAuthorityId = partlistauthorityid
return inventoryObjectRow
}
// Converts universal inventory object to an HTP row
// Settings can have modifiers that allow different value mappings without additional queries, currently only used for available vs sellable quantities
function convertInventory(inventoryObjectRow, productCode, settings: InventorySettings) {
const row = {
partnum: inventoryObjectRow.inventoryId,
store: inventoryObjectRow.storeId,
typenum: inventoryObjectRow.partListAuthorityId || inventoryObjectRow.typeNum,
sourcetypenum: inventoryObjectRow.typeNum,
stocknum: inventoryObjectRow.vehicle.id,
vinnum: inventoryObjectRow.vehicle.vin || '',
year: inventoryObjectRow.vehicle.year || inventoryObjectRow.year,
description: inventoryObjectRow.description || '',
interchangenumber: inventoryObjectRow.interchange.number,
subinterchangenumber: inventoryObjectRow.interchange.subNumber?.substring(0, 4) || '',
status: inventoryObjectRow.status,
suggestedprice: inventoryObjectRow.retailPrice || 0.00,
bottomprice: inventoryObjectRow.wholesalePrice,
replenish: inventoryObjectRow.replenish,
deplete: inventoryObjectRow.deplete,
quantity: settings.useSellableQuantity ? inventoryObjectRow.sellableQuantity : inventoryObjectRow.availableQuantity,
//dateentered: inventoryObjectRow.dateEntered !== '0000-00-00 00:00:00' ? row.dateentered = inventoryObjectRow.dateEntered : row.dateentered = null,
// datemodified: inventoryObjectRow.dateModified,
// datesold: inventoryObjectRow.dateSold,
label1: null,
label2: null,
label3: null,
label4: null,
data1: null,
data2: null,
data3: null,
data4: null,
cost: inventoryObjectRow.cost,
// Vehicle Make
make: inventoryObjectRow.vehicle?.make?.substring(0, 24) || '',
// Vehicle Model
model: inventoryObjectRow.vehicle?.model?.substring(0,49) || '',
pmanufacturer: inventoryObjectRow.parentManufacturer.name?.substring(0, 24), //matches trigger
pmodel: inventoryObjectRow.parentModel.name?.substring(0,49), //matches trigger
taxable: inventoryObjectRow.taxable,
oemnum: inventoryObjectRow.oemNumber,
condition: inventoryObjectRow.condition,
side: inventoryObjectRow.side,
// TODO: confirm this is desired or make schema change
tagnum: inventoryObjectRow.tagNumber?.substring(0, 19),
upc: inventoryObjectRow.upc || '',
category: inventoryObjectRow.category,
weight: inventoryObjectRow.shippingWeight?.value,
productcode: productCode,
lastupdate: mysql.raw('NOW()'),
worldviewable: inventoryObjectRow.public,
displayonline: inventoryObjectRow.displayOnline,
notes: inventoryObjectRow.notes || '',
shippinglength: inventoryObjectRow.shippingDimensions?.length,
shippingwidth: inventoryObjectRow.shippingDimensions?.width,
shippingheight: inventoryObjectRow.shippingDimensions?.height,
core: inventoryObjectRow.retailCore,
location: inventoryObjectRow.location,
//buynow: inventoryObjectRow.webSaleClass ? inventoryObjectRow.webSaleClass.replace(/s$/,'') : 'None',
inventoryvendorid: inventoryObjectRow.vendorId,
jobberprice: inventoryObjectRow.jobberPrice,
distributorprice: inventoryObjectRow.distributorPrice,
averagecost: inventoryObjectRow.averageCost,
sellpriceclassid: inventoryObjectRow.sellPriceClassId,
wholesalecore: inventoryObjectRow.wholeSaleCore ? inventoryObjectRow.wholeSaleCore : null,
jobbercore: inventoryObjectRow.jobberCore ? inventoryObjectRow.jobberCore : null,
distributorcore: inventoryObjectRow.distributorCore ? inventoryObjectRow.distributorCore : null,
// The following return ER_TRUNCATED_WRONG_VALUE if left as '0000-00-00 00:00:00'
dateentered: inventoryObjectRow.dateEntered !== '0000-00-00 00:00:00' ? inventoryObjectRow.dateEntered : undefined,
datemodified: inventoryObjectRow.dateModified !== '0000-00-00 00:00:00' ? inventoryObjectRow.dateModified : undefined,
datesold: inventoryObjectRow.dateSold !== '0000-00-00 00:00:00' ? inventoryObjectRow.dateSold : undefined,
// Buy now might optionally come from a function (LKQ at least)
buynow: inventoryObjectRow.buyNowSla ?? (inventoryObjectRow.webSaleClass?.replace(/s$/, '') ?? 'None'),
}
return row
}
/*
This is the primary function called by the queue and synch functions
The outer try/catch should return the same object as the other main functions
The nested try/catch is for transaction handling
*/
export async function upsertInventoryRows(htpConnection, inventoryArr, productCode, settings: InventorySettings = {}) {
const responses = []
try {
//
// Get partuse data for later steps
const partUseData = await getPartUseRows(htpConnection, inventoryArr, productCode)
// Create a Map from partUseData for efficient lookups (do this once)
const partUseMap = createPartUseMap(partUseData)
// Add partUseData to inventoryArr
const inventoryPartUseRows = inventoryArr.map(row => addPartUse(partUseMap, row))
// Remove undefines, these rows will be used for their tags
const filteredInventoryPartUseRows = inventoryPartUseRows.filter(row => row !== undefined)
// Convert changed product rows to HTP rows
const convertedRows = filteredInventoryPartUseRows.map(row => convertInventory(row, productCode, settings))
// Need to filter undefined rows due to missing partuse data
const filteredRows = convertedRows.filter(row => row !== undefined)
if (filteredRows.length) {
// Get a connection from the pool for transaction
const connection = await htpConnection.getConnection()
try {
// Start transaction
await connection.beginTransaction()
// Insert inventory rows first, return should be??
const multiRowUpsertResponse = await multiRowUpsertQuery(connection, 'inventory', filteredRows)
// call inventory option list stuff
const multiRowInventoryOptionListUpsertResponse = await upsertInventoryOptionListRows(connection, filteredInventoryPartUseRows, productCode)
const multiRowInventoryOptionUpsertResponse = await upsertInventoryOptionRows(connection, filteredInventoryPartUseRows, productCode)
// call inventory file and image stuff
const multiRowInventoryFileUpsertResponse = await upsertInventoryFileData(connection, filteredInventoryPartUseRows, productCode)
// call inventory source stuff
let multiRowInventorySourceUpsertResponse
if (settings.synchSource) {
multiRowInventorySourceUpsertResponse = await upsertInventorySourceRows(connection, filteredInventoryPartUseRows, productCode)
}
// Commit transaction
await connection.commit()
responses.push(`affected rows: inventory ${multiRowUpsertResponse?.affectedRows}, ${multiRowInventoryOptionListUpsertResponse}, ${multiRowInventoryOptionUpsertResponse}, ${[...multiRowInventoryFileUpsertResponse]}, ${multiRowInventorySourceUpsertResponse ? multiRowInventorySourceUpsertResponse : 'inventorysource affected rows: none' }`)
} catch (err) {
// Rollback transaction on error
await connection.rollback()
throw err
} finally {
// Always release connection back to pool
connection.release()
}
} else {
responses.push(`affected rows: inventory none`)
}
return {
success: true,
request: '',
data: responses
}
} catch (err) {
return {
success: false,
request: '',
data: err
}
}
}
async function deleteInventoryQuery(htpConnection, productCode, partNum, storeId ) {
const queryOptions = {
sql: "DELETE FROM inventory WHERE partnum = ? AND store = ? AND productcode = ?",
values: [partNum, storeId, productCode],
}
return await write(htpConnection, queryOptions)
}
export async function deleteInventoryData(htpConnection, productCode, partNum, storeId) {
const responses = []
try {
const deleteResponses = await pProps({
inventoryDeleteResponse: deleteInventoryQuery(htpConnection, productCode, partNum, storeId),
inventoryOptionDeleteResponse: deleteInventoryOptionRows(htpConnection, productCode, storeId, partNum),
inventoryFileDeleteResponse: deleteInventoryFileData(htpConnection, productCode, partNum),
inventorySourceDeleteResponse: deleteInventorySourceData(htpConnection, productCode, partNum)
})
const fileDeleteResponse = deleteResponses.inventoryFileDeleteResponse.data
responses.push(`inventory delete affected rows: ${deleteResponses.inventoryDeleteResponse?.affectedRows}, ${deleteResponses.inventoryOptionDeleteResponse}, ${fileDeleteResponse}, ${deleteResponses.inventorySourceDeleteResponse}`)
return {
success: true,
request: '',
data: responses
}
} catch (err) {
return {
success: false,
request: '',
data: err
}
}
}
// Multi-row insert query
async function insertInventoryRows(htpConnection, rows) {
const columns = Object.keys(rows[0])
const values = rows.map(obj => Object.values(obj))
const result = await multiRowInsertQuery(htpConnection, "inventory", columns, values)
return result
}
// Multi-row insert function for repush
// TODO: get partUseData outside of this function
export async function insertInventoryData(htpConnection, inventoryArr, productCode, settings: InventorySettings = {}) {
const responses = []
try {
//
// Get partuse data for later steps
const partUseData = await getPartUseRows(htpConnection, inventoryArr, productCode)
// Create a Map from partUseData for efficient lookups (do this once)
const partUseMap = createPartUseMap(partUseData)
// Add partUseData to inventoryArr
const inventoryPartUseRows = inventoryArr.map(row => addPartUse(partUseMap, row))
// Remove undefines, these rows will be used for their tags
const filteredInventoryPartUseRows = inventoryPartUseRows.filter(row => row !== undefined)
// Convert changed product rows to HTP rows
const convertedRows = filteredInventoryPartUseRows.map(row => convertInventory(row, productCode, settings))
// Need to filter undefined rows due to missing partuse data
const filteredRows = convertedRows.filter(row => row !== undefined)
if (filteredRows.length) {
// Get a connection from the pool for transaction
const connection = await htpConnection.getConnection()
try {
// Start transaction
await connection.beginTransaction()
// Insert inventory rows first
const initialResponse = await pProps({
inventoryInsertResponse: insertInventoryRows(connection, filteredRows),
inventoryFileResponse: insertInventoryFileData(connection, filteredInventoryPartUseRows, productCode),
inventoryOptionListResponse: insertInventoryOptionListRows(connection, filteredInventoryPartUseRows, productCode),
inventorySourceUpsertResponse: settings.synchSource ? upsertInventorySourceRows(connection, filteredInventoryPartUseRows, productCode) : Promise.resolve(null)
})
// This one has to be run after inventoryoptionlist since it needs to query for those rows
const inventoryOptionResponse = await insertInventoryOptionRows(connection, filteredInventoryPartUseRows, productCode)
// Commit transaction
await connection.commit()
responses.push(`affected rows: inventory ${initialResponse.inventoryInsertResponse?.affectedRows}, ${initialResponse.inventoryOptionListResponse}, ${inventoryOptionResponse}, ${initialResponse.inventoryFileResponse ? [...initialResponse.inventoryFileResponse] : ''}, ${initialResponse.inventorySourceUpsertResponse ? initialResponse.inventorySourceUpsertResponse : 'inventorysource affected rows: none' }`)
// responses.push(`affected rows: inventory ${inventoryInsertResponse.affectedRows}, ${inventoryOptionListResponse.affectedRows}, ${inventoryOptionResponse?.affectedRows}, ${inventoryFileResponse?.affectedRows}`)
} catch (err) {
// Rollback transaction on error
await connection.rollback()
throw err
} finally {
// Always release connection back to pool
connection.release()
}
}
return {
success: true,
request: '',
data: responses
}
} catch (err) {
//
return {
success: false,
request: '',
data: err
}
}
}
// ==== Conversion and Helper Functions ====
// Not used but keeping here for reference, this is what comes in on the inventory row from pro/ee
type InventoryAttachment = {
id: number
public: 'False' | 'True'
rank: number
file: number
type: string
video?: {
id: string | number
url: string
}
}
type FluffedAttachmentBase = {
id: number
public: 'False' | 'True'
rank: number
file: number
inventoryid: number
productcode: number
storeid: number
}
type ImageAttachment = FluffedAttachmentBase & { type: 'Image' }
type FileAttachment = FluffedAttachmentBase & { type: 'PDF' | 'Text' }
type VideoAttachment = FluffedAttachmentBase & {
type: 'YOUTUBE'
video: { id: number; url: string }
}
export type FluffedAttachmentRow = ImageAttachment | FileAttachment | VideoAttachment
// Only adds inventoryid and productcode to attachment rows
export function fluffAttachmentRows(productInventoryRow, productCode): (FluffedAttachmentRow | undefined)[] {
const inventoryId = productInventoryRow.inventoryId
const storeId = productInventoryRow.storeId
const attachmentRows = productInventoryRow.attachments.map(function (attachment) {
if (attachment.id) {
return { ...attachment,
inventoryid: inventoryId,
productcode: productCode,
storeid: storeId
}
}
})
return attachmentRows
}
export function convertManyInventoryImageRows(row) {
const imageLocation = `&productcode=${row.productcode}&id=${row.file}`
return {
productcode: row.productcode,
inventoryid: row.inventoryid,
fileid: row.file,
imagelocation: imageLocation,
rank: row.rank,
}
}
export function convertManyInventoryFileRows(row) {
return {
productcode: row.productcode,
associatedid: row.inventoryid,
associatedidtype: 'inventoryid',
fileid: row.file,
filetype: row.type
}
}
export function convertManyPartVideoListRows(row) {
return {
productcode: row.productcode,
store: row.storeid,
partnum: row.inventoryid,
videotype: row.type,
videoid: row.video.id,
url: row.video.url,
dirty: "False"
}
}
export function filterExistingFileListRows(newRows, existingRows) {
const uniqueRows = newRows.filter(function(newRow) {
return !existingRows.find(function(existingRow) {
return existingRow.associatedid === newRow.associatedid && existingRow.fileid === newRow.fileid && newRow.associatedidtype === existingRow.associatedidtype
})
})
return uniqueRows
}
// Compares incoming attachment rows (source of truth) against what already exists in the DB.
// Returns two lists in one pass:
// toInsert — rows in incoming that are missing from the DB (need to be written)
// toDelete — rows in the DB that are missing from incoming (no longer wanted, need to be removed)
//
// Identity is determined by a composite key string built by the caller, e.g. "1|100|inventoryid|10".
// We build a Set from each side first so every membership check is O(1) instead of scanning the array.
function partitionAttachmentRows<TIncoming, TExisting>(
incomingRows: TIncoming[],
existingRows: TExisting[],
buildIncomingKey: (row: TIncoming) => string,
buildExistingKey: (row: TExisting) => string
): { toInsert: TIncoming[]; toDelete: TExisting[] } {
// Index each side by its composite key for fast lookups below
const incomingKeySet = new Set(incomingRows.map(buildIncomingKey))
const existingKeySet = new Set(existingRows.map(buildExistingKey))
// An incoming row is new if its key is not found in the DB
const toInsert = incomingRows.filter(row => !existingKeySet.has(buildIncomingKey(row)))
// An existing DB row is stale if its key is not found in incoming
const toDelete = existingRows.filter(row => !incomingKeySet.has(buildExistingKey(row)))
return { toInsert, toDelete }
}
type ConvertedImageListRow = {
productcode: number
inventoryid: number
fileid: number
imagelocation: string
rank: number
}
// Wrapper for partimagelist rows.
// The composite key is (productcode, inventoryid, fileid)
// so fileid is sufficient to identify a unique image on a part.
export function partitionImageListRows<TExisting extends { productcode: number, inventoryid: number, fileid: number | null }>(
incomingRows: ConvertedImageListRow[],
existingRows: TExisting[]
): { toInsert: ConvertedImageListRow[]; toDelete: TExisting[] } {
return partitionAttachmentRows(
incomingRows,
existingRows,
row => `${row.productcode}|${row.inventoryid}|${row.fileid}`,
row => `${row.productcode}|${row.inventoryid}|${row.fileid}`
)
}
type ConvertedVideoListRow = {
productcode: number
store: number
partnum: number // inventoryid
videotype: string
videoid: string // varchar(50) in DB
url: string
dirty: 'False' | 'True'
}
// Wrapper for partvideolist rows.
// The composite key is (productcode, store, partnum, videoid)
// the same part can have different videos per store
// so a video for store 2 must not be treated as stale just because store 1 doesn't have it.
export function partitionVideoListRows<TExisting extends { productcode: number, store: number, partnum: number, videoid: string }>(
incomingRows: ConvertedVideoListRow[],
existingRows: TExisting[]
): { toInsert: ConvertedVideoListRow[]; toDelete: TExisting[] } {
return partitionAttachmentRows(
incomingRows,
existingRows,
row => `${row.productcode}|${row.store}|${row.partnum}|${row.videoid}`,
row => `${row.productcode}|${row.store}|${row.partnum}|${row.videoid}`
)
}
type ConvertedFileListRow = {
productcode: number
associatedid: number
associatedidtype: string
fileid: number
filetype: string
}
// Wrapper for partfilelist rows.
// The composite key is (productcode, associatedid, associatedidtype, fileid)
// associatedid alone is not enough: the same fileid can exist under different inventory rows.
export function partitionFileListRows<TExisting extends { productcode: number | string, associatedid: number, associatedidtype: string, fileid: number }>(
incomingRows: ConvertedFileListRow[],
existingRows: TExisting[]
): { toInsert: ConvertedFileListRow[]; toDelete: TExisting[] } {
return partitionAttachmentRows(
incomingRows,
existingRows,
row => `${row.productcode}|${row.associatedid}|${row.associatedidtype}|${row.fileid}`,
row => `${row.productcode}|${row.associatedid}|${row.associatedidtype}|${row.fileid}`
)
}
import pProps from 'p-props'
import _ from 'lodash'
import { fluffAttachmentRows } from './helpers.js'
import {
deletePartFileListData,
deletePartFileListRow,
deletePartImageListData,
deletePartImageListRow,
insertPartFileListRows,
insertPartImageListRows,
insertPartVideoList,
upsertPartFileList,
upsertPartImageList,
upsertPartVideoList
} from './queries.js'
// Main exported functions for inventoryFile/Attachments
// Main function for partimagelist and partfilelist upserts
export async function upsertInventoryFileData(htpConnection, productInventoryRows, productCode) {
const responses = []
// Filter just the inventory rows with attachments
const inventoryRowsWithAttachments = productInventoryRows.filter(row => row.attachments?.length > 0)
// Add inventoryid and storeid to all of the attachment rows for later
const modifiedAttachmentRows = inventoryRowsWithAttachments.map(row => fluffAttachmentRows(row, productCode)).flat().filter(Boolean)
// Separate image and file rows since they're handled differently
const imageAttachmentRows = modifiedAttachmentRows.filter(row => row.type === 'Image')
const fileAttachmentRows = modifiedAttachmentRows.filter(row => row.type === 'PDF' || row.type === 'Text')
const videoAttachmentRows = modifiedAttachmentRows.filter(row => row.type === 'YOUTUBE')
// Call the individual upsert functions in parallel using pProps
// Prepare the filtered rows and create promises object
const filteredImageAttachmentRows = imageAttachmentRows.length ?
_.uniqWith(imageAttachmentRows, _.isEqual) : []
const filteredFileAttachmentRows = fileAttachmentRows.length ?
_.uniqWith(fileAttachmentRows, _.isEqual) : []
const filteredYoutubeAttachmentRows = videoAttachmentRows.length ?
_.uniqWith(videoAttachmentRows, _.isEqual) : []
// Create an object with promises to run in parallel
const promisesObj: Record<string, Promise<any>> = {}
if (filteredImageAttachmentRows.length) {
promisesObj.imageResponse = upsertPartImageList(htpConnection, filteredImageAttachmentRows, productCode)
}
if (filteredFileAttachmentRows.length) {
promisesObj.fileResponse = upsertPartFileList(htpConnection, filteredFileAttachmentRows, productCode)
}
if (filteredYoutubeAttachmentRows.length) {
promisesObj.videoResponse = upsertPartVideoList(htpConnection, filteredYoutubeAttachmentRows, productCode)
}
// Run promises in parallel if any exist
if (Object.keys(promisesObj).length > 0) {
const results = await pProps(promisesObj)
// Add results to responses
if (results.imageResponse) {
responses.push(results.imageResponse)
}
if (results.fileResponse) {
responses.push(results.fileResponse)
}
if (results.videoResponse) {
responses.push(results.videoResponse)
}
}
return responses
}
// Called when an inventoryfile row is deleted
export async function deleteInventoryFileRow(htpConnection, productCode, inventoryId, fileId, type) {
const responses = []
try {
// Run delete operations in parallel using pProps
const deleteResults = await pProps({
imageListResult: deletePartImageListRow(htpConnection, productCode, inventoryId, fileId),
fileListResult: deletePartFileListRow(htpConnection, productCode, inventoryId, fileId, type)
})
responses.push(`${deleteResults.imageListResult}, ${deleteResults.fileListResult}`)
return {
success: true,
request: '',
data: responses
}
} catch (err) {
return {
success: false,
request: '',
data: err
}
}
}
// Called as part of an inventory row delete, fileid isn't known from changeDoc
export async function deleteInventoryFileData(htpConnection, productCode, inventoryId) {
// Two delete types: when an inventory row goes, and when inventoryfile goes (though they're likely related)
// Can't join a deleted row to file for the type so we'll have to do it with inventoryid, fileid, and productcode
const responses = []
try {
// Run delete operations in parallel using pProps
const deleteResults = await pProps({
imageListResult: deletePartImageListData(htpConnection, productCode, inventoryId),
fileListResult: deletePartFileListData(htpConnection, productCode, inventoryId)
})
responses.push(`${deleteResults.imageListResult}, ${deleteResults.fileListResult}`)
return {
success: true,
request: '',
data: responses
}
} catch (err) {
return {
success: false,
request: '',
data: err
}
}
}
// Bulk insert function for partimagelist and partfilelist
export async function insertInventoryFileData(htpConnection, productInventoryRows, productCode) {
const responses = []
// Filter just the inventory rows with attachments
const attachmentRows = productInventoryRows.filter(row => row.attachments?.length > 0)
// Add inventoryid to all of the attachment rows for later
const modifiedAttachmentRows = attachmentRows.map(row => fluffAttachmentRows(row, productCode)).flat()
// Separate image and file rows since they're handled differently
const imageAttachmentRows = modifiedAttachmentRows.filter(row => row.type === 'Image')
const fileAttachmentRows = modifiedAttachmentRows.filter(row => row.type === 'PDF' || row.type === 'Text')
const videoAttachmentRows = modifiedAttachmentRows.filter(row => row.type === 'YOUTUBE')
// Call the individual insert functions in parallel using pProps
// Prepare the filtered rows and create promises object
const filteredImageAttachmentRows = imageAttachmentRows.length ?
_.uniqWith(imageAttachmentRows, _.isEqual) : []
const filteredFileAttachmentRows = fileAttachmentRows.length ?
_.uniqWith(fileAttachmentRows, _.isEqual) : []
const filteredYoutubeAttachmentRows = videoAttachmentRows.length ?
_.uniqWith(videoAttachmentRows, _.isEqual) : []
// Create an object with promises to run in parallel
const promisesObj: Record<string, Promise<any>> = {}
if (filteredImageAttachmentRows.length) {
promisesObj.imageResponse = insertPartImageListRows(htpConnection, filteredImageAttachmentRows, productCode)
}
if (filteredFileAttachmentRows.length) {
promisesObj.fileResponse = insertPartFileListRows(htpConnection, filteredFileAttachmentRows, productCode)
}
if (filteredYoutubeAttachmentRows.length) {
promisesObj.videoResponse = insertPartVideoList(htpConnection, filteredYoutubeAttachmentRows, productCode)
}
// Run promises in parallel if any exist
if (Object.keys(promisesObj).length > 0) {
const results = await pProps(promisesObj)
// Add results to responses
if (results.imageResponse) {
responses.push(results.imageResponse)
}
if (results.fileResponse) {
responses.push(results.fileResponse)
}
if (results.videoResponse) {
responses.push(results.videoResponse)
}
}
return responses
}
import { query, multiRowInsertQuery, write } from '../shared.js'
import { convertManyInventoryFileRows, convertManyInventoryImageRows, convertManyPartVideoListRows, partitionFileListRows, partitionImageListRows, partitionVideoListRows } from './helpers.js'
// ==== Delete Query Functions ====
// Called when an inventory row is deleted
export async function deletePartImageListData(connection, productCode, partNum) {
try {
const queryOptions = {
sql: `DELETE FROM partimagelist WHERE productcode = ? AND inventoryid = ?`,
values: [productCode, partNum]
}
const deleteResponse = await write(connection, queryOptions)
return `partimagelist delete affected rows: ${deleteResponse?.affectedRows}`
} catch (err) {
return `error deleting partimagelist rows: ${err}`
}
}
// Called when an inventory row is deleted
export async function deletePartFileListData(connection, productCode, partNum) {
try {
const queryOptions = {
sql: `DELETE FROM partfilelist WHERE productcode = ? AND associatedidtype = 'inventoryid' AND associatedid = ?`,
values: [productCode, partNum]
}
const deleteResponse = await write(connection, queryOptions)
return `partfilelist delete affected rows: ${deleteResponse?.affectedRows}`
} catch (err) {
return `error deleting partfilelist rows: ${err}`
}
}
// Called in upsertPartImageList to cover non-public rows
export async function deletePartImageListRows(htpConnection, deleteRows){
//const deleteRows = nonPublicRows.map(row => ([productCode, row.inventoryid, row.id]))
try {
const queryOptions = {
sql: `DELETE FROM partimagelist WHERE (productcode, inventoryid, fileid) in (?)`,
values: [deleteRows]
}
const deleteResponse = await write(htpConnection, queryOptions)
return `partimagelist delete affected rows: ${deleteResponse?.affectedRows}`
} catch (err) {
return `error deleting partimagelist rows: ${err}`
}
// const deleteResponse = await deletePartImageListRows(htpConnection, deleteRows)
}
// Called in upsertPartImageList to cover possible non-public rows
export async function deletePartFileListRows(htpConnection, deleteRows){
//const deleteRows = nonPublicRows.map(row => ([productCode, 'inventoryid', row.inventoryid, row.id]))
try {
const queryOptions = {
sql: `DELETE FROM partfilelist WHERE (productcode, associatedid, associatedidtype, fileid) in (?)`,
values: [deleteRows]
}
const deleteResponse = await write(htpConnection, queryOptions)
return `non-public partfilelist delete affected rows: ${deleteResponse?.affectedRows}`
} catch (err) {
return `error deleting partfilelist rows: ${err}`
}
}
export async function deletePartVideoListRows(htpConnection, deleteRows){
//const deleteRows = nonPublicRows.map(row => ([productCode, 'inventoryid', row.inventoryid, row.id]))
try {
const queryOptions = {
sql: `DELETE FROM partvideolist WHERE (productcode, store, partnum, videoid) in (?)`,
values: [deleteRows]
}
const deleteResponse = await write(htpConnection, queryOptions)
return `non-public partvideolist delete affected rows: ${deleteResponse?.affectedRows}`
} catch (err) {
return `error deleting partvideolist rows: ${err}`
}
}
// Called in deleteInventoryFileData when an inventoryfile row is deleted
export async function deletePartImageListRow(connection, productCode, partNum, fileId) {
try {
const queryOptions = {
sql: `DELETE FROM partimagelist WHERE productcode = ? AND inventoryid = ? AND fileid = ?`,
values: [productCode, partNum, fileId]
}
const deleteResponse = await write(connection, queryOptions)
return `partimagelist delete affected rows: ${deleteResponse?.affectedRows}`
} catch (err) {
return `error deleting partimagelist rows: ${err}`
}
}
// Called by deleteInventoryFileRow when an inventoryfile row is deleted
export async function deletePartFileListRow(connection, productCode, partNum, fileId, type) {
try {
const queryOptions = {
sql: `DELETE FROM partfilelist WHERE productcode = ? AND associatedid = ? AND associatedidtype = ? AND fileid = ?`,
values: [productCode, partNum, type, fileId]
}
const deleteResponse = await write(connection, queryOptions)
return `partfilelist delete affected rows: ${deleteResponse?.affectedRows}`
} catch (err) {
return `error deleting partfilelist rows: ${err}`
}
}
// ==== Insert Query Functions ====
export async function insertPartVideoList(htpConnection, inventoryVideoRows, productCode) {
const responses = []
const publicRows = inventoryVideoRows.filter(row => row.public === 'True')
if (publicRows.length) {
const convertedRows = publicRows.map(row => convertManyPartVideoListRows(row))
const columns = Object.keys(convertedRows[0])
const values = convertedRows.map(obj => Object.values(obj))
const response = await multiRowInsertQuery(htpConnection, 'partvideolist', columns, values)
responses.push(`partvideolist affected rows: ${response?.affectedRows}`)
} else {
responses.push(`partvideolist affected rows: none`)
}
return responses
}
export async function insertPartImageListRows(htpConnection, inventoryImageRows, productCode) {
const responses = []
// Filter for public rows and upsert
const publicRows = inventoryImageRows.filter(row => row.public === 'True')
if (publicRows.length) {
// Convert rows
const convertedRows = publicRows.map(row => convertManyInventoryImageRows(row))
//const upsertResponse = await partImageListInsertQuery(htpConnection, convertedRows)
const columns = Object.keys(convertedRows[0])
const values = convertedRows.map(obj => Object.values(obj))
const response = await multiRowInsertQuery(htpConnection, 'partimagelist', columns, values)
responses.push(`partimagelist affected rows: ${response?.affectedRows}`)
} else {
responses.push(`partimagelist affected rows: none`)
}
return responses
}
export async function insertPartFileListRows(htpConnection, inventoryFileRows, productCode) {
const responses = []
const publicRows = inventoryFileRows.filter(row => row.public === 'True')
if (publicRows.length) {
// Convert rows
const convertedRows = publicRows.map(row => convertManyInventoryFileRows(row))
// const response = await partFileListInsertQuery(htpConnection, convertedRows)
const columns = Object.keys(convertedRows[0])
const values = convertedRows.map(obj => Object.values(obj))
const response = await multiRowInsertQuery(htpConnection, 'partfilelist', columns, values)
responses.push(`partfilelist affected rows: ${response?.affectedRows}`)
} else {
responses.push(`partfilelist affected rows: none`)
}
return responses
}
// ==== Upsert Query Functions ====
export async function partImageListMultiRowUpsertQuery(htpConnection, objectArray) {
// Get columns
const columns = Object.keys(objectArray[0])
const values = objectArray.map(obj => Object.values(obj))
const queryOptions = {
sql: 'INSERT INTO partimagelist (??) VALUES ? ON DUPLICATE KEY UPDATE `rank` = values(`rank`)',
values: [columns, values]
}
return await write(htpConnection, queryOptions)
}
export async function partVideoListMultiRowUpsertQuery(htpConnection, objectArray) {
// Get columns
const columns = Object.keys(objectArray[0])
const values = objectArray.map(obj => Object.values(obj))
const queryOptions = {
sql: 'INSERT INTO partvideolist (??) VALUES ? ON DUPLICATE KEY UPDATE `url` = values(`url`)',
values: [columns, values]
}
return await write(htpConnection, queryOptions)
}
export async function partFileListMultiRowUpsertQuery(htpConnection, objectArray) {
// Get columns
const columns = Object.keys(objectArray[0])
const values = objectArray.map(obj => Object.values(obj))
const queryOptions = {
sql: `INSERT IGNORE INTO partfilelist (??) VALUES ?`,
values: [columns, values]
}
return await write(htpConnection, queryOptions)
}
export async function upsertPartImageList(htpConnection, inventoryImageRows, productCode) {
const responses = []
// Get inventoryids and query existing rows up front — needed for both insert and stale-delete
const inventoryIds = inventoryImageRows.map(row => row.inventoryid)
const existingRows = await queryExistingAttachmentRows<ExistingImageListRow>(htpConnection, 'partimagelist', 'inventoryid', inventoryIds, productCode)
// Convert all public incoming rows
const publicRows = inventoryImageRows.filter(row => row.public === 'True')
const convertedRows = publicRows.map(row => convertManyInventoryImageRows(row))
// Single pass: new rows to insert, stale rows to delete
const { toInsert, toDelete } = partitionImageListRows(convertedRows, existingRows)
if (toDelete.length) {
const deleteRows = toDelete.map(row => ([row.productcode, row.inventoryid, row.fileid]))
const deleteResponse = await deletePartImageListRows(htpConnection, deleteRows)
responses.push(deleteResponse)
}
// Also delete any non-public rows that exist in the DB
const nonPublicRows = inventoryImageRows.filter(row => row.public === 'False')
if (nonPublicRows.length) {
const deleteRows = nonPublicRows.map(row => ([productCode, row.inventoryid, row.file]))
const deleteResponse = await deletePartImageListRows(htpConnection, deleteRows)
responses.push(deleteResponse)
}
if (toInsert.length) {
const upsertResponse = await partImageListMultiRowUpsertQuery(htpConnection, toInsert)
responses.push(`partimagelist affected rows: ${upsertResponse?.affectedRows}`)
} else {
responses.push(`partimagelist affected rows: none`)
}
return responses
}
export async function upsertPartVideoList(htpConnection, inventoryVideoRows, productCode) {
const responses = []
// Get inventoryids and query existing rows up front — needed for both insert and stale-delete
const inventoryIds = inventoryVideoRows.map(row => row.inventoryid)
const existingRows = await queryExistingAttachmentRows<ExistingVideoListRow>(htpConnection, 'partvideolist', 'partnum', inventoryIds, productCode)
// Convert all public incoming rows
const publicRows = inventoryVideoRows.filter(row => row.public === 'True')
const convertedRows = publicRows.map(row => convertManyPartVideoListRows(row))
// Single pass: new rows to insert, stale rows to delete
const { toInsert, toDelete } = partitionVideoListRows(convertedRows, existingRows)
if (toDelete.length) {
const deleteRows = toDelete.map(row => ([row.productcode, row.store, row.partnum, row.videoid]))
const deleteResponse = await deletePartVideoListRows(htpConnection, deleteRows)
responses.push(deleteResponse)
}
// Also delete any non-public rows that exist in the DB
const nonPublicRows = inventoryVideoRows.filter(row => row.public === 'False')
if (nonPublicRows.length) {
const deleteRows = nonPublicRows.map(row => ([productCode, row.storeid, row.inventoryid, row.video.id]))
const deleteResponse = await deletePartVideoListRows(htpConnection, deleteRows)
responses.push(deleteResponse)
}
if (toInsert.length) {
const upsertResponse = await partVideoListMultiRowUpsertQuery(htpConnection, toInsert)
responses.push(`partvideolist affected rows: ${upsertResponse?.affectedRows}`)
} else {
responses.push(`partvideolist affected rows: none`)
}
return responses
}
export async function upsertPartFileList(htpConnection, inventoryFileRows, productCode) {
const responses = []
// Get inventoryids and query existing rows up front — needed for both insert and stale-delete
const inventoryIds = inventoryFileRows.map(row => row.inventoryid)
const existingRows = await queryExistingAttachmentRows<ExistingFileListRow>(htpConnection, 'partfilelist', 'associatedid', inventoryIds, productCode)
// Convert all public incoming rows
const publicRows = inventoryFileRows.filter(row => row.public === 'True')
const convertedRows = publicRows.map(row => convertManyInventoryFileRows(row))
// Single pass: new rows to insert, stale rows to delete
const { toInsert, toDelete } = partitionFileListRows(convertedRows, existingRows)
if (toDelete.length) {
const deleteRows = toDelete.map(row => ([row.productcode, row.associatedid, row.associatedidtype, row.fileid]))
const deleteResponse = await deletePartFileListRows(htpConnection, deleteRows)
responses.push(deleteResponse)
}
// Also delete any non-public rows that exist in the DB
const nonPublicRows = inventoryFileRows.filter(row => row.public === 'False')
if (nonPublicRows.length) {
const deleteRows = nonPublicRows.map(row => ([productCode, 'inventoryid', row.inventoryid, row.file]))
const deleteResponse = await deletePartFileListRows(htpConnection, deleteRows)
responses.push(deleteResponse)
}
if (toInsert.length) {
const upsertResponse = await partFileListMultiRowUpsertQuery(htpConnection, toInsert)
responses.push(`partfilelist affected rows: ${upsertResponse?.affectedRows}`)
} else {
responses.push(`partfilelist affected rows: none`)
}
return responses
}
// ==== Select Query Functions ====
export type ExistingFileListRow = {
partfilelistid: number
productcode: number
associatedid: number
associatedidtype: 'inventoryid' | 'vehicleid'
fileid: number
filetype: 'Unknown' | 'Text' | 'PDF'
}
export type ExistingVideoListRow = {
partvideolistid: number
productcode: number
store: number
partnum: number
videotype: 'youtube'
videoid: string
url: string | null
dirty: 'False' | 'True'
}
export type ExistingImageListRow = {
partimagelistid: number
productcode: number
inventoryid: number
fileid: number | null
imagelocation: string | null
rank: number | null
dirty: 'False' | 'True'
}
export async function queryExistingAttachmentRows<T>(connection, table, column, ids, productCode) {
const queryOptions = {
sql: `SELECT * FROM ?? WHERE productcode = ? AND ?? IN (?)`,
values: [table, productCode, column, ids]
}
return await query<T>(connection, queryOptions)
}
import mysql from 'mysql2/promise'
import { multiRowInsertQuery, query, queryFirst, write } from './shared.js'
import _ from 'lodash'
function convertManyInventoryOptionValue(inventoryOptionListData, productInventoryRow, productCode) {
const tags = productInventoryRow.tags
const inventoryOptionRows = tags.map(function (tag) {
if (tag.value && tag.label) {
// Check inventoryoptionlist for match to use for building inventoryoption(value) row
const inventoryOptionListRow = inventoryOptionListData.find(element => tag.label === element.option && productInventoryRow.typeNum === element.sourcetypenum && (productInventoryRow.partListAuthorityId ? productInventoryRow.partListAuthorityId === element.typenum : productInventoryRow.typeNum === element.typenum))
if (inventoryOptionListRow) {
const { optionnum } = inventoryOptionListRow
return {
productcode: productCode,
storenum: productInventoryRow.storeId,
partnum: productInventoryRow.inventoryId,
value: tag.value,
optionnum,
lastupdate: mysql.raw('NOW()'),
}
} else {
console.debug(`No matching inventoryoptionlist row found for inventoryRow.tag.label ${tag.label} of inventory id ${productInventoryRow.inventoryId}`)
return
}
}
})
return inventoryOptionRows.filter(row => row !== undefined)
}
async function inventoryOptionMultiRowUpsertQuery(htpConnection, objectArray) {
// Get columns
const columns = Object.keys(objectArray[0])
const values = objectArray.map(obj => Object.values(obj))
const queryOptions = {
sql: `INSERT INTO inventoryoption (??) VALUES ? ON DUPLICATE KEY UPDATE ?? = values(??)`,
values: [columns, values, 'value', 'value']
}
return await write(htpConnection, queryOptions)
}
// Main multi-row upsert function
export async function upsertInventoryOptionRows(connection, productInventoryPartUseRows, productCode) {
// Get the relevant typenums to find existing optionlist rows
const allTypeNums = productInventoryPartUseRows.map(function(row) {
return row.typeNum
})
const uniqueTypeNums = _.uniqWith(allTypeNums, _.isEqual)
// Get a list of inventoryoptionlist rows
const inventoryOptionListData = await getInventoryOptionList(connection, productCode, uniqueTypeNums)
const inventoryOptionRows = productInventoryPartUseRows.map(row => convertManyInventoryOptionValue(inventoryOptionListData, row, productCode)).flat()
// const inventoryOptionMultiRowUpsertResponse= await inventoryOptionMultiRowUpsertQuery(connection, inventoryOptionRows)
if (inventoryOptionRows.length) {
const inventoryOptionMultiRowUpsertResponse = await inventoryOptionMultiRowUpsertQuery(connection, inventoryOptionRows)
return (`affected rows: inventoryoption ${inventoryOptionMultiRowUpsertResponse?.affectedRows}`)
} else {
return (`affected rows: inventoryoption none`)
}
}
async function queryInventoryOptionList(connection, productCode, typeNum, option) {
const queryOptions = {
sql: `SELECT * FROM inventoryoptionlist WHERE productcode = ? AND typenum = ? AND inventoryoptionlist.option = ?`,
values: [productCode, typeNum, option]
}
return await queryFirst<{ optionnum: number }>(connection, queryOptions)
}
// Deletes a specific row
export async function deleteInventoryOptionRow(connection, productCode, partNum, typeNum, option, value) {
// Need inventoryoptionlist data to get optionnum from inventoryoptionlist for a delete
const queryResult = await queryInventoryOptionList(connection, productCode, typeNum, option)
if (queryResult?.optionnum) {
try {
const queryOptions = {
sql: `DELETE FROM inventoryoption WHERE productcode = ? AND partnum = ? AND optionnum = ? AND value = ?`,
values: [ productCode, partNum, queryResult.optionnum, value ]
}
const deleteResponse = await write(connection, queryOptions)
return {
success: true,
request: '',
data: `inventoryoption delete affected rows: ${deleteResponse?.affectedRows}`
}
} catch (err) {
return {
success: false,
request: '',
data: `error deleting inventoryoption rows: ${err}`
}
}
} else {
return {
success: false,
request: '',
data: `no matching inventoryoptionlist value for typenum: ${typeNum} option: ${option}`
}
}
}
// Deletes all rows for an inventory item
export async function deleteInventoryOptionRows(connection, productCode, storeNum, partNum) {
// Get columns
try {
const queryOptions = {
sql: `DELETE FROM inventoryoption WHERE productcode = ? AND storenum = ? AND partnum = ?`,
values: [productCode, storeNum, partNum]
}
const deleteResponse = await write(connection, queryOptions)
return `inventoryoption delete affected rows: ${deleteResponse?.affectedRows}`
} catch (err) {
return `error deleting inventoryoption rows: ${err}`
}
}
// Bulk insert function
export async function insertInventoryOptionRows(connection, productInventoryPartUseRows, productCode) {
// grab all the relevant inventoryoptionlist values
// Get the relevant typenums to find existing optionlist rows
const allTypeNums = productInventoryPartUseRows.map(function(row) {
return row.typeNum
})
const uniqueTypeNums = _.uniqWith(allTypeNums, _.isEqual)
// Get a list of inventoryoptionrows
const inventoryOptionListData = await getInventoryOptionList(connection, productCode, uniqueTypeNums)
const inventoryOptionRows = productInventoryPartUseRows.map(row => convertManyInventoryOptionValue(inventoryOptionListData, row, productCode)).flat()
if (inventoryOptionRows.length > 0) {
const columns = Object.keys(inventoryOptionRows[0])
const values = inventoryOptionRows.map(obj => Object.values(obj))
const response = await multiRowInsertQuery(connection, 'inventoryoption', columns, values)
return (`affected rows: inventoryoption ${response?.affectedRows}`)
} else {
return (`affected rows: inventoryoption none`)
}
}
async function getInventoryOptionList(connection, productCode, typeNums) {
const queryData = [typeNums]
const queryOptions = {
sql: "SELECT * FROM inventoryoptionlist WHERE productcode = ? AND sourcetypenum IN ?",
values: [productCode, queryData]
}
return await query(connection, queryOptions)
}
import { multiRowInsertQuery, query, queryFirst, write } from './shared.js'
import _ from "lodash"
import mysql from 'mysql2/promise'
// Get partlistauthorityid and partuse from HTP using inventorytype name from universal inventory object
export async function getPartUseInfoQuery(connection, inventoryObjectRow, productCode) {
return await queryFirst(connection, {
sql: `SELECT partuse.*, partlistauthoritymap.partlistauthorityid
FROM partuse
LEFT JOIN partlistauthoritymap
ON partuse.productcode = partlistauthoritymap.productcode and
partuse.typenum = partlistauthoritymap.sourceid
WHERE partuse.productcode = ? AND part = ?`,
values: [productCode, inventoryObjectRow.inventoryType], //only inventorytype.name is provided
})
}
async function getExistingInventoryOptionList(connection, productCode, uniqueTypeNums) {
const queryData = [uniqueTypeNums]
// product.typeNum => htp.sourcetypenum
const queryOptions = {
sql: "SELECT optionnum, typenum, sourcetypenum, `option`, `rank`, public FROM inventoryoptionlist WHERE productcode = ? AND sourcetypenum IN ?",
values: [productCode, queryData],
}
const result = await query<{ optionnum: number, typenum: number, sourcetypenum: number, option: string, rank: number, public: boolean }>(connection, queryOptions)
// Create a list to compare against
const resultCache = result.map(function (row) {
return {
optionnum: row.optionnum,
typenum: row.typenum,
sourcetypenum: row.sourcetypenum,
option: row.option,
rank: row.rank,
productcode: productCode,
public: row.public // default is true so we should be safe
//lastupdate: mysql.raw('NOW()'),
}
})
return resultCache
}
async function inventoryOptionListMultiRowUpsertQuery(htpConnection, optionListArray) {
const newOptionListRows = optionListArray.map(obj => ({ ...obj, lastupdate: mysql.raw('NOW()') }))
const columns = Object.keys(newOptionListRows[0])
const values = optionListArray.map(obj => Object.values(obj))
const queryOptions = {
sql: 'INSERT INTO inventoryoptionlist (??) VALUES ? ON DUPLICATE KEY UPDATE `rank` = values(`rank`), lastupdate = values(lastupdate), public = values(public)',
values: [columns, values]
}
return await write(htpConnection, queryOptions)
}
function convertManyInventoryOptionList(productInventoryRow, productCode) {
//10 valid tags
const inventoryOptionListRows = productInventoryRow.tags.map(function (tag) {
// This only checks that both a value and label exist
if (tag.value && tag.label) {
return {
sourcetypenum: productInventoryRow.typeNum,
typenum: productInventoryRow.partListAuthorityId || productInventoryRow.typeNum,
option: tag.label,
rank: tag.rank,
productcode: productCode,
// TODO: Confirm this is correct or not
public: tag.public ? tag.public : 'True',
//lastupdate: mysql.raw('NOW()'),
}
}
})
// This gets rid of undefined rows, usually caused by empty strings
const filteredRows = inventoryOptionListRows.filter(row => row !== undefined)
return filteredRows
}
// Only a 5x difference in speed compared to the previous filter + find method but always tests faster
function filterExistingOptionListRows(newRows, existingRows) {
// Create a Map for lookups
const existingRowsMap = new Map()
// Populate the Map with composite keys
existingRows.forEach(row => {
const key = `${row.typenum}|${row.option}|${row.sourcetypenum}`
existingRowsMap.set(key, true)
})
// Filter newRows using the Map for lookups
return newRows.filter(newRow => {
const key = `${newRow.typenum}|${newRow.option}|${newRow.sourcetypenum}`
return !existingRowsMap.has(key)
})
}
// Old method, keeping here in case there are any side effects of the new Map method
// that testing didn't catch.
function filterExistingOptionListRowsOld(newRows, existingRows) {
const uniqueRows = newRows.filter(function(newRow) {
return !existingRows.find(function(existingRow) {
return existingRow.typenum === newRow.typenum && existingRow.option === newRow.option && existingRow.sourcetypenum === newRow.sourcetypenum
})
})
return uniqueRows
}
function matchExistingFileListRows(newRows, existingRows) {
const updatedRows = []
// Find matching rows, add optionnum and return updated object
//const testRows = newRows.map(row => addOptionNum(existingRows, row))
//or
for (const row of newRows) {
const existingRowMatch = existingRows.find(element => row.typenum === element.typenum && row.option === element.option)
if (existingRowMatch) {
const {optionnum} = existingRowMatch
row.optionnum = optionnum
updatedRows.push(row)
}
}
return updatedRows
}
// New function that takes the entire array of inventory rows with partuse data
// Option, rank, and public are the possible changeable values
export async function upsertInventoryOptionListRows(connection, inventoryRows, productCode) {
const responses = []
// Get the tags from all of the inventoryRows, there will be some duplicates
const optionListRows = inventoryRows.map(row => convertManyInventoryOptionList(row, productCode)).flat()
//Remove duplicates
const uniqueFilteredRows = _.uniqWith(optionListRows, _.isEqual)
// Get the relevant typenums to find existing optionlist rows
const allTypeNums = inventoryRows.map(function(row) {
return row.typeNum
})
const uniqueTypeNums = _.uniqWith(allTypeNums, _.isEqual)
// Get existing optionlist rows
const existingOptionList = await getExistingInventoryOptionList(connection, productCode, uniqueTypeNums)
// Create list of new and unique items for upsert
const uniqueOptionListRows = filterExistingOptionListRows(uniqueFilteredRows, existingOptionList)
// Upsert probably won't work as optionnum + productcode is the primary key
// If option changes, it will add a new one
if (uniqueOptionListRows.length) {
// Add lastupdate
const newOptionListRows = uniqueOptionListRows.map(obj => ({ ...obj, lastupdate: mysql.raw('NOW()') }))
const columns = Object.keys(newOptionListRows[0])
const values = newOptionListRows.map(obj => Object.values(obj))
const response = await multiRowInsertQuery(connection, 'inventoryoptionlist', columns, values)
responses.push(`affected rows: new inventoryoptionlist ${response?.affectedRows}`)
} else {
responses.push(`affected rows: new inventoryoptionlist none`)
}
const rowsWithOptionNum = matchExistingFileListRows(uniqueFilteredRows, existingOptionList)
if (rowsWithOptionNum.length) {
const newOptionListRows = rowsWithOptionNum.map(obj => ({ ...obj, lastupdate: mysql.raw('NOW()') }))
// Need to do a custom upsert query
const upsertResponse = await inventoryOptionListMultiRowUpsertQuery(connection, newOptionListRows)
responses.push(`affected rows: existing inventoryoptionlist ${upsertResponse?.affectedRows}`)
}
return responses
}
export async function deleteInventoryOptionListRow(connection, productCode, typeNum, option, rank) {
const queryOptions = {
sql: `DELETE FROM inventoryoptionlist WHERE productcode = ? AND typenum = ? AND inventoryoptionlist.option = ?`,
values: [productCode, typeNum, option]
}
try {
const result = await write(connection, queryOptions)
return {
success: true,
request: '',
data: `inventoryoptionlist delete affected rows: ${result.affectedRows}`
}
} catch (err) {
return {
success: false,
request: '',
data: err
}
}
}
// Bulk synch insert
export async function insertInventoryOptionListRows(connection, productInventoryRows, productCode) {
const responses = []
// Convert rows to insertable objects
const optionListRows = productInventoryRows.map(row => convertManyInventoryOptionList(row, productCode)).flat()
// Remove duplicates
const uniqueFilteredRows = _.uniqWith(optionListRows, _.isEqual)
// Get the relevant typenums to find existing optionlist rows
const allTypeNums = productInventoryRows.map(function(row) {
return row.typeNum
})
const uniqueTypeNums = _.uniqWith(allTypeNums, _.isEqual)
// Get list of existing rows from db to compare to
const existingOptionListRows = await getExistingInventoryOptionList(connection, productCode, uniqueTypeNums)
// Create a list of new items to be inserted
const uniqueOptionListRows = filterExistingOptionListRows(uniqueFilteredRows, existingOptionListRows)
// Add lastupdate to rows
// TODO: this can be done as part of the conversion, its a leftover from a failed unique comparison func
if (uniqueOptionListRows.length) {
const newOptionListRows = uniqueOptionListRows.map(obj => ({ ...obj, lastupdate: mysql.raw('NOW()') }))
const columns = Object.keys(newOptionListRows[0])
const values = newOptionListRows.map(obj => Object.values(obj))
const response = await multiRowInsertQuery(connection, 'inventoryoptionlist', columns, values)
responses.push(`affected rows: inventoryoptionlist ${response?.affectedRows}`)
} else {
responses.push(`affected rows: inventoryoptionlist none`)
}
return responses
}
import { multiRowUpsertQuery, multiRowInsertQuery, write } from './shared.js'
import pProps from 'p-props'
// TODO: remove insert functions...everything should work with upserts
// TODO: upserts may not be working correctly and instead inserting new rows every time
// Called by inventory row delete
export async function deleteInventorySourceData(connection, productCode, partNum) {
try {
const queryOptions = {
sql: `DELETE FROM inventorysource WHERE productcode = ? AND partnum = ?`,
values: [productCode, partNum]
}
const deleteResponse = await write(connection, queryOptions)
return `inventorysource delete affected rows: ${deleteResponse?.affectedRows}`
} catch (err) {
return `error deleting inventorysource row: ${err}`
}
}
// Called when an inventory source row is deleted
export async function deleteInventorySourceRow(connection, productCode, inventorySourceId) {
try {
const queryOptions = {
sql: `DELETE FROM inventorysource WHERE productcode = ? AND inventorysourceid`,
values: [productCode, inventorySourceId]
}
const deleteResponse = await write(connection, queryOptions)
return {
success: true,
data: `inventorysource delete affected rows: ${deleteResponse?.affectedRows}`
}
} catch (err) {
return {
success: false,
data: `error deleting inventorysource row: ${err}`
}
}
}
// #things that can change: quantity, quantityallocated, date? status?
// #Compare new and existing rows by
// #inventorystoreid, inventoryid, documenttype, documentid, documentstoreid, documentlineid
// #if any match, update those rows after grabbing the htpinventorysourceid
// #if any new rows don't match, insert those as new
// Finds existing rows to be updated
function filterExistingInventorySourceRows(newRows, existingRows) {
// For each new row, find the matching existing row
const updateRows = []
for (const newRow of newRows) {
const matchingRow = existingRows.find(function(existingRow) {
return existingRow.inventorystoreid === newRow.inventorystoreid && existingRow.inventoryid === newRow.inventoryid
&& newRow.documenttype === existingRow.documenttype && newRow.documentid == existingRow.documentid &&
newRow.documentstoreid == existingRow.documentstoreid && newRow.documentlineid == existingRow.documentlineid
})
// If exists, get the htpinventorysourceid and add it to updateRows
if (matchingRow) {
newRow.htpinventorysourceid = matchingRow.htpinventorysourceid
updateRows.push(newRow)
}
}
return updateRows
}
// Finds new rows for insert
function filterNewInventorySourceRows(newRows, existingRows) {
const newInventorySourceRows = newRows.filter(function(newRow) {
return !existingRows.find(function(existingRow) {
return existingRow.inventorystoreid === newRow.inventorystoreid && existingRow.inventoryid === newRow.inventoryid
&& newRow.documenttype === existingRow.documenttype && newRow.documentid == existingRow.documentid &&
newRow.documentstoreid == existingRow.documentstoreid && newRow.documentlineid == existingRow.documentlineid
})
})
return newInventorySourceRows
}
async function queryExistingInventorySourceRows(connection, inventoryIds, productCode) {
const queryOptions = {
sql: `SELECT * FROM inventorysource WHERE productcode = ? AND inventoryid IN (?)`,
values: [productCode, inventoryIds]
}
return await write(connection, queryOptions)
}
// Called by inventory upsert
export async function upsertInventorySourceRows(connection, productInventoryRows, productCode) {
const responses = []
// Flatten the rows then create source map array, add productcode and partnum
const convertedInventorySourceRows = productInventoryRows.flatMap(inventoryRow =>
inventoryRow.openSources.map(sourceRow => ({
inventorystoreid: sourceRow.inventoryStoreId,
quantity: sourceRow.quantity,
quantityallocated: sourceRow.quantityAllocated,
documenttype: sourceRow.documentType,
documentstoreid: sourceRow.documentStoreId,
documentlineid: sourceRow.documentLineId,
date: sourceRow.date,
status: sourceRow.status,
productcode: productCode,
inventoryid: inventoryRow.inventoryId,
}))
)
// Filter out new rows for insert
if (convertedInventorySourceRows.length) {
// Get the inventoryids and query existing rows
const inventoryIds = productInventoryRows.map(row => row.inventoryId )
const existingInventorySourceRows = await queryExistingInventorySourceRows(connection, inventoryIds, productCode)
//things that can change: quantity, quantityallocated, date? status?
// Find new rows for insert
const newInventorySourceRows = filterNewInventorySourceRows(convertedInventorySourceRows, existingInventorySourceRows)
// Find existing rows for possible update
const updateInventorySourceRows = filterExistingInventorySourceRows(convertedInventorySourceRows, existingInventorySourceRows)
//const uniqueRows = filterExistingInventorySourceRows(convertedInventorySourceRows, existingInventorySourceRows)
let promisesObj: Record<string, Promise<any>> = {}
if (newInventorySourceRows.length) {
const columns = Object.keys(newInventorySourceRows[0])
const values = newInventorySourceRows.map(obj => Object.values(obj))
promisesObj.insertResponse = multiRowInsertQuery(connection, "inventorysource", columns, values)
}
if (updateInventorySourceRows.length) {
promisesObj.updateResponse = multiRowUpsertQuery(connection, 'inventorysource', updateInventorySourceRows)
}
if (Object.keys(promisesObj).length > 0) {
const results = await pProps(promisesObj)
if (results.insertResponse) {
responses.push(`new inventorysource affected rows: ${results.insertResponse?.affectedRows}`)
}
if (results.updateResponse) {
responses.push(`updated inventorysource affected rows: ${results.updateResponse?.affectedRows}`)
}
} else {
responses.push(`inventorysource affected rows: none`)
}
} else {
responses.push(`inventory source affected rows: none`)
}
return responses
}
import { multiRowUpsertQuery, write } from './shared.js'
import mysql from 'mysql2/promise'
// TODO: Change naming to invmaster to match others
function convertVehicle(vehicleRow, productCode) {
return {
stocknum: vehicleRow.id,
// Original trigger doesn't use stocknumber
store: vehicleRow.storeId,
vinnum: vehicleRow.vin,
make: vehicleRow.make,
model: vehicleRow.model,
year: vehicleRow.year,
mileage: vehicleRow.mileage,
bodystyle: vehicleRow.bodyStyle,
extcolor: vehicleRow.colors[1].name,
extcolorcode: vehicleRow.colors[1].code,
intcolor: vehicleRow.colors[0].name,
intcolorcode: vehicleRow.colors[0].code,
status: vehicleRow.status,
dateentered: vehicleRow.dateEntered,
trackingnum: vehicleRow.tagNumber,
damage: vehicleRow.description,
productcode: productCode,
lastupdate: mysql.raw('NOW()'),
titlenum: vehicleRow.title?.number || '',
titlestatus: vehicleRow.title?.status || '',
titletype: vehicleRow.title?.type || ''
}
}
// Multiple row insert
export async function upsertVehicleRows(connection, vehicleArr, productCode) {
const responses = []
try {
// Convert to HTP insertable object
const convertedRows = vehicleArr.map(row => convertVehicle(row, productCode))
const multiRowUpsertResponse = await multiRowUpsertQuery(connection, 'invmaster', convertedRows)
responses.push(`invmaster affected rows: ${multiRowUpsertResponse.affectedRows}`)
return {
success: true,
request: '',
data: responses
}
} catch (err) {
return {
success: false,
request: '',
data: err
}
}
}
export async function deleteVehicle(connection, productCode, stockNum, store) {
const responses = []
try {
const queryOptions = {
sql: `DELETE FROM invmaster WHERE productcode = ? AND stocknum = ? AND store = ?`,
values: [productCode, stockNum, store],
}
const deleteResponse = await write(connection, queryOptions)
responses.push(`invmaster delete affected rows: ${deleteResponse?.affectedRows}`)
return {
success: true,
request: '',
data: responses
}
} catch (err) {
return {
success: false,
request: '',
data: err
}
}
}
import { multiRowInsertQuery, multiRowUpsertQuery, query, write } from './shared.js'
import mysql from 'mysql2/promise'
// Convert universal model object to HTP insertable row for mnfcrmod
function convertMnfcrmod(modelRow, productCode) {
return {
typenum: modelRow.inventoryTypeId,
pmanufacturer: modelRow.manufacturerName?.substring(0, 24) ?? '',
pmodel: modelRow.name,
firstyear: modelRow.firstYear,
lastyear: modelRow.lastYear,
//weight: ,
productcode: productCode,
active: modelRow.active,
// Following are required but not in object
sourcetypenum: modelRow.inventoryTypeId,
category: 'Used',
intcode: '',
lastupdate: mysql.raw('NOW()'),
}
}
// Convert universal model object to HTP insertable row for model
function convertModel(modelRow, productCode) {
return {
Model: modelRow.name?.substring(0, 29) ?? '',
Make: modelRow.manufacturerName?.substring(0, 24) ?? '',
FirstYear: modelRow.firstYear,
LastYear: modelRow.lastYear,
Series: modelRow.series,
Active: modelRow.active,
lastupdate: mysql.raw('NOW()'),
productcode: productCode,
}
}
async function queryExistingMnfcrmod(connection, typenums, pmanufacturers, pmodels, productCode) {
const queryOptions = {
sql: `SELECT * FROM mnfcrmod WHERE productcode = ? AND typenum IN (?) AND pmanufacturer IN (?) AND pmodel IN (?)`,
values: [productCode, typenums, pmanufacturers, pmodels]
}
return await query(connection, queryOptions)
}
/*
Looks like we can only update the first/last year and active columns
Leaving out mnfcrmod.name will find the nearest match of type and manufacturer but only the first of the .names gets added
If a name changes, it won't delete or change the existing row, only create a new one and there's no current way to fix that
*/
async function multiRowMnfcrModUpsertQuery(htpConnection, objectArray) {
// Get columns
const columns = Object.keys(objectArray[0])
const values = objectArray.map(obj => Object.values(obj))
// Create query string addition for all of the columns to be updated
// const queryStringAdd = columns.map(row => `?? = VALUES(??)`)
const queryOptions = {
sql: 'INSERT INTO mnfcrmod (??) VALUES ? ON DUPLICATE KEY UPDATE firstyear = VALUES(firstyear), lastyear = VALUES(lastyear), `active` = VALUES(`active`)',
values: [columns, values],
}
return await write(htpConnection, queryOptions)
}
// Look for existing rows and add htpmnfcrmodid if exists
function filterMnfcrmodRows(mnfcrmodArr, existingRowsArr) {
const updateRows = []
mnfcrmodArr.forEach(row => {
const matchingRow = existingRowsArr.find(element => element.typenum === row.typenum && element.sourcetypenum === row.typenum && element.pmanufacturer === row.pmanufacturer && row.pmodel === element.pmodel)
if (matchingRow) {
row.htpmnfcrmodid = matchingRow.htpmnfcrmodid
updateRows.push(row)
} else {
row.htpmnfcrmodid = undefined
updateRows.push(row)
}
})
return updateRows
}
/*
Unable to run a simple ON DUPLICATE KEY UPDATE on all rows as we get multiple copies
We'll have to check for existing mnfcrmod rows and add the primary key to the object then update
or insert if there's no match.
*/
async function upsertMnfcrmodRows(connection, mnfcrmodArr, productCode) {
// Convert to HTP rows
const convertedMnfcrmodRows = mnfcrmodArr.map(row => convertMnfcrmod(row, productCode))
// Create all the columns needed to build query to check for existing rows
const typenums = convertedMnfcrmodRows.map(row => row.typenum)
const pmanufacturers = convertedMnfcrmodRows.map(row => row.pmanufacturer)
const pmodels = convertedMnfcrmodRows.map(row => row.pmodel)
// Query for existing rows
const existingRowsResponse = await queryExistingMnfcrmod(connection, typenums, pmanufacturers, pmodels, productCode)
// Filter for matching rows and add key if exists
const updateRows = filterMnfcrmodRows(convertedMnfcrmodRows, existingRowsResponse)
// Run upsert query
const upsertResponse = await multiRowMnfcrModUpsertQuery(connection, updateRows)
return (`mnfcrmod affected rows: ${upsertResponse?.affectedRows}`)
}
export async function upsertMnfcrmodModelRows(connection, modelArr, productCode) {
const responses = []
// Need to filter into mnfcrmod vs model rows
const nonVehicleUnitRows = modelArr.filter(row => row.vehicleUnit === 'False')
const vehicleUnitRows = modelArr.filter(row => row.vehicleUnit === 'True')
try {
if (nonVehicleUnitRows.length) {
// Convert
const mnfcrmodUpsertResponse = await upsertMnfcrmodRows(connection, nonVehicleUnitRows, productCode)
responses.push(mnfcrmodUpsertResponse)
}
if (vehicleUnitRows.length) {
// Convert rows to HTP
const convertedModelRows = vehicleUnitRows.map(row => convertModel(row, productCode))
const modelUpsertResponse = await multiRowUpsertQuery(connection, 'model', convertedModelRows)
responses.push(`model affected rows: ${modelUpsertResponse?.affectedRows}`)
}
return {
success: true,
request: '',
data: responses
}
} catch (err) {
return {
success: false,
request: '',
data: err
}
}
}
async function deleteMnfcrmod(connection, typeNum, manufacturerName, modelName, firstYear, lastYear, productCode ) {
const queryOptions = {
sql: `DELETE FROM mnfcrmod WHERE productcode = ? AND typenum = ? AND pmanufacturer = ? AND pmodel = ? AND firstyear = ? AND lastyear = ?`,
values: [ productCode, typeNum, manufacturerName, modelName, firstYear, lastYear ]
}
return await write(connection, queryOptions)
}
async function deleteModel(connection, manufacturerName, modelName, firstYear, lastYear, productCode) {
const queryOptions = {
sql: `DELETE FROM model WHERE productcode = ? AND Make = ? AND Model = ? AND FirstYear = ? AND LastYear = ?`,
values: [ productCode, manufacturerName, modelName, firstYear, lastYear]
}
return await write(connection, queryOptions)
}
export async function deleteMnfcrmodModelRow(connection, typeNum, manufacturerName, modelName, firstYear, lastYear, vehicleUnit, productCode ) {
const responses = []
// Get manufacturer name
// Get vehicleunit true/false
try {
if (vehicleUnit === 'False') {
// delete from mnfcrmodel
const deleteResult = await deleteMnfcrmod(connection, typeNum, manufacturerName, modelName, firstYear, lastYear, productCode )
responses.push(`mnfcrmod delete affected rows: ${deleteResult?.affectedRows}`)
} else if (vehicleUnit === 'True') {
// delete from Model
const deleteResult = await deleteModel(connection, manufacturerName, modelName, firstYear, lastYear, productCode)
responses.push(`Model delete affected rows: ${deleteResult?.affectedRows}`)
}
return {
success: true,
request: '',
data: responses
}
} catch (err) {
return {
success: false,
request: '',
data: err
}
}
}
export async function insertMnfcrmodModelRows(connection, modelArr, productCode) {
const responses = []
// Need to filter into mnfcrmod vs model rows
const nonVehicleUnitRows = modelArr.filter(row => row.vehicleUnit === 'False')
const vehicleUnitRows = modelArr.filter(row => row.vehicleUnit === 'True')
try {
if (nonVehicleUnitRows.length) {
// Convert rows to HTP mnfcrmod
const convertedNonVehicleRows = nonVehicleUnitRows.map(row => convertMnfcrmod(row, productCode))
const nonVehicleColumns = Object.keys(convertedNonVehicleRows[0])
const nonVehicleValues = convertedNonVehicleRows.map(obj => Object.values(obj))
const mnfcrmodInsertResponse = await multiRowInsertQuery(connection, 'mnfcrmod', nonVehicleColumns, nonVehicleValues)
responses.push(`mnfcrmod affected rows: ${mnfcrmodInsertResponse?.affectedRows}`)
}
if (vehicleUnitRows.length) {
// Convert rows to HTP model
const convertedModelRows = vehicleUnitRows.map(row => convertModel(row, productCode))
const vehicleColumns = Object.keys(convertedModelRows[0])
const vehicleValues = convertedModelRows.map(obj => Object.values(obj))
const modelInsertResponse = await multiRowInsertQuery(connection, 'model', vehicleColumns, vehicleValues)
responses.push(`model affected rows: ${modelInsertResponse?.affectedRows}`)
}
return {
success: true,
request: '',
data: responses
}
} catch (err) {
return {
success: false,
request: '',
data: err
}
}
}
import { multiRowUpsertQuery, write } from './shared.js'
function convertPartListAuthorityMap(inventoryTypeRow, productCode) {
return {
partlisttype: 'inventorytype',
productcode: productCode,
sourceid: inventoryTypeRow.inventoryTypeId,
partlistauthorityid: inventoryTypeRow.partListAuthorityId,
}
}
export async function upsertPartListAuthorityMapRows(connection, inventoryTypeArr, productCode) {
const partListRows = inventoryTypeArr.filter(row => row.partListAuthorityId)
if (partListRows.length) {
try {
const convertedRows = partListRows.map(row => convertPartListAuthorityMap(row, productCode))
const queryResponse = await multiRowUpsertQuery(connection, 'partlistauthoritymap', convertedRows)
return {
success: true,
request: '',
data: queryResponse.affectedRows || 'none'
}
} catch (err) {
return {
success: false,
request: '',
data: err
}
}
} else {
return {
success: true,
request: '',
data: 'none'
}
}
}
export async function deletePartListAuthorityMapRow(connection, productCode, inventoryType, partListAuthorityId) {
try {
const queryOptions = {
sql: `DELETE FROM partlistauthoritymap WHERE productcode = ? AND partlisttype = 'inventorytype' AND sourceid = ? AND partlistauthorityid = ?`,
values: [productCode, inventoryType, partListAuthorityId],
}
const deleteResponse = await write(connection, queryOptions)
//responses.push(`partlistauthoritymap affected rows: ${deleteResponse?.affectedRows}`)
return `partlistauthoritymap delete affected rows: ${deleteResponse?.affectedRows}`
} catch (err) {
return `error deleting partlistauthoritymap rows: ${err}`
}
}
import mysql from 'mysql2/promise'
import pProps from 'p-props'
import { upsertPartListAuthorityMapRows, deletePartListAuthorityMapRow } from './partListAuthority.js'
import { multiRowUpsertQuery, write } from './shared.js'
function convertInventoryType(row, productCode) {
return {
typenum: row.inventoryTypeId,
part: row.name,
set: row.typeSetId,
used: row.active, //boolean
manmod: row.useManufacturer,
productcode: productCode,
lastupdate: mysql.raw('NOW()'),
// more fields are excluded as they're part of a unified tags object
}
}
// Multiple row upsert
export async function upsertPartUseRows(connection, inventoryTypeArr, productCode) {
const responses = []
try {
// Convert to HTP insertable object
const convertedRows = inventoryTypeArr.map(row => convertInventoryType(row, productCode))
// Use pProps to run both database operations in parallel
const results = await pProps({
partUse: multiRowUpsertQuery(connection, 'partuse', convertedRows),
partListAuthorityMap: upsertPartListAuthorityMapRows(connection, inventoryTypeArr, productCode)
})
// Process results
responses.push(
`partUse affected rows: ${results.partUse.affectedRows}`,
)
if (results.partListAuthorityMap.success) {
responses.push(
`partlistauthoritymap affected rows: ${results.partListAuthorityMap.data}`,
)
} else {
responses.push(
`partlistauthoritymap error: ${results.partListAuthorityMap.data}`,
)
}
return ({
success: true,
request: '',
data: responses,
})
} catch (err) {
return {
success: false,
request: '',
data: err,
}
}
}
export async function deletePartUseRow(connection, productCode, inventoryType, partListAuthorityId) {
const responses = []
// Partuse delete query
const queryOptions = {
sql: `DELETE FROM partuse WHERE productcode = ? AND typenum = ?`,
values: [productCode, inventoryType]
}
try {
const partuseDeleteResult = await write(connection, queryOptions)
responses.push(`partuse delete affected rows: ${partuseDeleteResult?.affectedRows}`)
// Delete partlistauthoritymap row if exists
if (partListAuthorityId) {
const partListAuthorityDeleteResponse = await deletePartListAuthorityMapRow(connection, productCode, inventoryType, partListAuthorityId)
responses.push(partListAuthorityDeleteResponse)
}
return {
success: true,
request: '',
data: responses
}
} catch (err) {
return {
success: false,
request: '',
data: err
}
}
}
import { multiRowUpsertQuery, write } from './shared.js'
import { PaymentRow, HtpPaymentRow } from './types.js'
function convertPaymentRow(row: PaymentRow, productCode: number): HtpPaymentRow {
return {
productcode: productCode,
paymentid: row.paymentId,
storeid: row.storeId,
paymentmethodname: row.paymentMethodName,
documentnumber: row.documentNumber,
dateentered: row.dateEntered,
date: row.date,
void: row.void,
comments: row.comments
}
}
export async function upsertPaymentRows(connection, paymentArr, productCode) {
const responses = []
try {
const convertedRows = paymentArr.map(row => convertPaymentRow(row, productCode))
const multiRowUpsertResponse = await multiRowUpsertQuery(connection, 'payment', convertedRows)
responses.push(`payment affected rows: ${multiRowUpsertResponse.affectedRows}`)
return {
success: true,
data: responses
}
} catch (err) {
return {
success: false,
data: err
}
}
}
export async function deletePaymentRow(connection, productCode, htpPaymentId) {
const responses = []
try {
const queryOptions = {
sql: `DELETE FROM payment WHERE productcode = ? AND htppaymentid = ?`,
values: [productCode, htpPaymentId],
}
const deleteResponse = await write(connection, queryOptions)
responses.push(`payment delete affected rows: ${deleteResponse?.affectedRows}`)
return {
success: true,
data: responses
}
} catch (err) {
return {
success: false,
data: err
}
}
}
import { multiRowUpsertQuery, write } from './shared.js'
import { PaymentLineRow, HtpPaymentLineRow } from './types.js'
function convertPaymentLineRow(row: PaymentLineRow, productCode: number): HtpPaymentLineRow {
return {
productcode: productCode,
paymentlineid: row.paymentLineId,
storeid: row.storeId,
paymentid: row.paymentId,
salesorderstoreid: row.salesOrderStoreId,
salesorderid: row.salesOrderId,
amount: row.amount,
}
}
export async function upsertPaymentLineRows(connection, paymentLineArr, productCode) {
const responses = []
try {
const convertedRows = paymentLineArr.map(row => convertPaymentLineRow(row, productCode))
const multiRowUpsertResponse = await multiRowUpsertQuery(connection, 'paymentline', convertedRows)
responses.push(`paymentline affected rows: ${multiRowUpsertResponse.affectedRows}`)
return {
success: true,
data: responses
}
} catch (err) {
return {
success: false,
data: err
}
}
}
export async function deletePaymentLineRow(connection, productCode, htpPaymentLineId) {
const responses = []
try {
const queryOptions = {
sql: `DELETE FROM paymentline WHERE productcode = ? AND htppaymentlineid = ?`,
values: [productCode, htpPaymentLineId],
}
const deleteResponse = await write(connection, queryOptions)
responses.push(`paymentline delete affected rows: ${deleteResponse?.affectedRows}`)
return {
success: true,
data: responses
}
} catch (err) {
return {
success: false,
data: err
}
}
}
import { multiRowUpsertQuery, write } from './shared.js'
import { SalesOrderRow, HtpSalesOrderRow } from './types.js'
function convertSalesOrderRow(row: SalesOrderRow, productCode: number): HtpSalesOrderRow {
return {
productcode: productCode,
storeid: row.storeId,
salesorderid: row.salesOrderId,
sequentialid: row.sequentialId,
parentsalesorderid: row.parentSalesOrderId,
date: row.date,
void: row.void,
salesorderdocumentname: row.salesOrderDocumentName,
closed: row.closed,
expirationdate: row.expirationDate,
dateentered: row.dateEntered,
dateclosed: row.dateClosed,
datemodified: row.dateModified,
termname: row.termName,
shippingmethodname: row.shippingMethodName,
shippingtrackingnumber: row.shippingTrackingNumber,
customerid: row.customerId,
tax: row.tax,
subtotal: row.subtotal,
internalcomments: row.internalComments,
externalcomments: row.externalComments,
billingcompany: row.billingCompany,
billingcontact: row.billingContact,
billingstreet: row.billingStreet,
billingcity: row.billingCity,
billingstate: row.billingState,
billingzip: row.billingZip,
billingcountry: row.billingCountry,
billingphone: row.billingPhone,
billingemail: row.billingEmail,
shipcompany: row.shipCompany,
shipcontact: row.shipContact,
shipstreet: row.shipStreet,
shipcity: row.shipCity,
shipstate: row.shipState,
shipzip: row.shipZip,
shipcountry: row.shipCountry,
shipphone: row.shipPhone,
shipemail: row.shipEmail,
shippingcustomeraddressid: row.shippingCustomerAddressId,
purchaseordernumber: row.purchaseOrderNumber,
trucknumber: row.truckNumber,
websaleid: row.webSaleId,
websaletype: row.webSaleType,
websaleorigin: row.webSaleOrigin,
vendorapprovalstatus: row.vendorApprovalStatus,
customerapprovalstatus: row.customerApprovalStatus,
}
}
export async function upsertSalesOrderRows(connection, salesOrderArr, productCode) {
const responses = []
try {
const convertedRows = salesOrderArr.map(row => convertSalesOrderRow(row, productCode))
const multiRowUpsertResponse = await multiRowUpsertQuery(connection, 'salesorder', convertedRows)
responses.push(`salesorder affected rows: ${multiRowUpsertResponse.affectedRows}`)
return {
success: true,
data: responses
}
} catch (err) {
return {
success: false,
data: err
}
}
}
export async function deleteSalesOrderRow(connection, productCode, htpSalesOrderId) {
const responses = []
try {
const queryOptions = {
sql: `DELETE FROM salesorder WHERE productcode = ? AND htpsalesorderid = ?`,
values: [productCode, htpSalesOrderId],
}
const deleteResponse = await write(connection, queryOptions)
responses.push(`salesorder delete affected rows: ${deleteResponse?.affectedRows}`)
return {
success: true,
data: responses
}
} catch (err) {
return {
success: false,
data: err
}
}
}
import { multiRowUpsertQuery, write } from './shared.js'
import { SalesOrderAdjustmentRow, HtpSalesOrderAdjustmentRow } from './types.js'
function convertSalesOrderAdjustmentRow(row: SalesOrderAdjustmentRow, productCode: number): HtpSalesOrderAdjustmentRow {
return {
productcode: productCode,
salesorderadjustmentid: row.salesOrderAdjustmentId,
salesorderid: row.salesOrderId,
salesorderlineid: row.salesOrderLineId,
salesorderstoreid: row.salesOrderStoreId,
amount: row.amount,
date: row.date,
adjustmenttypename: row.adjustmentTypeName,
description: row.description,
void: row.void,
taxable: row.taxable,
subtotaladjustment: row.subtotalAdjustment,
}
}
export async function upsertSalesOrderAdjustmentRows(connection, salesOrderAdjustmentArr, productCode) {
const responses = []
try {
const convertedRows = salesOrderAdjustmentArr.map(row => convertSalesOrderAdjustmentRow(row, productCode))
const multiRowUpsertResponse = await multiRowUpsertQuery(connection, 'salesorderadjustment', convertedRows)
responses.push(`salesorderadjustment affected rows: ${multiRowUpsertResponse.affectedRows}`)
return {
success: true,
data: responses
}
} catch (err) {
return {
success: false,
data: err
}
}
}
export async function deleteSalesOrderAdjustmentRow(connection, productCode, htpSalesOrderAdjustmentId) {
const responses = []
try {
const queryOptions = {
sql: `DELETE FROM salesorderadjustment WHERE productcode = ? AND htpsalesorderadjustmentid = ?`,
values: [productCode, htpSalesOrderAdjustmentId],
}
const deleteResponse = await write(connection, queryOptions)
responses.push(`salesorderadjustment delete affected rows: ${deleteResponse?.affectedRows}`)
return {
success: true,
data: responses
}
} catch (err) {
return {
success: false,
data: err
}
}
}
import { multiRowUpsertQuery, write } from './shared.js'
import { SalesOrderLineRow, HtpSalesOrderLineRow } from './types.js'
function convertSalesOrderLineRow(row: SalesOrderLineRow, productCode: number): HtpSalesOrderLineRow {
return {
productcode: productCode,
salesorderlineid: row.salesOrderLineId,
salesorderid: row.salesOrderId,
storeid: row.storeId,
rank: row.rank,
parentsalesorderlineid: row.parentSalesOrderLineId,
inventoryid: row.inventoryId,
inventorystoreid: row.inventoryStoreId,
inventorytypeid: row.inventoryTypeId,
type: row.type,
collectiontype: row.collectionType,
collectionquantity: row.collectionQuantity,
vin: row.vin,
make: row.make,
vehiclemodel: row.vehicleModel,
manufacturer: row.manufacturer,
model: row.model,
year: row.year,
bodystyle: row.bodyStyle,
side: row.side,
description: row.description,
interchangenumber: row.interchangeNumber,
subinterchangenumber: row.subInterchangeNumber,
vehicleid: row.vehicleId,
partnumber: row.partNumber,
vendorid: row.vendorId,
retailprice: row.retailPrice,
wholesaleprice: row.wholesalePrice,
jobberprice: row.jobberPrice,
distributorprice: row.distributorPrice,
customerprice: row.customerPrice,
cost: row.cost,
averagecost: row.averageCost,
coreclass: row.coreClass,
serialnumber: row.serialNumber,
core: row.core,
price: row.price,
quantity: row.quantity,
deliverystatus: row.deliveryStatus,
taxable: row.taxable,
creditissued: row.creditIssued,
creditstoreid: row.creditStoreId,
creditsalesorderid: row.creditSalesOrderId,
creditsalesorderlineid: row.creditSalesOrderLineId,
returnable: row.returnable,
daystoreturn: row.daysToReturn,
corerequired: row.coreRequired,
daystoreturncore: row.daysToReturnCore,
returncodename: row.returnCodeName,
deliverydate: row.deliveryDate,
dateentered: row.dateEntered,
websalelineid: row.webSaleLineId,
corestatus: row.coreStatus,
}
}
export async function upsertSalesOrderLineRows(connection, salesOrderLineArr, productCode) {
const responses = []
try {
const convertedRows = salesOrderLineArr.map(row => convertSalesOrderLineRow(row, productCode))
const multiRowUpsertResponse = await multiRowUpsertQuery(connection, 'salesorderline', convertedRows)
responses.push(`salesorderline affected rows: ${multiRowUpsertResponse.affectedRows}`)
return {
success: true,
data: responses
}
} catch (err) {
return {
success: false,
data: err
}
}
}
export async function deleteSalesOrderLineRow(connection, productCode, htpSalesOrderLineId) {
const responses = []
try {
const queryOptions = {
sql: `DELETE FROM salesorderline WHERE productcode = ? AND htpsalesorderlineid = ?`,
values: [productCode, htpSalesOrderLineId],
}
const deleteResponse = await write(connection, queryOptions)
responses.push(`salesorderline delete affected rows: ${deleteResponse?.affectedRows}`)
return {
success: true,
data: responses
}
} catch (err) {
return {
success: false,
data: err
}
}
}
import { multiRowUpsertQuery, write } from './shared.js'
function convertSellPriceContract(row, productCode ) {
return {
productcode: productCode,
sellpriceclassid: row.sellPriceClassId,
name: row.name,
parentsellpriceclassid: row.parentSellPriceClassId
}
}
export async function upsertSellPriceClassRows(connection, sellPriceClassArr, productCode) {
const responses = []
try {
const convertedRows = sellPriceClassArr.map(row => convertSellPriceContract(row, productCode))
const multiRowUpsertResponse = await multiRowUpsertQuery(connection, 'sellpriceclass', convertedRows)
responses.push(`sellpriceclass affected rows: ${multiRowUpsertResponse.affectedRows}`)
return {
success: true,
data: responses
}
} catch (err) {
return {
success: false,
data: err
}
}
}
// Do we need parentsellpriceclassid too?
export async function deleteSellPriceClassRow(connection, productCode, sellPriceClassId) {
const responses = []
try {
const queryOptions = {
sql: `DELETE FROM sellpriceclass WHERE productcode = ? AND sellpriceclassid = ?`,
values: [productCode, sellPriceClassId],
}
const deleteResponse = await write(connection, queryOptions)
responses.push(`sellpriceclass delete affected rows: ${deleteResponse?.affectedRows}`)
return {
success: true,
data: responses
}
} catch (err) {
return {
success: false,
data: err
}
}
}
import { Pool, Connection, QueryOptions, ResultSetHeader } from 'mysql2/promise'
/**
* Execute a query and return results
* Works similar to utility-db's query function
*
*/
export async function query<T = unknown>(connection: Pool | Connection, queryOptions: QueryOptions | string): Promise<T[]> {
// mysql2/promise returns [rows, fields]
// Both Pool and Connection have .query() method
// Pool.query() automatically manages connection get/release
const [rows] = await connection.query(queryOptions as QueryOptions)
return rows as T[]
}
/**
* Execute a query and return the first result
* Works similar to utility-db's readFirst function
*
*/
export async function queryFirst<T = unknown>(connection: Pool | Connection, queryOptions: QueryOptions | string): Promise<T | null> {
// mysql2/promise returns [rows, fields]
// Both Pool and Connection have .query() method
// Pool.query() automatically manages connection get/release
const [rows] = await connection.query(queryOptions as QueryOptions)
return ((rows as T[])[0]) ?? null
}
export async function write(connection: Pool | Connection, queryOptions: QueryOptions | string): Promise<ResultSetHeader> {
// mysql2/promise returns [ResultSetHeader, fields] for write operations
// ResultSetHeader contains: affectedRows, insertId, warningStatus, etc.
// Both Pool and Connection have .query() method
// Pool.query() automatically manages connection get/release
const [result] = await connection.query(queryOptions as QueryOptions)
return result as ResultSetHeader
}
export async function upsertQuery(connection: Pool | Connection, table: string, row: object): Promise<ResultSetHeader> {
const queryOptions = {
sql: "INSERT INTO ?? SET ? ON DUPLICATE KEY UPDATE ?",
values: [ table, row, row ],
}
return await write(connection, queryOptions)
}
export async function multiRowUpsertQuery(htpConnection: Pool | Connection, table: string, objectArray: object[]): Promise<ResultSetHeader> {
// Get columns
const columns = Object.keys(objectArray[0])
const values = objectArray.map(obj => Object.values(obj))
// Duplicate the columns so it can be passed in query values
const doubleColumns = columns.flatMap(i => [ i, i ])
// Create query string addition for all of the columns to be updated
const queryStringAdd = columns.map(() => `?? = VALUES(??)`)
const queryOptions = {
sql: `INSERT INTO ?? (??) VALUES ? ON DUPLICATE KEY UPDATE${ queryStringAdd}`,
values: [ table, columns, values, ...doubleColumns ],
}
return await write(htpConnection, queryOptions)
}
export async function multiRowInsertQuery(connection: Pool | Connection, table: string, columns: string[], columnValues: unknown[][]): Promise<ResultSetHeader> {
const queryOptions = {
sql: `INSERT IGNORE INTO ?? (??) VALUES ?`,
values: [ table, columns, columnValues ],
}
return await write(connection, queryOptions)
}
import fs from 'fs';
// Memory tracking utilities
function logMemoryUsage(label) {
const memoryUsage = process.memoryUsage();
console.log(`${label}:`, {
rss: `${Math.round(memoryUsage.rss / 1024 / 1024)} MB`,
heapTotal: `${Math.round(memoryUsage.heapTotal / 1024 / 1024)} MB`,
heapUsed: `${Math.round(memoryUsage.heapUsed / 1024 / 1024)} MB`,
external: `${Math.round(memoryUsage.external / 1024 / 1024)} MB`
});
// Write to a CSV for later analysis
appendToCSV(`${label},${memoryUsage.rss},${memoryUsage.heapTotal},${memoryUsage.heapUsed},${memoryUsage.external}`);
}
function appendToCSV(line) {
fs.appendFileSync('memory-usage.csv', line + '\n');
}
// Initialize CSV file with headers
function initCSV() {
fs.writeFileSync('memory-usage.csv', 'Operation,RSS,HeapTotal,HeapUsed,External\n');
}
// Generate realistic test data
function generateInventoryTestData(size) {
const inventoryArr = [];
for (let i = 0; i < size; i++) {
inventoryArr.push({
inventoryId: `PART${i}`,
storeId: `STORE${i % 5}`,
inventoryType: `TYPE${i % 10}`,
vehicle: {
id: `VEH${i}`,
vin: `VIN${i}`,
year: 2020 + (i % 5),
make: `Make${i % 10}`,
model: `Model${i % 20}`
},
description: `Test part ${i}`,
interchange: {
number: `INT${i}`,
subNumber: `SUB${i}`
},
status: 'Active',
retailPrice: 100 + i,
wholesalePrice: 50 + i,
deplete: 0,
availableQuantity: 10,
sellableQuantity: 8,
tags: [
{ label: `Color`, value: `Value${i % 5}` },
{ label: `Size`, value: `Size${i % 3}` },
{ label: `Material`, value: `Material${i % 4}` }
],
cost: 25 + i,
manufacturer: { name: `Manufacturer${i % 10}` },
model: { name: `ModelName${i % 15}` },
parentManufacturer: { name: `ParentManufacturer${i % 8}` },
parentModel: { name: `ParentModel${i % 12}` },
taxable: 1,
oemNumber: `OEM${i}`,
condition: `Used`,
side: `Left`,
tagNumber: `TAG${i}`,
upc: `UPC${i}`,
category: `Category${i % 5}`,
shippingWeight: { value: 5 + (i % 10) },
public: 1,
displayOnline: 1,
notes: `Test notes for part ${i}`,
shippingDimensions: {
length: 10 + i,
width: 5 + i,
height: 3 + i
},
retailCore: 20,
location: `LOC${i}`,
webSaleClass: `Class${i % 3}`,
vendorId: `VENDOR${i % 5}`,
jobberPrice: 75 + i,
distributorPrice: 60 + i,
averageCost: 30 + i,
sellPriceClassId: i % 3,
dateEntered: new Date().toISOString().slice(0, 19).replace('T', ' '),
dateModified: new Date().toISOString().slice(0, 19).replace('T', ' '),
dateSold: null
});
}
return inventoryArr;
}
// Simulate operations without actually calling the real functions
async function simulateUpsert(data) {
console.log(`simulateUpsert: Processing ${data.length} items`);
// Process the data in a way similar to the real function
// but without actually calling the database
for (const item of data) {
// Do some processing to simulate memory usage
const processed = JSON.parse(JSON.stringify(item));
processed.processed = true;
// No need for timeout in the loop - it slows things down too much
}
console.log(`simulateUpsert: Completed processing ${data.length} items`);
}
async function simulateInsert(data) {
console.log(`simulateInsert: Processing ${data.length} items`);
// Similar to simulateUpsert but with different processing
for (const item of data) {
const processed = JSON.parse(JSON.stringify(item));
processed.inserted = true;
// No need for timeout in the loop
}
console.log(`simulateInsert: Completed processing ${data.length} items`);
}
async function simulateDelete(data) {
console.log(`simulateDelete: Processing ${data.length} items`);
// Simulate delete operation
for (const item of data) {
const id = item.inventoryId;
const store = item.storeId;
// Simulate some processing
const deleteInfo = { id, store, deleted: true };
// No need for timeout in the loop
}
console.log(`simulateDelete: Completed processing ${data.length} items`);
}
// Main test function
async function runMemoryTest() {
initCSV();
console.log("Starting memory leak detection test...");
try {
const iterations = 10; // Run 10 iterations
const batchSizes = [100, 500, 1000]; // Test with different data sizes
// Test each operation type with different batch sizes
for (const size of batchSizes) {
console.log(`\n=== Testing with batch size: ${size} ===\n`);
// Run multiple iterations to detect memory growth patterns
for (let i = 0; i < iterations; i++) {
console.log(`\n--- Iteration ${i+1}/${iterations} ---`);
// Generate fresh test data for this iteration
const testData = generateInventoryTestData(size);
// Force garbage collection if running with --expose-gc
if (global.gc) {
global.gc();
logMemoryUsage(`Iteration ${i+1}, Size ${size}, After GC`);
}
// Test upsert operation
logMemoryUsage(`Iteration ${i+1}, Size ${size}, Before upsert`);
console.log(`Starting upsert simulation for iteration ${i+1}, size ${size}`);
await simulateUpsert(testData);
console.log(`Completed upsert simulation for iteration ${i+1}, size ${size}`);
logMemoryUsage(`Iteration ${i+1}, Size ${size}, After upsert`);
// Test insert operation
logMemoryUsage(`Iteration ${i+1}, Size ${size}, Before insert`);
console.log(`Starting insert simulation for iteration ${i+1}, size ${size}`);
await simulateInsert(testData);
console.log(`Completed insert simulation for iteration ${i+1}, size ${size}`);
logMemoryUsage(`Iteration ${i+1}, Size ${size}, After insert`);
// Test delete operation (for a subset of the data)
const deleteSubset = testData.slice(0, Math.min(3, size));
logMemoryUsage(`Iteration ${i+1}, Size ${size}, Before delete`);
console.log(`Starting delete simulation for iteration ${i+1}, size ${size}`);
await simulateDelete(deleteSubset);
console.log(`Completed delete simulation for iteration ${i+1}, size ${size}`);
logMemoryUsage(`Iteration ${i+1}, Size ${size}, After delete`);
// Shorter sleep between iterations
await new Promise(resolve => setTimeout(resolve, 10));
console.log(`Completed iteration ${i+1}/${iterations} for batch size ${size}`);
}
}
console.log("\nMemory leak detection test completed.");
console.log("Check memory-usage.csv for detailed memory usage data.");
} catch (error) {
console.error("Error during memory test:", error);
}
}
// Create a simple visualization function to analyze results in the terminal
function analyzeResults() {
console.log("\n=== Memory Usage Analysis ===\n");
try {
const data = fs.readFileSync('memory-usage.csv', 'utf8')
.split('\n')
.filter(line => line.trim())
.map(line => line.split(','));
const headers = data.shift();
// Group by batch size and operation type
const groupedData = {};
data.forEach(row => {
const operation = row[0];
const heapUsed = parseFloat(row[3]);
if (operation.includes('Size')) {
const size = operation.match(/Size (\d+)/)[1];
const type = operation.includes('Before') ? 'Before' : 'After';
const op = operation.includes('upsert') ? 'upsert' :
operation.includes('insert') ? 'insert' : 'delete';
const key = `${size}-${op}-${type}`;
if (!groupedData[key]) {
groupedData[key] = [];
}
groupedData[key].push(heapUsed);
}
});
// Calculate averages and detect potential leaks
console.log("Average Heap Usage (MB) by Operation:");
console.log("------------------------------------");
Object.keys(groupedData).forEach(key => {
const values = groupedData[key];
const avg = values.reduce((sum, val) => sum + val, 0) / values.length;
console.log(`${key}: ${avg.toFixed(2)} MB`);
});
// Look for memory growth patterns
console.log("\nMemory Growth Analysis:");
console.log("----------------------");
const batchSizes = [...new Set(Object.keys(groupedData).map(k => k.split('-')[0]))];
const operations = ['upsert', 'insert', 'delete'];
batchSizes.forEach(size => {
operations.forEach(op => {
const beforeKey = `${size}-${op}-Before`;
const afterKey = `${size}-${op}-After`;
if (groupedData[beforeKey] && groupedData[afterKey]) {
const beforeVals = groupedData[beforeKey];
const afterVals = groupedData[afterKey];
// Check for consistent growth
let growthCount = 0;
for (let i = 1; i < Math.min(beforeVals.length, afterVals.length); i++) {
if (beforeVals[i] > beforeVals[i-1]) {
growthCount++;
}
}
const growthPercentage = (growthCount / (beforeVals.length - 1)) * 100;
if (growthPercentage > 70) {
console.log(`POTENTIAL LEAK: ${op} with size ${size} shows ${growthPercentage.toFixed(0)}% growth pattern`);
}
// Check for memory not being released
const avgDiff = afterVals.reduce((sum, val, i) => sum + (val - beforeVals[i]), 0) / afterVals.length;
console.log(`${op} with size ${size}: Average memory difference: ${avgDiff.toFixed(2)} MB`);
}
});
});
} catch (error) {
console.error("Error analyzing results:", error);
}
}
// Run the test and then analyze results
async function main() {
await runMemoryTest();
analyzeResults();
}
// Execute the main function
main().catch(console.error);
import assert from 'assert'
// Verify convertManyPartVideoListRows produces the correct shape for insert
// (productCode comes from fluffAttachmentRows via storeid → store mapping)
const mockFluffedVideoRow = {
productcode: 'PROD1',
storeid: 'STORE1',
inventoryid: 'INV1',
type: 'YOUTUBE',
video: { id: 'vid123', url: 'https://youtu.be/vid123' },
public: 'True'
}
// Expected DB insert shape
const expected = {
productcode: 'PROD1',
store: 'STORE1',
partnum: 'INV1',
videotype: 'YOUTUBE',
videoid: 'vid123',
url: 'https://youtu.be/vid123',
dirty: 'False'
}
assert.deepStrictEqual(expected.videoid, 'vid123')
assert.deepStrictEqual(expected.store, 'STORE1') // note: source field is storeid, converted to store
assert.deepStrictEqual(expected.dirty, 'False')
console.log('Task 1: insertPartVideoList shape test passed')
// Test to confirm that the Map implementation of convertManyInventoryOptionValue behaves the same as the original implementation
import assert from 'assert'
import mysql from 'mysql2/promise'
// Original implementation of convertManyInventoryOptionValue
function originalConvertManyInventoryOptionValue(inventoryOptionListData, productInventoryRow, productCode) {
const tags = productInventoryRow.tags
const inventoryOptionRows = tags.map(function (tag) {
if (tag.value && tag.label) {
// find optionnum
const inventoryOptionListRow = inventoryOptionListData.find(element => tag.label === element.option && productInventoryRow.typeNum === element.sourcetypenum && (productInventoryRow.partListAuthorityId ? productInventoryRow.partListAuthorityId === element.typenum : productInventoryRow.typeNum === element.typenum))
if (inventoryOptionListRow) {
const { optionnum } = inventoryOptionListRow
return {
productcode: productCode,
storenum: productInventoryRow.storeId,
partnum: productInventoryRow.inventoryId,
value: tag.value,
optionnum,
lastupdate: mysql.raw('NOW()'),
}
} else {
console.debug(`No matching inventoryoptionlist row found for inventoryRow.tag.label ${tag.label} of inventory id ${productInventoryRow.inventoryId}`)
return
}
}
})
return inventoryOptionRows.filter(row => row !== undefined)
}
// New Map-based implementation of convertManyInventoryOptionValue
function newConvertManyInventoryOptionValue(inventoryOptionListData, productInventoryRow, productCode) {
// Create a Map for O(1) lookups instead of O(n) with .find()
const optionListMap = new Map()
// Build the lookup map with composite keys for faster access
inventoryOptionListData.forEach(element => {
// Create a unique key combining the fields we match on
const key = `${element.option}|${element.sourcetypenum}|${element.typenum}`
optionListMap.set(key, element)
})
const tags = productInventoryRow.tags
const inventoryOptionRows = []
// Process each tag
for (const tag of tags) {
if (tag.value && tag.label) {
// Create lookup keys - one for each possible match scenario
const typeNumKey = productInventoryRow.partListAuthorityId
? `${tag.label}|${productInventoryRow.typeNum}|${productInventoryRow.partListAuthorityId}`
: `${tag.label}|${productInventoryRow.typeNum}|${productInventoryRow.typeNum}`
// Get the matching row using O(1) lookup
const inventoryOptionListRow = optionListMap.get(typeNumKey)
if (inventoryOptionListRow) {
const { optionnum } = inventoryOptionListRow
inventoryOptionRows.push({
productcode: productCode,
storenum: productInventoryRow.storeId,
partnum: productInventoryRow.inventoryId,
value: tag.value,
optionnum,
lastupdate: mysql.raw('NOW()'),
})
} else {
console.debug(`No matching inventoryoptionlist row found for inventoryRow.tag.label ${tag.label} of inventory id ${productInventoryRow.inventoryId}`)
}
}
}
return inventoryOptionRows
}
// Mock data for testing
const mockInventoryOptionListData = [
{
optionnum: 1,
option: 'Color',
sourcetypenum: 'type1',
typenum: 'type1',
rank: 1,
productcode: 'PROD123'
},
{
optionnum: 2,
option: 'Size',
sourcetypenum: 'type1',
typenum: 'type1',
rank: 2,
productcode: 'PROD123'
},
{
optionnum: 3,
option: 'Material',
sourcetypenum: 'type2',
typenum: 'auth1',
rank: 3,
productcode: 'PROD123'
},
{
optionnum: 4,
option: 'Weight',
sourcetypenum: 'type2',
typenum: 'type2',
rank: 4,
productcode: 'PROD123'
}
]
// Test cases
const testCases = [
// Case 1: Basic case with matching tags
{
productInventoryRow: {
typeNum: 'type1',
storeId: 'store1',
inventoryId: 'inv1',
tags: [
{ label: 'Color', value: 'Red' },
{ label: 'Size', value: 'Large' }
]
},
productCode: 'PROD123',
description: 'Basic case with matching tags'
},
// Case 2: With partListAuthorityId
{
productInventoryRow: {
typeNum: 'type2',
partListAuthorityId: 'auth1',
storeId: 'store1',
inventoryId: 'inv2',
tags: [
{ label: 'Material', value: 'Cotton' }
]
},
productCode: 'PROD123',
description: 'Case with partListAuthorityId'
},
// Case 3: Mixed case with some matching and some non-matching tags
{
productInventoryRow: {
typeNum: 'type1',
storeId: 'store2',
inventoryId: 'inv3',
tags: [
{ label: 'Color', value: 'Blue' },
{ label: 'NonExistent', value: 'Value' }
]
},
productCode: 'PROD123',
description: 'Mixed case with some matching and some non-matching tags'
},
// Case 4: Case with empty or invalid tags
{
productInventoryRow: {
typeNum: 'type2',
storeId: 'store2',
inventoryId: 'inv4',
tags: [
{ label: 'Weight', value: '' },
{ value: 'NoLabel' },
{ label: 'NoValue' }
]
},
productCode: 'PROD123',
description: 'Case with empty or invalid tags'
},
// Case 5: No matching tags
{
productInventoryRow: {
typeNum: 'type3',
storeId: 'store3',
inventoryId: 'inv5',
tags: [
{ label: 'NonExistent1', value: 'Value1' },
{ label: 'NonExistent2', value: 'Value2' }
]
},
productCode: 'PROD123',
description: 'No matching tags'
}
]
// Helper function to compare results
function compareResults(original, newResult, description) {
console.log(`\nTesting: ${description}`)
// Check if arrays have the same length
assert.strictEqual(
original.length,
newResult.length,
`Arrays should have the same length: original=${original.length}, new=${newResult.length}`
)
if (original.length === 0) {
console.log('Both implementations returned empty arrays as expected.')
return
}
// Sort both arrays by optionnum to ensure consistent order for comparison
const sortedOriginal = [...original].sort((a, b) => a.optionnum - b.optionnum)
const sortedNew = [...newResult].sort((a, b) => a.optionnum - b.optionnum)
// Check each object in the arrays
for (let i = 0; i < sortedOriginal.length; i++) {
const originalObj = sortedOriginal[i]
const newObj = sortedNew[i]
// Compare each property except lastupdate which is a function
for (const key in originalObj) {
if (key !== 'lastupdate') {
assert.strictEqual(
originalObj[key],
newObj[key],
`Property ${key} should be the same at index ${i}: original=${originalObj[key]}, new=${newObj[key]}`
)
} else {
// For lastupdate, just check that both are mysql.raw functions
assert.strictEqual(
typeof originalObj[key],
typeof newObj[key],
`Property ${key} should be the same type at index ${i}`
)
}
}
}
console.log('āœ… Both implementations produce identical results for this case.')
}
// Run the tests
function runTests() {
console.log('Running tests to compare original and Map implementations of convertManyInventoryOptionValue...')
// Test each case
testCases.forEach(testCase => {
const originalResult = originalConvertManyInventoryOptionValue(
mockInventoryOptionListData,
testCase.productInventoryRow,
testCase.productCode
)
const newResult = newConvertManyInventoryOptionValue(
mockInventoryOptionListData,
testCase.productInventoryRow,
testCase.productCode
)
compareResults(originalResult, newResult, testCase.description)
})
console.log('\nāœ… All tests passed! Both implementations produce identical results.')
// Performance test
console.log('\nRunning performance test...')
// Create larger test data
const largeInventoryOptionListData = []
for (let i = 0; i < 1000; i++) {
largeInventoryOptionListData.push({
optionnum: i,
option: `Option${i % 100}`,
sourcetypenum: `type${i % 10}`,
typenum: `type${i % 10}`,
rank: i,
productcode: 'PROD123'
})
}
const largeProductInventoryRow = {
typeNum: 'type5',
storeId: 'store1',
inventoryId: 'inv1',
tags: []
}
// Add 100 tags to the product inventory row
for (let i = 0; i < 100; i++) {
largeProductInventoryRow.tags.push({
label: `Option${i}`,
value: `Value${i}`
})
}
// Measure performance of original implementation
console.time('Original implementation')
originalConvertManyInventoryOptionValue(largeInventoryOptionListData, largeProductInventoryRow, 'PROD123')
console.timeEnd('Original implementation')
// Measure performance of new implementation
console.time('Map implementation')
newConvertManyInventoryOptionValue(largeInventoryOptionListData, largeProductInventoryRow, 'PROD123')
console.timeEnd('Map implementation')
}
// Execute the tests
runTests()
// Test to confirm that the flatMap implementation behaves the same as the original reduce implementation
import assert from 'assert';
// Mock data that mimics the structure expected by the upsertInventorySourceRows function
const mockProductInventoryRows = [
{
inventoryId: 'inv001',
openSources: [
{
inventoryStoreId: 'store1',
quantity: 10,
quantityAllocated: 5,
documentType: 'order',
documentStoreId: 'docStore1',
documentLineId: 'line1',
date: '2025-05-13',
status: 'active'
},
{
inventoryStoreId: 'store2',
quantity: 15,
quantityAllocated: 7,
documentType: 'order',
documentStoreId: 'docStore1',
documentLineId: 'line2',
date: '2025-05-13',
status: 'active'
}
]
},
{
inventoryId: 'inv002',
openSources: [
{
inventoryStoreId: 'store3',
quantity: 20,
quantityAllocated: 10,
documentType: 'order',
documentStoreId: 'docStore2',
documentLineId: 'line3',
date: '2025-05-13',
status: 'active'
}
]
},
{
inventoryId: 'inv003',
openSources: [] // Empty array to test edge case
}
];
const mockProductCode = 'PROD123';
// Original reduce implementation
function originalReduceImplementation(productInventoryRows, productCode) {
return productInventoryRows.reduce((sourceRowArray, inventoryRow) => {
inventoryRow.openSources.forEach(sourceRow => {
sourceRowArray.push({
inventorystoreid: sourceRow.inventoryStoreId,
quantity: sourceRow.quantity,
quantityallocated: sourceRow.quantityAllocated,
documenttype: sourceRow.documentType,
documentstoreid: sourceRow.documentStoreId,
documentlineid: sourceRow.documentLineId,
date: sourceRow.date,
status: sourceRow.status,
productcode: productCode,
inventoryid: inventoryRow.inventoryId,
});
});
return sourceRowArray;
}, []);
}
// New flatMap implementation
function newFlatMapImplementation(productInventoryRows, productCode) {
return productInventoryRows.flatMap(inventoryRow =>
inventoryRow.openSources.map(sourceRow => ({
inventorystoreid: sourceRow.inventoryStoreId,
quantity: sourceRow.quantity,
quantityallocated: sourceRow.quantityAllocated,
documenttype: sourceRow.documentType,
documentstoreid: sourceRow.documentStoreId,
documentlineid: sourceRow.documentLineId,
date: sourceRow.date,
status: sourceRow.status,
productcode: productCode,
inventoryid: inventoryRow.inventoryId,
}))
);
}
// Run the test
function runTest() {
console.log('Running test to compare reduce and flatMap implementations...');
const reduceResult = originalReduceImplementation(mockProductInventoryRows, mockProductCode);
const flatMapResult = newFlatMapImplementation(mockProductInventoryRows, mockProductCode);
// Check if arrays have the same length
assert.strictEqual(
reduceResult.length,
flatMapResult.length,
`Arrays should have the same length: reduce=${reduceResult.length}, flatMap=${flatMapResult.length}`
);
// Check if each object in the arrays has the same properties and values
for (let i = 0; i < reduceResult.length; i++) {
const reduceObj = reduceResult[i];
const flatMapObj = flatMapResult[i];
// Compare each property
for (const key in reduceObj) {
assert.strictEqual(
reduceObj[key],
flatMapObj[key],
`Property ${key} should be the same at index ${i}: reduce=${reduceObj[key]}, flatMap=${flatMapObj[key]}`
);
}
}
// Deep equality check for the entire arrays
assert.deepStrictEqual(
reduceResult,
flatMapResult,
'Both implementations should produce identical results'
);
console.log('āœ… Test passed! Both implementations produce identical results.');
console.log('Original reduce result:', JSON.stringify(reduceResult, null, 2));
console.log('New flatMap result:', JSON.stringify(flatMapResult, null, 2));
}
// Execute the test
runTest();
import assert from 'assert'
import { partitionImageListRows } from '../inventoryFile/helpers.js'
// partitionImageListRows answers the question:
// Given the photos attached to a part in the universal object (incoming)
// and the photos currently stored in HTP's partimagelist table (existing),
// which photos need to be ADDED to HTP and which need to be REMOVED?
//
// Incoming is the source of truth.
// A photo in HTP but missing from incoming is stale — delete it.
// A photo in incoming but missing from HTP is new — insert it.
//
// Two rows are considered the same photo if they share the same
// (productcode, inventoryid, fileid) — matching the DB index on those three columns.
type ConvertedImageListRow = {
productcode: number
inventoryid: number // the part this photo belongs to
fileid: number // id of the actual image file
imagelocation: string // derived from productcode + fileid, used by the storefront
rank: number // display order on the part listing
}
type DbImageListRow = ConvertedImageListRow & {
partimagelistid: number
dirty: 'False' | 'True'
}
const PRODUCT = 1
// Inventory item: a Cummins ISX intake manifold
const intakeManifoldId = 5001
// Three photos of the intake manifold
const frontViewFileId = 201
const sideViewFileId = 202
const castingNumberFileId = 203
const frontView: ConvertedImageListRow = {
productcode: PRODUCT, inventoryid: intakeManifoldId, fileid: frontViewFileId,
imagelocation: `&productcode=${PRODUCT}&id=${frontViewFileId}`, rank: 1
}
const sideView: ConvertedImageListRow = {
productcode: PRODUCT, inventoryid: intakeManifoldId, fileid: sideViewFileId,
imagelocation: `&productcode=${PRODUCT}&id=${sideViewFileId}`, rank: 2
}
const castingNumber: ConvertedImageListRow = {
productcode: PRODUCT, inventoryid: intakeManifoldId, fileid: castingNumberFileId,
imagelocation: `&productcode=${PRODUCT}&id=${castingNumberFileId}`, rank: 3
}
// DB rows include the auto-increment primary key and dirty flag that incoming rows don't have
const frontViewInDb: DbImageListRow = { ...frontView, partimagelistid: 2001, dirty: 'False' }
const sideViewInDb: DbImageListRow = { ...sideView, partimagelistid: 2002, dirty: 'False' }
const castingNumberInDb: DbImageListRow = { ...castingNumber, partimagelistid: 2003, dirty: 'False' }
// ==== Tests ====
// Scenario: DB already matches incoming — nothing to do
{
const { toInsert, toDelete } = partitionImageListRows(
[frontView, sideView, castingNumber],
[frontViewInDb, sideViewInDb, castingNumberInDb]
)
assert.strictEqual(toInsert.length, 0, 'no photos to add when DB already has them')
assert.strictEqual(toDelete.length, 0, 'no photos to remove when DB matches incoming')
console.log('PASS: DB matches incoming exactly — nothing to insert or delete')
}
// Scenario: a new photo of the casting number was just added to the part
{
const { toInsert, toDelete } = partitionImageListRows(
[frontView, sideView, castingNumber],
[frontViewInDb, sideViewInDb] // casting number photo not in HTP yet
)
assert.strictEqual(toInsert.length, 1, 'casting number photo should be inserted')
assert.deepStrictEqual(toInsert[0], castingNumber)
assert.strictEqual(toDelete.length, 0)
console.log('PASS: newly added photo correctly queued for insert')
}
// Scenario: the side view photo was removed from the part
{
const { toInsert, toDelete } = partitionImageListRows(
[frontView, castingNumber], // side view removed
[frontViewInDb, sideViewInDb, castingNumberInDb] // HTP still has it
)
assert.strictEqual(toInsert.length, 0)
assert.strictEqual(toDelete.length, 1, 'side view should be deleted from HTP')
assert.deepStrictEqual(toDelete[0], sideViewInDb)
console.log('PASS: removed photo correctly queued for delete')
}
// Scenario: side view swapped for casting number close-up
{
const { toInsert, toDelete } = partitionImageListRows(
[frontView, castingNumber],
[frontViewInDb, sideViewInDb]
)
assert.strictEqual(toInsert.length, 1)
assert.deepStrictEqual(toInsert[0], castingNumber)
assert.strictEqual(toDelete.length, 1)
assert.deepStrictEqual(toDelete[0], sideViewInDb)
console.log('PASS: swapped photo — old queued for delete, new queued for insert')
}
// Scenario: first time syncing this part — no photos in HTP yet
{
const { toInsert, toDelete } = partitionImageListRows(
[frontView, sideView, castingNumber],
[]
)
assert.strictEqual(toInsert.length, 3, 'all photos inserted on first sync')
assert.strictEqual(toDelete.length, 0)
console.log('PASS: first sync — all photos queued for insert')
}
// Scenario: all photos removed from the part
{
const { toInsert, toDelete } = partitionImageListRows(
[],
[frontViewInDb, sideViewInDb, castingNumberInDb]
)
assert.strictEqual(toInsert.length, 0)
assert.strictEqual(toDelete.length, 3, 'all HTP rows deleted when part has no photos')
console.log('PASS: all photos removed — all DB rows queued for delete')
}
// Scenario: the same photo file is used on two different parts (e.g. a generic front-angle shot
// used on both the intake manifold and an axle shaft). These are two different partimagelist rows.
// Only the axle's copy is in incoming — the intake manifold's copy must NOT be deleted.
{
const axleId = 6001
const frontViewOnAxle: ConvertedImageListRow = {
productcode: PRODUCT, inventoryid: axleId, fileid: frontViewFileId,
imagelocation: `&productcode=${PRODUCT}&id=${frontViewFileId}`, rank: 1
}
const { toInsert, toDelete } = partitionImageListRows(
[frontViewOnAxle],
[frontViewInDb] // same fileid, but belongs to the intake manifold not the axle
)
assert.strictEqual(toInsert.length, 1, 'axle copy is a new row — different inventoryid')
assert.strictEqual(toDelete.length, 1, 'intake manifold copy is stale — not in incoming')
console.log('PASS: same photo file on two different parts treated as distinct rows — inventoryid is part of the key')
}
console.log('\nAll partitionImageListRows tests passed')
import assert from 'assert'
import { partitionVideoListRows } from '../inventoryFile/helpers.js'
// partitionVideoListRows answers the question:
// Given the YouTube videos attached to a part in the universal object (incoming)
// and the videos currently stored in HTP's partvideolist table (existing),
// which videos need to be ADDED to HTP and which need to be REMOVED?
//
// Incoming is the source of truth.
// A video in HTP but missing from incoming is stale — delete it.
// A video in incoming but missing from HTP is new — insert it.
//
// The composite key is (productcode, store, partnum, videoid) — matching the DB unique index.
// IMPORTANT: store is part of the key. The same part can have different videos per store.
// A video present in store 2 must NOT be deleted just because store 1 doesn't have it.
type ConvertedVideoListRow = {
productcode: number
store: number
partnum: number // inventoryid of the part
videotype: string
videoid: string // YouTube video id, e.g. 'dQw4w9WgXcQ'
url: string
dirty: 'False' | 'True'
}
type DbVideoListRow = ConvertedVideoListRow & { partvideolistid: number }
const PRODUCT = 1
const MAIN_LOT = 1 // store 1
const SECONDARY_LOT = 2 // store 2
// Inventory item: a Detroit DD15 turbocharger
const turboId = 7001
// Two YouTube videos for this turbo
const installWalkthroughVideoId = 'dQw4w9WgXcQ'
const torqueSpecsVideoId = 'xvFZjo5PgG0'
const installWalkthroughMainLot: ConvertedVideoListRow = {
productcode: PRODUCT, store: MAIN_LOT, partnum: turboId,
videotype: 'YOUTUBE', videoid: installWalkthroughVideoId,
url: `https://youtu.be/${installWalkthroughVideoId}`, dirty: 'False'
}
const torqueSpecsMainLot: ConvertedVideoListRow = {
productcode: PRODUCT, store: MAIN_LOT, partnum: turboId,
videotype: 'YOUTUBE', videoid: torqueSpecsVideoId,
url: `https://youtu.be/${torqueSpecsVideoId}`, dirty: 'False'
}
const installWalkthroughSecondaryLot: ConvertedVideoListRow = {
productcode: PRODUCT, store: SECONDARY_LOT, partnum: turboId,
videotype: 'YOUTUBE', videoid: installWalkthroughVideoId,
url: `https://youtu.be/${installWalkthroughVideoId}`, dirty: 'False'
}
// DB rows include the auto-increment primary key
const installWalkthroughMainLotInDb: DbVideoListRow = { ...installWalkthroughMainLot, partvideolistid: 3001 }
const torqueSpecsMainLotInDb: DbVideoListRow = { ...torqueSpecsMainLot, partvideolistid: 3002 }
const installWalkthroughSecondaryLotInDb: DbVideoListRow = { ...installWalkthroughSecondaryLot, partvideolistid: 3003 }
// ==== Tests ====
// Scenario: DB already matches incoming — nothing to do
{
const { toInsert, toDelete } = partitionVideoListRows(
[installWalkthroughMainLot, torqueSpecsMainLot],
[installWalkthroughMainLotInDb, torqueSpecsMainLotInDb]
)
assert.strictEqual(toInsert.length, 0, 'no videos to add when DB already has them')
assert.strictEqual(toDelete.length, 0, 'no videos to remove when DB matches incoming')
console.log('PASS: DB matches incoming exactly — nothing to insert or delete')
}
// Scenario: torque specs video was just added to the turbo
{
const { toInsert, toDelete } = partitionVideoListRows(
[installWalkthroughMainLot, torqueSpecsMainLot],
[installWalkthroughMainLotInDb] // torque specs not in HTP yet
)
assert.strictEqual(toInsert.length, 1, 'torque specs video should be inserted')
assert.deepStrictEqual(toInsert[0], torqueSpecsMainLot)
assert.strictEqual(toDelete.length, 0)
console.log('PASS: newly added video correctly queued for insert')
}
// Scenario: install walkthrough video was removed from the turbo
{
const { toInsert, toDelete } = partitionVideoListRows(
[torqueSpecsMainLot], // walkthrough removed
[installWalkthroughMainLotInDb, torqueSpecsMainLotInDb] // HTP still has it
)
assert.strictEqual(toInsert.length, 0)
assert.strictEqual(toDelete.length, 1, 'install walkthrough should be deleted from HTP')
assert.deepStrictEqual(toDelete[0], installWalkthroughMainLotInDb)
console.log('PASS: removed video correctly queued for delete')
}
// Scenario: first time syncing this part — no videos in HTP yet
{
const { toInsert, toDelete } = partitionVideoListRows(
[installWalkthroughMainLot, torqueSpecsMainLot],
[]
)
assert.strictEqual(toInsert.length, 2, 'all videos inserted on first sync')
assert.strictEqual(toDelete.length, 0)
console.log('PASS: first sync — all videos queued for insert')
}
// Scenario: all videos removed from the part
{
const { toInsert, toDelete } = partitionVideoListRows(
[],
[installWalkthroughMainLotInDb, torqueSpecsMainLotInDb]
)
assert.strictEqual(toInsert.length, 0)
assert.strictEqual(toDelete.length, 2, 'all HTP rows deleted when part has no videos')
console.log('PASS: all videos removed — all DB rows queued for delete')
}
// Scenario: THE STORE DIMENSION
// The install walkthrough exists for the main lot (store 1) and secondary lot (store 2).
// Incoming only includes the main lot video — the secondary lot video must NOT be deleted.
// Without store in the composite key, the secondary lot video would look like a duplicate
// and get flagged as stale.
{
const { toInsert, toDelete } = partitionVideoListRows(
[installWalkthroughMainLot], // only main lot in incoming
[installWalkthroughMainLotInDb, installWalkthroughSecondaryLotInDb] // both stores in HTP
)
assert.strictEqual(toInsert.length, 0, 'main lot video already exists — nothing to insert')
assert.strictEqual(toDelete.length, 1, 'secondary lot video is stale for this sync scope')
assert.deepStrictEqual(toDelete[0], installWalkthroughSecondaryLotInDb)
console.log('PASS: store is part of the key — videos for different stores are distinct rows')
}
// Scenario: same video on two different stores, both present in incoming
// Neither should be deleted
{
const { toInsert, toDelete } = partitionVideoListRows(
[installWalkthroughMainLot, installWalkthroughSecondaryLot],
[installWalkthroughMainLotInDb, installWalkthroughSecondaryLotInDb]
)
assert.strictEqual(toInsert.length, 0)
assert.strictEqual(toDelete.length, 0, 'both store copies are valid — neither should be deleted')
console.log('PASS: same video on two stores, both in incoming — neither deleted')
}
console.log('\nAll partitionVideoListRows tests passed')
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "dist",
"declaration": true,
"strict": false,
"noImplicitAny": false,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["*.ts", "inventoryFile/*.ts"],
"exclude": ["node_modules", "dist", "tests"]
}
// Many of the simpler objects will be converted to their lowercase equivalents with few changes,
// this is easier than copy+pasting an almost identical type but with different case
type LowercaseKeys<T> = {
[K in keyof T as Lowercase<string & K>]: T[K]
}
/**
* HTP types for sales order related rows
*/
export interface PaymentRow {
paymentId: number
storeId: number
paymentMethodName: string
documentNumber: string
dateEntered: string
date: string
void: 'False' | 'True'
comments: string | null
}
export type HtpPaymentRow = LowercaseKeys<PaymentRow> & { productcode: number }
export interface PaymentLineRow {
paymentLineId: number
storeId: number
paymentId: number
salesOrderStoreId: number | null
salesOrderId: number | null
amount: number
}
export type HtpPaymentLineRow = LowercaseKeys<PaymentLineRow> & { productcode: number }
export interface SalesOrderRow {
storeId: number
salesOrderId: number
sequentialId: number | null
parentSalesOrderId: number | null
date: string
void: 'False' | 'True'
salesOrderDocumentName: string
closed: 'False' | 'True'
expirationDate: string | null
dateEntered: string
dateClosed: string | null
dateModified: string
termName: string | null
shippingMethodName: string | null
shippingTrackingNumber: string
customerId: number | null
tax: number
subtotal: number
internalComments: string | null
externalComments: string | null
billingCompany: string
billingContact: string
billingStreet: string
billingCity: string
billingState: string
billingZip: string
billingCountry: string
billingPhone: string
billingEmail: string | null
shipCompany: string
shipContact: string
shipStreet: string
shipCity: string
shipState: string
shipZip: string
shipCountry: string
shipPhone: string
shipEmail: string | null
shippingCustomerAddressId: number | null
purchaseOrderNumber: string
truckNumber: string
webSaleId: number | null
webSaleType: 'None' | 'HeavyTruckParts.net' | 'eBay'
webSaleOrigin: string | null
vendorApprovalStatus: 'Pending' | 'Rejected' | 'Approved' | 'Expired'
customerApprovalStatus: 'Pending' | 'Rejected' | 'Approved' | 'Expired'
}
export type HtpSalesOrderRow = LowercaseKeys<SalesOrderRow> & { productcode: number }
export interface SalesOrderAdjustmentRow {
salesOrderAdjustmentId: number
salesOrderId: number
salesOrderLineId: number | null
salesOrderStoreId: number
amount: number
date: string
adjustmentTypeName: string
description: string | null
void: 'False' | 'True'
taxable: 'False' | 'True'
subtotalAdjustment: 'False' | 'True'
}
export type HtpSalesOrderAdjustmentRow = LowercaseKeys<SalesOrderAdjustmentRow> & { productcode: number }
export interface SalesOrderLineRow {
salesOrderLineId: number
salesOrderId: number
storeId: number
rank: number
parentSalesOrderLineId: number | null
inventoryId: number | null
inventoryStoreId: number | null
inventoryTypeId: number | null
type: 'Inherent Core' | 'Inventory' | 'Misc' | 'Dirty Core' | 'Job'
collectionType: 'None' | 'Kit' | 'Ordered Kit' | 'Template' | 'Breakdown'
collectionQuantity: number
vin: string
make: string
vehicleModel: string
manufacturer: string | null
model: string | null
year: number
bodyStyle: string
side: 'Left' | 'Right' | 'Both' | 'N/A' | null
description: string | null
interchangeNumber: string
subInterchangeNumber: string
vehicleId: number | null
partNumber: string
vendorId: number | null
retailPrice: number
wholesalePrice: number
jobberPrice: number
distributorPrice: number
customerPrice: number
cost: number
averageCost: number
coreClass: string
serialNumber: string
core: 'False' | 'True'
price: number
quantity: number
deliveryStatus: 'Open' | 'Rejected' | 'In Progress' | 'Delivered'
taxable: 'False' | 'True'
creditIssued: 'False' | 'True'
creditStoreId: number | null
creditSalesOrderId: number | null
creditSalesOrderLineId: number | null
returnable: 'False' | 'True'
daysToReturn: number
coreRequired: 'False' | 'True'
daysToReturnCore: number
returnCodeName: string
deliveryDate: string | null
dateEntered: string
webSaleLineId: number | null
coreStatus: 'N/A' | 'Rejected' | 'Available' | 'Processed'
}
export type HtpSalesOrderLineRow = LowercaseKeys<SalesOrderLineRow> & { productcode: number }
+11
-3
{
"name": "@isoftdata/universal-object-htp-utility",
"version": "3.18.1",
"version": "3.19.0",
"description": "Functions to convert universal objects to htp schema",
"main": "index.js",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"type": "module",

@@ -14,5 +15,12 @@ "author": "",

},
"devDependencies": {
"@types/lodash": "^4.17.24",
"@types/node": "^25.5.0",
"tsx": "^4.21.0",
"typescript": "^5.9.3"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
"build": "tsc",
"test": "node --import tsx/esm --test tests/*.test.ts"
}
}
import { multiRowUpsertQuery, query } from './shared.js'
function convertCategory(categoryRow, productCode ) {
return {
productcode: productCode,
categoryid: categoryRow.categoryId,
inventorytypeid: categoryRow.inventoryTypeId,
category: categoryRow.category,
description: categoryRow.description,
partlistauthorityid: categoryRow.partListAuthorityId
}
}
export async function upsertCategoryRows(connection, categoryArr, productCode) {
const responses = []
try {
const convertedRows = categoryArr.map(row => convertCategory(row, productCode))
const multiRowUpsertResponse = await multiRowUpsertQuery(connection, 'category', convertedRows)
responses.push(`category affected rows: ${multiRowUpsertResponse.affectedRows}`)
return {
success: true,
data: responses
}
} catch (err) {
return {
success: false,
data: err
}
}
}
export async function deleteCategory(connection, productCode, categoryId) {
const responses = []
try {
const queryOptions = {
sql: `DELETE FROM category WHERE productcode = ? AND categoryid = ?`,
values: [productCode, categoryId],
}
const deleteResponse = await query(connection, queryOptions)
responses.push(`category delete affected rows: ${deleteResponse?.affectedRows}`)
return {
success: true,
data: responses
}
} catch (err) {
return {
success: false,
data: err
}
}
}
import { multiRowUpsertQuery, query } from './shared.js'
// This still expects companyinfo_replikwando to be setup ahead of time
function convertStore(row, productCode, companyInfoResults) {
const companyInfoMatch = companyInfoResults.find(element => element.store === row.id)
if (!companyInfoMatch) return
return {
address: row.physicalAddress?.street,
city: row.physicalAddress?.city,
companyname: row.name,
country: row.physicalAddress?.country,
faxnum: row.contact[0]?.fax,
//logoname: row.logoname,
phonenum: row.contact[0]?.phone,
productcode: productCode,
state: row.physicalAddress?.state,
store: row.id,
//system: row.system || "enterprise",
zip: row.physicalAddress?.zip,
// These were likely intended to come from Contact[]
contact: companyInfoMatch?.contact,
email: companyInfoMatch?.email,
latitude: companyInfoMatch?.latitude,
longitude: companyInfoMatch?.longitude,
website: companyInfoMatch?.website,
// Additional fields same as trigger and companyinfo_replikwando
accounttype: companyInfoMatch?.accounttype,
active: companyInfoMatch?.active,
admin_email: companyInfoMatch?.admin_email,
alt_phonenum: companyInfoMatch?.alt_phonenum,
buynow_active: companyInfoMatch?.buynow_active,
buynow_email: companyInfoMatch?.buynow_email,
buynow_message: companyInfoMatch?.buynow_message,
combined_site_public: companyInfoMatch?.combined_site_public,
contact: companyInfoMatch?.contact,
ecommerce_portal: companyInfoMatch?.ecommerce_portal,
ERequest: companyInfoMatch?.ERequest,
fogbugz_case: companyInfoMatch?.fogbugz_case,
hosted_itrack: companyInfoMatch?.hosted_itrack,
htuserlogin: companyInfoMatch?.htuserlogin,
htpassword: companyInfoMatch?.htpassword,
image_source: companyInfoMatch?.image_source,
images: companyInfoMatch?.images,
invpartslimit: companyInfoMatch?.invpartslimit,
invsellvehicles: companyInfoMatch?.invsellvehicles,
linkDescription: companyInfoMatch?.linkDescription,
local_currency: companyInfoMatch?.local_currency,
local_host: companyInfoMatch?.local_host,
local_username: companyInfoMatch?.local_username,
local_password: companyInfoMatch?.local_password,
local_database: companyInfoMatch?.local_database,
local_port: companyInfoMatch?.local_port,
logoname: companyInfoMatch?.logoname,
no_price_message: companyInfoMatch?.no_price_message,
onlineinvactive: companyInfoMatch?.onlineinvactive,
Policies: companyInfoMatch?.Policies,
salvage_request_email: companyInfoMatch?.salvage_request_email,
schema_priceRange: companyInfoMatch?.schema_priceRange,
sellvehicles: companyInfoMatch?.sellvehicles,
signupdate: companyInfoMatch?.signupdate,
system: companyInfoMatch?.system,
system_factor: companyInfoMatch?.system_factor,
vendor_factor: companyInfoMatch?.vendor_factor,
vendorinfluence: companyInfoMatch?.vendorinfluence,
}
}
async function getCompanyInfo(connection, productCode) {
const queryOptions = {
sql: `SELECT * FROM companyinfo_replikwando WHERE productcode = ?`,
values: [productCode],
}
return await query(connection, queryOptions)
}
export async function upsertCompanyInfoRows(connection, storeArr, productCode) {
const responses = []
const companyInfoResults = await getCompanyInfo(connection, productCode)
if (!companyInfoResults.length) {
return {
success: false,
request: '',
data: `No companyinfo inserts because no rows found in companyinfo_replikwando for productcode ${productCode}`
}
}
try {
const convertedRows = storeArr.map(row => convertStore(row, productCode, companyInfoResults))
const filteredRows = convertedRows.filter(row => row)
if (filteredRows.length) {
const multiRowUpsertResponse = await multiRowUpsertQuery(connection, 'companyinfo', filteredRows)
responses.push(`companyinfo affected rows: ${multiRowUpsertResponse?.affectedRows}`)
} else {
responses.push(`companyinfo affected rows: 0, check companyinfo_replikwando`)
}
return {
success: true,
request: '',
data: responses
}
} catch (err) {
return {
success: false,
request: '',
data: err
}
}
}
export async function deleteCompanyInfo(connection, productCode, store) {
const responses = []
try {
const queryOptions = {
sql: `DELETE FROM companyinfo WHERE productcode = ? AND store = ?`,
values: [productCode, store],
}
const deleteResponse = await query(connection, queryOptions)
responses.push(`companyinfo affected rows: ${deleteResponse?.affectedRows}`)
return {
success: true,
request: '',
data: responses
}
} catch (err) {
return {
success: false,
request: '',
data: err
}
}
}
import { multiRowUpsertQuery, query } from './shared.js'
function convertCustomer(row, productCode ) {
return {
productcode: productCode,
phone: row.phone,
customerid: row.customerId,
storeid: row.storeId,
company: row.company,
name: row.name,
street: row.street,
city: row.city,
county: row.country,
state: row.state,
zip: row.zip,
country: row.country,
fax: row.fax,
email: row.email,
web: row.web,
comments: row.comments,
type: row.type,
defaulttermid: row.defaultTermId,
dateentered: row.dateEntered,
taxitemid: row.taxItemId,
defaultpricetype: row.defaultPriceType,
percentofprice: row.percentOfPrice,
porequired: row.poRequired,
blanketpurchaseorder: row.blanketPurchaseOrder,
blanketpurchaseorderexpiration: row.blanketPurchaseOrderExpiration,
webuserid: row.webUserId
}
}
export async function upsertCustomerRows(connection, customerArr, productCode) {
const responses = []
try {
const convertedRows = customerArr.map(row => convertCustomer(row, productCode))
const multiRowUpsertResponse = await multiRowUpsertQuery(connection, 'customer', convertedRows)
responses.push(`customer affected rows: ${multiRowUpsertResponse.affectedRows}`)
return {
success: true,
data: responses
}
} catch (err) {
return {
success: false,
data: err
}
}
}
export async function deleteCustomerRow(connection, productCode, customerId) {
const responses = []
try {
const queryOptions = {
sql: `DELETE FROM customer WHERE productcode = ? AND customerid = ?`,
values: [productCode, customerId],
}
const deleteResponse = await query(connection, queryOptions)
responses.push(`customer delete affected rows: ${deleteResponse?.affectedRows}`)
return {
success: true,
data: responses
}
} catch (err) {
return {
success: false,
data: err
}
}
}
import { multiRowUpsertQuery, query } from './shared.js'
function convertCustomerAddress(row, productCode ) {
return {
productcode: productCode,
...row
}
}
export async function upsertCustomerAddressRows(connection, customerAddressArr, productCode) {
const responses = []
try {
const convertedRows = customerAddressArr.map(row => convertCustomerAddress(row, productCode))
const multiRowUpsertResponse = await multiRowUpsertQuery(connection, 'customeraddress', convertedRows)
responses.push(`customeraddress affected rows: ${multiRowUpsertResponse.affectedRows}`)
return {
success: true,
data: responses
}
} catch (err) {
return {
success: false,
data: err
}
}
}
export async function deleteCustomerAddressRow(connection, productCode, customerAddressId) {
const responses = []
try {
const queryOptions = {
sql: `DELETE FROM customeraddress WHERE productcode = ? AND customeraddressid = ?`,
values: [productCode, customerAddressId],
}
const deleteResponse = await query(connection, queryOptions)
responses.push(`customeraddress delete affected rows: ${deleteResponse?.affectedRows}`)
return {
success: true,
data: responses
}
} catch (err) {
return {
success: false,
data: err
}
}
}
import { multiRowUpsertQuery, query } from './shared.js'
function convertCustomerOption(row, productCode ) {
return {
productcode: productCode,
customeroptionid: row.customerOptionId,
optiontype: row.optionType,
option: row.option,
showincustomerlist: row.showInCustomerList
}
}
export async function upsertCustomerOptionRows(connection, customerOptionArr, productCode) {
const responses = []
try {
const convertedRows = customerOptionArr.map(row => convertCustomerOption(row, productCode))
const multiRowUpsertResponse = await multiRowUpsertQuery(connection, 'customeroption', convertedRows)
responses.push(`customeroption affected rows: ${multiRowUpsertResponse.affectedRows}`)
return {
success: true,
data: responses
}
} catch (err) {
return {
success: false,
data: err
}
}
}
export async function deleteCustomerOptionRow(connection, productCode, customerOptionId) {
const responses = []
try {
const queryOptions = {
sql: `DELETE FROM customeroption WHERE productcode = ? AND customeroptionid = ?`,
values: [productCode, customerOptionId],
}
const deleteResponse = await query(connection, queryOptions)
responses.push(`customeroption delete affected rows: ${deleteResponse?.affectedRows}`)
return {
success: true,
data: responses
}
} catch (err) {
return {
success: false,
data: err
}
}
}
import { multiRowUpsertQuery, query } from './shared.js'
function convertCustomerOptionValue(row, productCode ) {
return {
productcode: productCode,
customeroptionvalueid: row.customerOptionValueId,
customeroptionid: row.customerOptionId,
value: row.value,
customerid: row.customerId
}
}
export async function upsertCustomerOptionValueRows(connection, customerOptionValueArr, productCode) {
const responses = []
try {
const convertedRows = customerOptionValueArr.map(row => convertCustomerOptionValue(row, productCode))
const multiRowUpsertResponse = await multiRowUpsertQuery(connection, 'customeroptionvalue', convertedRows)
responses.push(`customeroptionvalue affected rows: ${multiRowUpsertResponse.affectedRows}`)
return {
success: true,
data: responses
}
} catch (err) {
return {
success: false,
data: err
}
}
}
export async function deleteCustomerOptionValueRow(connection, productCode, customerOptionValueId) {
const responses = []
try {
const queryOptions = {
sql: `DELETE FROM customeroptionvalue WHERE productcode = ? AND customeroptionvalueid = ?`,
values: [productCode, customerOptionValueId],
}
const deleteResponse = await query(connection, queryOptions)
responses.push(`customeroptionvalue delete affected rows: ${deleteResponse?.affectedRows}`)
return {
success: true,
data: responses
}
} catch (err) {
return {
success: false,
data: err
}
}
}
import { multiRowUpsertQuery, query } from './shared.js'
function convertCustomerPriceContract(row, productCode ) {
return {
productcode: productCode,
customerpricecontractid: row.customerPriceContractId,
customerid: row.customerId,
inventoryid: row.inventoryId,
price: row.price,
startdate: row.startDate,
enddate: row.endDate
}
}
export async function upsertCustomerPriceContractRows(connection, customerPriceContractArr, productCode) {
const responses = []
try {
const convertedRows = customerPriceContractArr.map(row => convertCustomerPriceContract(row, productCode))
const multiRowUpsertResponse = await multiRowUpsertQuery(connection, 'customerpricecontract', convertedRows)
responses.push(`customerpricecontract affected rows: ${multiRowUpsertResponse.affectedRows}`)
return {
success: true,
data: responses
}
} catch (err) {
return {
success: false,
data: err
}
}
}
export async function deleteCustomerPriceContractRow(connection, productCode, customerPriceContractId) {
const responses = []
try {
const queryOptions = {
sql: `DELETE FROM customerpricecontract WHERE productcode = ? AND customerpricecontractid = ?`,
values: [productCode, customerPriceContractId],
}
const deleteResponse = await query(connection, queryOptions)
responses.push(`customerpricecontract delete affected rows: ${deleteResponse?.affectedRows}`)
return {
success: true,
data: responses
}
} catch (err) {
return {
success: false,
data: err
}
}
}
import { deleteCategory, upsertCategoryRows } from './category.js'
import { deleteCompanyInfo, upsertCompanyInfoRows } from './companyInfo.js'
import { deleteCustomerRow, upsertCustomerRows } from './customer.js'
import { deleteCustomerAddressRow, upsertCustomerAddressRows } from './customerAddress.js'
import { deleteCustomerOptionRow, upsertCustomerOptionRows } from './customerOption.js'
import { deleteCustomerOptionValueRow, upsertCustomerOptionValueRows } from './customerOptionValue.js'
import { deleteCustomerPriceContractRow, upsertCustomerPriceContractRows } from './customerPriceContract.js'
import { deleteInventoryData, insertInventoryData, upsertInventoryRows } from './inventory.js'
import {
insertInventoryFileData,
upsertInventoryFileData,
deleteInventoryFileData,
deleteInventoryFileRow
} from './inventoryFile.js'
import {
insertInventoryOptionRows,
upsertInventoryOptionRows,
deleteInventoryOptionRows,
deleteInventoryOptionRow
} from './inventoryOption.js'
import {
deleteInventoryOptionListRow,
getPartUseInfoQuery,
insertInventoryOptionListRows,
upsertInventoryOptionListRows,
} from './inventoryOptionList.js'
import { deleteInventorySourceData, deleteInventorySourceRow, upsertInventorySourceRows } from './inventorySource.js'
import { deleteVehicle, upsertVehicleRows } from './invmaster.js'
import { insertMnfcrmodModelRows, upsertMnfcrmodModelRows, deleteMnfcrmodModelRow } from './mnfcrmod.js'
import { deletePartUseRow, upsertPartUseRows } from './partUse.js'
import { deleteSellPriceClassRow, upsertSellPriceClassRows } from './sellPriceClass.js'
export {
deleteCategory,
deleteCompanyInfo,
deleteCustomerAddressRow,
deleteCustomerOptionRow,
deleteCustomerOptionValueRow,
deleteCustomerPriceContractRow,
deleteCustomerRow,
deleteInventoryData,
deleteInventoryFileData,
deleteInventoryFileRow,
deleteInventoryOptionListRow,
deleteInventoryOptionRow,
deleteInventoryOptionRows,
deleteInventorySourceData,
deleteInventorySourceRow,
deleteMnfcrmodModelRow,
deletePartUseRow,
deleteSellPriceClassRow,
deleteVehicle,
getPartUseInfoQuery,
insertInventoryData,
insertInventoryFileData,
insertInventoryOptionListRows,
insertInventoryOptionRows,
insertMnfcrmodModelRows,
upsertCategoryRows,
upsertCompanyInfoRows,
upsertCustomerAddressRows,
upsertCustomerOptionRows,
upsertCustomerOptionValueRows,
upsertCustomerPriceContractRows,
upsertCustomerRows,
upsertInventoryFileData,
upsertInventoryOptionListRows,
upsertInventoryOptionRows,
upsertInventoryRows,
upsertInventorySourceRows,
upsertMnfcrmodModelRows,
upsertPartUseRows,
upsertSellPriceClassRows,
upsertVehicleRows
}
// module.exports = {
// insertMnfcrmodModelRows: require('./mnfcrmod').insertMnfcrmodModelRows,
// insertInventoryData: require('./inventory').insertInventoryData,
// insertInventoryFileData: require('./inventoryFile').insertInventoryFileData,
// insertInventoryOptionListRows: require('./inventoryOptionList').insertInventoryOptionListRows,
// insertInventoryOptionRows: require('./inventoryOption').insertInventoryOptionRows,
// upsertCategoryRows: require('./category').upsertCategoryRows,
// upsertCompanyInfoRows: require('./companyInfo').upsertCompanyInfoRows,
// upsertCustomerAddressRows: require('./customerAddress').upsertCustomerAddressRows,
// upsertCustomerRows: require('./customer').upsertCustomerRows,
// upsertCustomerOptionRows: require('./customerOption').upsertCustomerOptionRows,
// upsertCustomerOptionValueRows: require('./customerOptionValue').upsertCustomerOptionValueRows,
// upsertCustomerPriceContractRows: require('./customerPriceContract').upsertCustomerPriceContractRows,
// upsertInventoryFileData: require('./inventoryFile').upsertInventoryFileData,
// upsertInventoryRows: require('./inventory').upsertInventoryRows,
// upsertInventoryOptionListRows: require('./inventoryOptionList').upsertInventoryOptionListRows,
// upsertInventoryOptionRows: require('./inventoryOption').upsertInventoryOptionRows,
// upsertMnfcrmodModelRows: require('./mnfcrmod').upsertMnfcrmodModelRows,
// upsertPartListAuthorityMapRows: require('./partListAuthority').upsertPartListAuthorityMapRows,
// upsertPartUseRows: require('./partUse').upsertPartUseRows,
// upsertSellPriceClassRows: require('./sellPriceClass').upsertSellPriceClassRows,
// upsertVehicleRows: require('./invmaster').upsertVehicleRows,
// deleteCategory: require('./category').deleteCategory,
// deleteCompanyInfo: require('./companyInfo').deleteCompanyInfo,
// deleteCustomerRow: require('./customer').deleteCustomerRow,
// deleteCustomerAddressRow: require('./customerAddress').deleteCustomerAddressRow,
// deleteCustomerOptionRow: require('./customerOption').deleteCustomerOptionRow,
// deleteCustomerOptionValueRow: require('./customerOptionValue').deleteCustomerOptionValueRow,
// deleteCustomerPriceContractRow: require('./customerPriceContract').deleteCustomerPriceContractRow,
// deleteInventoryData: require('./inventory').deleteInventoryData,
// deleteInventoryFileData: require('./inventoryFile').deleteInventoryFileData,
// deleteInventoryFileRow: require('./inventoryFile').deleteInventoryFileRow,
// deleteInventoryOptionListRow: require('./inventoryOptionList').deleteInventoryOptionListRow,
// deleteInventoryOptionRow: require('./inventoryOption').deleteInventoryOptionRow,
// deleteInventoryOptionRows: require('./inventoryOption').deleteInventoryOptionRows,
// deleteInventorySourceData: require('./inventorySource').deleteInventorySourceData,
// deleteInventorySourceRow: require('./inventorySource').deleteInventorySourceRow,
// deleteMnfcrmodModelRow: require('./mnfcrmod').deleteMnfcrmodModelRow,
// deletePartListAuthorityMapRow: require('./partListAuthority').deletePartListAuthorityMapRow,
// deletePartUseRow: require('./partUse').deletePartUseRow,
// deleteSellPriceClassRow: require('./sellPriceClass').deleteSellPriceClassRow,
// deleteVehicle: require('./invmaster').deleteVehicle,
// getPartUseInfoQuery: require('./inventoryOptionList').getPartUseInfoQuery,
// }
import _ from "lodash"
import mysql from 'mysql2/promise'
import pProps from 'p-props'
import { multiRowInsertQuery, multiRowUpsertQuery, query } from './shared.js'
import { insertInventoryOptionListRows, upsertInventoryOptionListRows } from './inventoryOptionList.js'
import { insertInventoryOptionRows, upsertInventoryOptionRows, deleteInventoryOptionRows } from './inventoryOption.js'
import { deleteInventoryFileData, insertInventoryFileData, upsertInventoryFileData } from './inventoryFile.js'
import { upsertInventorySourceRows, deleteInventorySourceData } from './inventorySource.js'
// Get partuse data for all of the rows being handled
async function getPartUseRows(htpConnection, inventoryArr, productCode) {
// Create an array of all the inventorytype names, remove null and duplicates
const uniquePartUseRows = Array.from(new Set(inventoryArr.map(row => row.inventoryType?.id).filter(element => element !== null)))
if (!uniquePartUseRows.length) {
return
}
// Need to do this to escape correctly
const queryData = [uniquePartUseRows]
const queryOptions = {
sql: `SELECT partuse.typenum, partuse.part, partlistauthoritymap.partlistauthorityid
FROM partuse
LEFT JOIN partlistauthoritymap
ON partuse.productcode = partlistauthoritymap.productcode
AND partuse.typenum = partlistauthoritymap.sourceid
WHERE partuse.productcode = ? AND typenum IN ? `,
values: [productCode, queryData],
}
const response = await query(htpConnection, queryOptions)
return response.map(row => (
{
typenum: row.typenum,
part: row.part,
partlistauthorityid: row.partlistauthorityid,
}
))
}
// Create a Map from partUseData for efficient lookups
function createPartUseMap(partUseData) {
if (!partUseData) {
return null
}
return new Map(partUseData.map(row => [row.typenum, row]))
}
// TODO: Need to return the error instead of logging separately
// TODO: Needs revisited but handles empty partUseData
function addPartUse(partUseMap, inventoryObjectRow) {
// If partUseMap is empty, log and return inventoryObjectRow as-is
if (!partUseMap) {
console.info("No part use data for inventory partnum: ", inventoryObjectRow.inventoryId)
return
}
// If there is partUseData but no match, log and return nothing as there is likely a data issue
//const partUseMatch = partUseData.find(element => inventoryObjectRow.inventoryType.id === element.typenum)
const inventoryTypeId = inventoryObjectRow.inventoryType.id
if (!inventoryTypeId) {
console.info("No inventorytypeid given for this inventory partnum: ", inventoryObjectRow.inventoryId)
return
}
const partUseMatch = partUseMap.get(inventoryTypeId)
if (!partUseMatch) {
console.info("No insert because no matching partuse entry", "inventory", "partnum", inventoryObjectRow.inventoryId)
return
}
// Otherwise return an updated inventory object row
const { typenum, partlistauthorityid } = partUseMatch
inventoryObjectRow.typeNum = typenum
inventoryObjectRow.partListAuthorityId = partlistauthorityid
return inventoryObjectRow
}
// Converts universal inventory object to an HTP row
// Settings can have modifiers that allow different value mappings without additional queries, currently only used for available vs sellable quantities
function convertInventory(inventoryObjectRow, productCode, settings) {
const row = {
partnum: inventoryObjectRow.inventoryId,
store: inventoryObjectRow.storeId,
typenum: inventoryObjectRow.partListAuthorityId || inventoryObjectRow.typeNum,
sourcetypenum: inventoryObjectRow.typeNum,
stocknum: inventoryObjectRow.vehicle.id,
vinnum: inventoryObjectRow.vehicle.vin || '',
year: inventoryObjectRow.vehicle.year || inventoryObjectRow.year,
description: inventoryObjectRow.description || '',
interchangenumber: inventoryObjectRow.interchange.number,
subinterchangenumber: inventoryObjectRow.interchange.subNumber?.substring(0, 4) || '',
status: inventoryObjectRow.status,
suggestedprice: inventoryObjectRow.retailPrice || 0.00,
bottomprice: inventoryObjectRow.wholesalePrice,
replenish: inventoryObjectRow.replenish,
deplete: inventoryObjectRow.deplete,
quantity: settings.useSellableQuantity ? inventoryObjectRow.sellableQuantity : inventoryObjectRow.availableQuantity,
//dateentered: inventoryObjectRow.dateEntered !== '0000-00-00 00:00:00' ? row.dateentered = inventoryObjectRow.dateEntered : row.dateentered = null,
// datemodified: inventoryObjectRow.dateModified,
// datesold: inventoryObjectRow.dateSold,
label1: null,
label2: null,
label3: null,
label4: null,
data1: null,
data2: null,
data3: null,
data4: null,
cost: inventoryObjectRow.cost,
// make: inventoryObjectRow.vehicle.make?.substring(0, 24) || '', //incorrect?
// model: inventoryObjectRow.vehicle.model || '', //incorrect?
//make: inventoryObjectRow.manufacturer?.name?.substring(0, 24) || inventoryObjectRow.vehicle?.make?.substring(0, 24) || '',
// Vehicle Make
make: inventoryObjectRow.vehicle?.make?.substring(0, 24) || '',
// Vehicle Model
model: inventoryObjectRow.vehicle?.model?.substring(0, 49) || '',
//model: inventoryObjectRow.model?.name || inventoryObjectRow.vehicle?.model || '',
pmanufacturer: inventoryObjectRow.parentManufacturer.name?.substring(0, 24), //matches trigger
pmodel: inventoryObjectRow.parentModel.name?.substring(0,49), //matches trigger
taxable: inventoryObjectRow.taxable,
oemnum: inventoryObjectRow.oemNumber,
condition: inventoryObjectRow.condition,
side: inventoryObjectRow.side,
// TODO: confirm this is desired or make schema change
tagnum: inventoryObjectRow.tagNumber?.substring(0, 19),
upc: inventoryObjectRow.upc || '',
category: inventoryObjectRow.category,
weight: inventoryObjectRow.shippingWeight?.value,
productcode: productCode,
lastupdate: mysql.raw('NOW()'),
worldviewable: inventoryObjectRow.public,
displayonline: inventoryObjectRow.displayOnline,
notes: inventoryObjectRow.notes || '',
shippinglength: inventoryObjectRow.shippingDimensions?.length,
shippingwidth: inventoryObjectRow.shippingDimensions?.width,
shippingheight: inventoryObjectRow.shippingDimensions?.height,
core: inventoryObjectRow.retailCore,
location: inventoryObjectRow.location,
//buynow: inventoryObjectRow.webSaleClass ? inventoryObjectRow.webSaleClass.replace(/s$/,'') : 'None',
inventoryvendorid: inventoryObjectRow.vendorId,
jobberprice: inventoryObjectRow.jobberPrice,
distributorprice: inventoryObjectRow.distributorPrice,
averagecost: inventoryObjectRow.averageCost,
sellpriceclassid: inventoryObjectRow.sellPriceClassId,
wholesalecore: inventoryObjectRow.wholeSaleCore ? inventoryObjectRow.wholeSaleCore : null,
jobbercore: inventoryObjectRow.jobberCore ? inventoryObjectRow.jobberCore : null,
distributorcore: inventoryObjectRow.distributorCore ? inventoryObjectRow.distributorCore : null,
}
// The following return ER_TRUNCATED_WRONG_VALUE if left as is
if (inventoryObjectRow.dateEntered !== '0000-00-00 00:00:00') {
row.dateentered = inventoryObjectRow.dateEntered
} else row.dateentered = undefined
if (inventoryObjectRow.dateModified !== '0000-00-00 00:00:00') {
row.datemodified = inventoryObjectRow.dateModified
} else row.datemodified = undefined
if (inventoryObjectRow.dateSold !== '0000-00-00 00:00:00') {
row.datesold = inventoryObjectRow.dateSold
} else row.datesold = undefined
// Buy now might optionally come from a function (LKQ at least)
if (inventoryObjectRow.buyNowSla) {
row.buynow = inventoryObjectRow.buyNowSla
} else if (inventoryObjectRow.webSaleClass) {
row.buynow = inventoryObjectRow.webSaleClass.replace(/s$/,'')
} else {
row.buynow = 'None'
}
return row
}
/*
This is the primary function called by the queue and synch functions
The outer try/catch should return the same object as the other main functions
The nested try/catch is for transaction handling
*/
export async function upsertInventoryRows(htpConnection, inventoryArr, productCode, settings = {}) {
const responses = []
try {
//
// Get partuse data for later steps
const partUseData = await getPartUseRows(htpConnection, inventoryArr, productCode)
// Create a Map from partUseData for efficient lookups (do this once)
const partUseMap = createPartUseMap(partUseData)
// Add partUseData to inventoryArr
const inventoryPartUseRows = inventoryArr.map(row => addPartUse(partUseMap, row))
// Remove undefines, these rows will be used for their tags
const filteredInventoryPartUseRows = inventoryPartUseRows.filter(row => row !== undefined)
// Convert changed product rows to HTP rows
const convertedRows = filteredInventoryPartUseRows.map(row => convertInventory(row, productCode, settings))
// Need to filter undefined rows due to missing partuse data
const filteredRows = convertedRows.filter(row => row !== undefined)
if (filteredRows.length) {
// Get a connection from the pool for transaction
const connection = await htpConnection.getConnection()
try {
// Start transaction
await connection.beginTransaction()
// Insert inventory rows first, return should be??
const multiRowUpsertResponse = await multiRowUpsertQuery(connection, 'inventory', filteredRows)
// call inventory option list stuff
const multiRowInventoryOptionListUpsertResponse = await upsertInventoryOptionListRows(connection, filteredInventoryPartUseRows, productCode)
const multiRowInventoryOptionUpsertResponse = await upsertInventoryOptionRows(connection, filteredInventoryPartUseRows, productCode)
// call inventory file and image stuff
const multiRowInventoryFileUpsertResponse = await upsertInventoryFileData(connection, filteredInventoryPartUseRows, productCode)
// call inventory source stuff
let multiRowInventorySourceUpsertResponse
if (settings.synchSource) {
multiRowInventorySourceUpsertResponse = await upsertInventorySourceRows(connection, filteredInventoryPartUseRows, productCode)
}
// Commit transaction
await connection.commit()
responses.push(`affected rows: inventory ${multiRowUpsertResponse?.affectedRows}, ${multiRowInventoryOptionListUpsertResponse}, ${multiRowInventoryOptionUpsertResponse}, ${[...multiRowInventoryFileUpsertResponse]}, ${multiRowInventorySourceUpsertResponse ? multiRowInventorySourceUpsertResponse : 'inventorysource affected rows: none' }`)
} catch (err) {
// Rollback transaction on error
await connection.rollback()
throw err
} finally {
// Always release connection back to pool
connection.release()
}
} else {
responses.push(`affected rows: inventory none`)
}
return {
success: true,
request: '',
data: responses
}
} catch (err) {
return {
success: false,
request: '',
data: err
}
}
}
async function deleteInventoryQuery(htpConnection, productCode, partNum, storeId ) {
const queryOptions = {
sql: "DELETE FROM inventory WHERE partnum = ? AND store = ? AND productcode = ?",
values: [partNum, storeId, productCode],
}
return await query(htpConnection, queryOptions)
}
export async function deleteInventoryData(htpConnection, productCode, partNum, storeId) {
const responses = []
try {
const deleteResponses = await pProps({
inventoryDeleteResponse: deleteInventoryQuery(htpConnection, productCode, partNum, storeId),
inventoryOptionDeleteResponse: deleteInventoryOptionRows(htpConnection, productCode, storeId, partNum),
inventoryFileDeleteResponse: deleteInventoryFileData(htpConnection, productCode, partNum),
inventorySourceDeleteResponse: deleteInventorySourceData(htpConnection, productCode, partNum)
})
const fileDeleteResponse = deleteResponses.inventoryFileDeleteResponse.data
responses.push(`inventory delete affected rows: ${deleteResponses.inventoryDeleteResponse?.affectedRows}, ${deleteResponses.inventoryOptionDeleteResponse}, ${fileDeleteResponse}, ${deleteResponses.inventorySourceDeleteResponse}`)
return {
success: true,
request: '',
data: responses
}
} catch (err) {
return {
success: false,
request: '',
data: err
}
}
}
// Multi-row insert query
async function insertInventoryRows(htpConnection, rows) {
const columns = Object.keys(rows[0])
const values = rows.map(obj => Object.values(obj))
const result = await multiRowInsertQuery(htpConnection, "inventory", columns, values)
return result
}
// Multi-row insert function for repush
// TODO: get partUseData outside of this function
export async function insertInventoryData(htpConnection, inventoryArr, productCode, settings = {}) {
const responses = []
try {
//
// Get partuse data for later steps
const partUseData = await getPartUseRows(htpConnection, inventoryArr, productCode)
// Create a Map from partUseData for efficient lookups (do this once)
const partUseMap = createPartUseMap(partUseData)
// Add partUseData to inventoryArr
const inventoryPartUseRows = inventoryArr.map(row => addPartUse(partUseMap, row))
// Remove undefines, these rows will be used for their tags
const filteredInventoryPartUseRows = inventoryPartUseRows.filter(row => row !== undefined)
// Convert changed product rows to HTP rows
const convertedRows = filteredInventoryPartUseRows.map(row => convertInventory(row, productCode, settings))
// Need to filter undefined rows due to missing partuse data
const filteredRows = convertedRows.filter(row => row !== undefined)
if (filteredRows.length) {
// Get a connection from the pool for transaction
const connection = await htpConnection.getConnection()
try {
// Start transaction
await connection.beginTransaction()
// Insert inventory rows first
const initialResponse = await pProps({
inventoryInsertResponse: insertInventoryRows(connection, filteredRows),
inventoryFileResponse: insertInventoryFileData(connection, filteredInventoryPartUseRows, productCode),
inventoryOptionListResponse: insertInventoryOptionListRows(connection, filteredInventoryPartUseRows, productCode),
inventorySourceUpsertResponse: settings.synchSource ? upsertInventorySourceRows(connection, filteredInventoryPartUseRows, productCode) : Promise.resolve(null)
})
// This one has to be run after inventoryoptionlist since it needs to query for those rows
const inventoryOptionResponse = await insertInventoryOptionRows(connection, filteredInventoryPartUseRows, productCode)
// Commit transaction
await connection.commit()
responses.push(`affected rows: inventory ${initialResponse.inventoryInsertResponse?.affectedRows}, ${initialResponse.inventoryOptionListResponse}, ${inventoryOptionResponse}, ${initialResponse.inventoryFileResponse ? [...initialResponse.inventoryFileResponse] : ''}, ${initialResponse.inventorySourceUpsertResponse ? initialResponse.inventorySourceUpsertResponse : 'inventorysource affected rows: none' }`)
// responses.push(`affected rows: inventory ${inventoryInsertResponse.affectedRows}, ${inventoryOptionListResponse.affectedRows}, ${inventoryOptionResponse?.affectedRows}, ${inventoryFileResponse?.affectedRows}`)
} catch (err) {
// Rollback transaction on error
await connection.rollback()
throw err
} finally {
// Always release connection back to pool
connection.release()
}
}
return {
success: true,
request: '',
data: responses
}
} catch (err) {
//
return {
success: false,
request: '',
data: err
}
}
}
import { query, multiRowInsertQuery } from './shared.js'
import pProps from 'p-props'
import _ from 'lodash'
// ==== Conversion and Helper Functions ====
// Only adds inventoryid and productcode to attachment rows
function fluffAttachmentRows(productInventoryRow, productCode) {
const inventoryId = productInventoryRow.inventoryId
const storeId = productInventoryRow.storeId
const attachmentRows = productInventoryRow.attachments.map(function (attachment) {
if (attachment.id) {
return { ...attachment,
inventoryid: inventoryId,
productcode: productCode,
storeid: storeId
}
}
})
return attachmentRows
}
function convertManyInventoryImageRows(row) {
const imageLocation = `&productcode=${row.productcode}&id=${row.file}`
return {
productcode: row.productcode,
inventoryid: row.inventoryid,
fileid: row.file,
imagelocation: imageLocation,
rank: row.rank,
}
}
function convertManyInventoryFileRows(row) {
return {
productcode: row.productcode,
associatedid: row.inventoryid,
associatedidtype: 'inventoryid',
fileid: row.file,
filetype: row.type
}
}
function convertManyPartVideoListRows(row) {
return {
productcode: row.productcode,
store: row.storeid,
partnum: row.inventoryid,
videotype: row.type,
videoid: row.video.id,
url: row.video.url,
dirty: "False"
}
}
function filterExistingFileListRows(newRows, existingRows) {
const uniqueRows = newRows.filter(function(newRow) {
return !existingRows.find(function(existingRow) {
return existingRow.associatedid === newRow.associatedid && existingRow.fileid === newRow.fileid && newRow.associatedidtype === existingRow.associatedidtype
})
})
return uniqueRows
}
// ==== Query Functions ====
// Called when an inventory row is deleted
async function deletePartImageListData(connection, productCode, partNum) {
try {
const queryOptions = {
sql: `DELETE FROM partimagelist WHERE productcode = ? AND inventoryid = ?`,
values: [productCode, partNum]
}
const deleteResponse = await query(connection, queryOptions)
return `partimagelist delete affected rows: ${deleteResponse?.affectedRows}`
} catch (err) {
return `error deleting partimagelist rows: ${err}`
}
}
// Called when an inventory row is deleted
async function deletePartFileListData(connection, productCode, partNum) {
try {
const queryOptions = {
sql: `DELETE FROM partfilelist WHERE productcode = ? AND associatedidtype = 'inventoryid' AND associatedid = ?`,
values: [productCode, partNum]
}
const deleteResponse = await query(connection, queryOptions)
return `partfilelist delete affected rows: ${deleteResponse?.affectedRows}`
} catch (err) {
return `error deleting partfilelist rows: ${err}`
}
}
// Called in upsertPartImageList to cover non-public rows
async function deletePartImageListRows(htpConnection, deleteRows){
//const deleteRows = nonPublicRows.map(row => ([productCode, row.inventoryid, row.id]))
try {
const queryOptions = {
sql: `DELETE FROM partimagelist WHERE (productcode, inventoryid, fileid) in (?)`,
values: [deleteRows]
}
const deleteResponse = await query(htpConnection, queryOptions)
return `partimagelist delete affected rows: ${deleteResponse?.affectedRows}`
} catch (err) {
return `error deleting partimagelist rows: ${err}`
}
// const deleteResponse = await deletePartImageListRows(htpConnection, deleteRows)
}
// Called in upsertPartImageList to cover possible non-public rows
async function deletePartFileListRows(htpConnection, deleteRows){
//const deleteRows = nonPublicRows.map(row => ([productCode, 'inventoryid', row.inventoryid, row.id]))
try {
const queryOptions = {
sql: `DELETE FROM partfilelist WHERE (productcode, associatedid, associatedidtype, fileid) in (?)`,
values: [deleteRows]
}
const deleteResponse = await query(htpConnection, queryOptions)
return `non-public partfilelist delete affected rows: ${deleteResponse?.affectedRows}`
} catch (err) {
return `error deleting partfilelist rows: ${err}`
}
}
async function deletePartVideoListRows(htpConnection, deleteRows){
//const deleteRows = nonPublicRows.map(row => ([productCode, 'inventoryid', row.inventoryid, row.id]))
try {
const queryOptions = {
sql: `DELETE FROM partvideolist WHERE (productcode, store, partnum, videoid) in (?)`,
values: [deleteRows]
}
const deleteResponse = await query(htpConnection, queryOptions)
return `non-public partvideolist delete affected rows: ${deleteResponse?.affectedRows}`
} catch (err) {
return `error deleting partvideolist rows: ${err}`
}
}
// Called in deleteInventoryFileData when an inventoryfile row is deleted
async function deletePartImageListRow(connection, productCode, partNum, fileId) {
try {
const queryOptions = {
sql: `DELETE FROM partimagelist WHERE productcode = ? AND inventoryid = ? AND fileid = ?`,
values: [productCode, partNum, fileId]
}
const deleteResponse = await query(connection, queryOptions)
return `partimagelist delete affected rows: ${deleteResponse?.affectedRows}`
} catch (err) {
return `error deleting partimagelist rows: ${err}`
}
}
// Called by deleteInventoryFileRow when an inventoryfile row is deleted
async function deletePartFileListRow(connection, productCode, partNum, fileId, type) {
try {
const queryOptions = {
sql: `DELETE FROM partfilelist WHERE productcode = ? AND associatedid = ? AND associatedidtype = ? AND fileid = ?`,
values: [productCode, partNum, type, fileId]
}
const deleteResponse = await query(connection, queryOptions)
return `partfilelist delete affected rows: ${deleteResponse?.affectedRows}`
} catch (err) {
return `error deleting partfilelist rows: ${err}`
}
}
async function partImageListMultiRowUpsertQuery(htpConnection, objectArray) {
// Get columns
const columns = Object.keys(objectArray[0])
const values = objectArray.map(obj => Object.values(obj))
const queryOptions = {
sql: 'INSERT INTO partimagelist (??) VALUES ? ON DUPLICATE KEY UPDATE `rank` = values(`rank`)',
values: [columns, values]
}
return await query(htpConnection, queryOptions)
}
async function partVideoListMultiRowUpsertQuery(htpConnection, objectArray) {
// Get columns
const columns = Object.keys(objectArray[0])
const values = objectArray.map(obj => Object.values(obj))
const queryOptions = {
sql: 'INSERT INTO partvideolist (??) VALUES ? ON DUPLICATE KEY UPDATE `url` = values(`url`)',
values: [columns, values]
}
return await query(htpConnection, queryOptions)
}
async function partFileListMultiRowUpsertQuery(htpConnection, objectArray) {
// Get columns
const columns = Object.keys(objectArray[0])
const values = objectArray.map(obj => Object.values(obj))
const queryOptions = {
sql: `INSERT IGNORE INTO partfilelist (??) VALUES ?`,
values: [columns, values, 'value', 'value']
}
return await query(htpConnection, queryOptions)
}
// ==== Work Functions ====
async function queryExistingPartFileRow(connection, inventoryIds, productCode) {
const queryOptions = {
sql: `SELECT productcode, associatedid, associatedidtype, fileid, filetype FROM partfilelist WHERE productcode = ? AND associatedid IN (?)`,
values: [productCode, inventoryIds]
}
return await query(connection, queryOptions)
}
async function upsertPartImageList(htpConnection, inventoryImageRows, productCode) {
const responses = []
// Filter non public rows and delete
const nonPublicRows = inventoryImageRows.filter(row => row.public === 'False')
if (nonPublicRows.length) {
// Delete rows
const deleteRows = nonPublicRows.map(row => ([productCode, row.inventoryid, row.file]))
const deleteResponse = await deletePartImageListRows(htpConnection, deleteRows)
responses.push(deleteResponse)
}
// Filter for public rows and upsert
const publicRows = inventoryImageRows.filter(row => row.public === 'True')
if (publicRows.length) {
// Convert rows
const convertedRows = publicRows.map(row => convertManyInventoryImageRows(row))
const upsertResponse = await partImageListMultiRowUpsertQuery(htpConnection, convertedRows)
responses.push(`partimagelist affected rows: ${upsertResponse?.affectedRows}`)
}
return responses
}
async function upsertPartVideoList(htpConnection, inventoryVideoRows, productCode) {
const responses = []
// Filter non public rows and delete
const nonPublicRows = inventoryVideoRows.filter(row => row.public === 'False')
if (nonPublicRows.length) {
const deleteRows = nonPublicRows.map(row => ([productCode, row.store, row.inventoryid, row.video.id]))
const deleteResponse = await deletePartVideoListRows(htpConnection, deleteRows)
responses.push(deleteResponse)
}
const publicRows = inventoryVideoRows.filter(row => row.public === 'True')
// Convert
const convertedRows = publicRows.map(row => convertManyPartVideoListRows(row))
// Upsert
const upsertResponse = await partVideoListMultiRowUpsertQuery(htpConnection, convertedRows)
responses.push(`partvideolist affected rows: ${upsertResponse?.affectedRows}`)
return responses
}
async function upsertPartFileList(htpConnection, inventoryFileRows, productCode) {
const responses = []
// Filter non public rows and delete
const nonPublicRows = inventoryFileRows.filter(row => row.public === 'False')
if (nonPublicRows.length) {
// Delete rows
const deleteRows = nonPublicRows.map(row => ([productCode, 'inventoryid', row.inventoryid, row.file]))
const deleteResponse = await deletePartFileListRows(htpConnection, deleteRows)
responses.push(deleteResponse)
}
// Filter for public rows and upsert
const publicRows = inventoryFileRows.filter(row => row.public === 'True')
if (publicRows.length) {
// Convert rows
const convertedRows = publicRows.map(row => convertManyInventoryFileRows(row))
// partfilelist rows can't be "upserted", just insert or delete
// Get inventoryids for query
const inventoryIds = inventoryFileRows.map(row => row.inventoryid )
// Query for existing rows
const existingRows = await queryExistingPartFileRow(htpConnection, inventoryIds, productCode)
// need to filter out existing rows and only upsert new ones
const uniqueRows = filterExistingFileListRows(convertedRows, existingRows)
if (uniqueRows.length) {
const upsertResponse = await partFileListMultiRowUpsertQuery(htpConnection, uniqueRows)
responses.push(`partfilelist affected rows: ${upsertResponse?.affectedRows}`)
} else {
responses.push(`partfilelist affected rows: none`)
}
} else {
responses.push(`partfilelist affected rows: none`)
}
return responses
}
// Main function for partimagelist and partfilelist upserts
export async function upsertInventoryFileData(htpConnection, productInventoryRows, productCode) {
const responses = []
// Filter just the inventory rows with attachments
const inventoryRowsWithAttachments = productInventoryRows.filter(row => row.attachments?.length > 0)
// Add inventoryid and storeid to all of the attachment rows for later
const modifiedAttachmentRows = inventoryRowsWithAttachments.map(row => fluffAttachmentRows(row, productCode)).flat()
// Separate image and file rows since they're handled differently
const imageAttachmentRows = modifiedAttachmentRows.filter(row => row.type === 'Image')
const fileAttachmentRows = modifiedAttachmentRows.filter(row => row.type === 'PDF' || row.type === 'Text')
const videoAttachmentRows = modifiedAttachmentRows.filter(row => row.type === 'YOUTUBE')
// Call the individual upsert functions in parallel using pProps
// Prepare the filtered rows and create promises object
const filteredImageAttachmentRows = imageAttachmentRows.length ?
_.uniqWith(imageAttachmentRows, _.isEqual) : []
const filteredFileAttachmentRows = fileAttachmentRows.length ?
_.uniqWith(fileAttachmentRows, _.isEqual) : [];
const filteredYoutubeAttachmentRows = videoAttachmentRows.length ?
_.uniqWith(videoAttachmentRows, _.isEqual) : [];
// Create an object with promises to run in parallel
const promisesObj = {};
if (filteredImageAttachmentRows.length) {
promisesObj.imageResponse = upsertPartImageList(htpConnection, filteredImageAttachmentRows, productCode);
}
if (filteredFileAttachmentRows.length) {
promisesObj.fileResponse = upsertPartFileList(htpConnection, filteredFileAttachmentRows, productCode);
}
if (filteredYoutubeAttachmentRows.length) {
promisesObj.videoResponse = upsertPartVideoList(htpConnection, filteredYoutubeAttachmentRows, productCode)
}
// Run promises in parallel if any exist
if (Object.keys(promisesObj).length > 0) {
const results = await pProps(promisesObj);
// Add results to responses
if (results.imageResponse) {
responses.push(results.imageResponse);
}
if (results.fileResponse) {
responses.push(results.fileResponse);
}
if (results.videoResponse) {
responses.push(results.videoResponse)
}
}
return responses
}
// Called when an inventoryfile row is deleted
export async function deleteInventoryFileRow(htpConnection, productCode, inventoryId, fileId, type) {
const responses = []
try {
// Run delete operations in parallel using pProps
const deleteResults = await pProps({
imageListResult: deletePartImageListRow(htpConnection, productCode, inventoryId, fileId),
fileListResult: deletePartFileListRow(htpConnection, productCode, inventoryId, fileId, type)
});
responses.push(`${deleteResults.imageListResult}, ${deleteResults.fileListResult}`);
return {
success: true,
request: '',
data: responses
}
} catch (err) {
return {
success: false,
request: '',
data: err
}
}
}
// Called as part of an inventory row delete, fileid isn't known from changeDoc
export async function deleteInventoryFileData(htpConnection, productCode, inventoryId) {
// Two delete types: when an inventory row goes, and when inventoryfile goes (though they're likely related)
// Can't join a deleted row to file for the type so we'll have to do it with inventoryid, fileid, and productcode
const responses = []
try {
// Run delete operations in parallel using pProps
const deleteResults = await pProps({
imageListResult: deletePartImageListData(htpConnection, productCode, inventoryId),
fileListResult: deletePartFileListData(htpConnection, productCode, inventoryId)
});
responses.push(`${deleteResults.imageListResult}, ${deleteResults.fileListResult}`);
return {
success: true,
request: '',
data: responses
}
} catch (err) {
return {
success: false,
request: '',
data: err
}
}
}
async function insertPartImageListRows(htpConnection, inventoryImageRows, productCode) {
const responses = []
// Filter for public rows and upsert
const publicRows = inventoryImageRows.filter(row => row.public === 'True')
if (publicRows.length) {
// Convert rows
const convertedRows = publicRows.map(row => convertManyInventoryImageRows(row))
//const upsertResponse = await partImageListInsertQuery(htpConnection, convertedRows)
const columns = Object.keys(convertedRows[0])
const values = convertedRows.map(obj => Object.values(obj))
const response = await multiRowInsertQuery(htpConnection, 'partimagelist', columns, values)
responses.push(`partimagelist affected rows: ${response?.affectedRows}`)
} else {
responses.push(`partimagelist affected rows: none`)
}
return responses
}
async function insertPartFileListRows(htpConnection, inventoryFileRows, productCode) {
const responses = []
const publicRows = inventoryFileRows.filter(row => row.public === 'True')
if (publicRows.length) {
// Convert rows
const convertedRows = publicRows.map(row => convertManyInventoryFileRows(row))
// const response = await partFileListInsertQuery(htpConnection, convertedRows)
const columns = Object.keys(convertedRows[0])
const values = convertedRows.map(obj => Object.values(obj))
const response = await multiRowInsertQuery(htpConnection, 'partfilelist', columns, values)
responses.push(`partfilelist affected rows: ${response?.affectedRows}`)
} else {
responses.push(`partfilelist affected rows: none`)
}
return responses
}
// Bulk insert function for partimagelist and partfilelist
export async function insertInventoryFileData(htpConnection, productInventoryRows, productCode) {
const responses = []
// Filter just the inventory rows with attachments
const attachmentRows = productInventoryRows.filter(row => row.attachments?.length > 0)
// Add inventoryid to all of the attachment rows for later
const modifiedAttachmentRows = attachmentRows.map(row => fluffAttachmentRows(row, productCode)).flat()
// Separate image and file rows since they're handled differently
const imageAttachmentRows = modifiedAttachmentRows.filter(row => row.type === 'Image')
const fileAttachmentRows = modifiedAttachmentRows.filter(row => row.type === 'PDF' || row.type === 'Text')
const videoAttachmentRows = modifiedAttachmentRows.filter(row => row.type === 'YOUTUBE')
// Call the individual insert functions in parallel using pProps
// Prepare the filtered rows and create promises object
const filteredImageAttachmentRows = imageAttachmentRows.length ?
_.uniqWith(imageAttachmentRows, _.isEqual) : [];
const filteredFileAttachmentRows = fileAttachmentRows.length ?
_.uniqWith(fileAttachmentRows, _.isEqual) : [];
const filteredYoutubeAttachmentRows = videoAttachmentRows.length ?
_.uniqWith(videoAttachmentRows, _.isEqual) : [];
// Create an object with promises to run in parallel
const promisesObj = {};
if (filteredImageAttachmentRows.length) {
promisesObj.imageResponse = insertPartImageListRows(htpConnection, filteredImageAttachmentRows, productCode);
}
if (filteredFileAttachmentRows.length) {
promisesObj.fileResponse = insertPartFileListRows(htpConnection, filteredFileAttachmentRows, productCode);
}
if (filteredYoutubeAttachmentRows.length) {
promisesObj.videoResponse = upsertPartVideoList(htpConnection, filteredYoutubeAttachmentRows, productCode)
}
// Run promises in parallel if any exist
if (Object.keys(promisesObj).length > 0) {
const results = await pProps(promisesObj);
// Add results to responses
if (results.imageResponse) {
responses.push(results.imageResponse);
}
if (results.fileResponse) {
responses.push(results.fileResponse);
}
if (results.videoResponse) {
responses.push(results.videoResponse)
}
}
return responses
}
import mysql from 'mysql2/promise'
import { multiRowInsertQuery, query, queryFirst } from './shared.js'
import _ from 'lodash'
function convertManyInventoryOptionValue(inventoryOptionListData, productInventoryRow, productCode) {
const tags = productInventoryRow.tags
const inventoryOptionRows = tags.map(function (tag) {
if (tag.value && tag.label) {
// Check inventoryoptionlist for match to use for building inventoryoption(value) row
const inventoryOptionListRow = inventoryOptionListData.find(element => tag.label === element.option && productInventoryRow.typeNum === element.sourcetypenum && (productInventoryRow.partListAuthorityId ? productInventoryRow.partListAuthorityId === element.typenum : productInventoryRow.typeNum === element.typenum))
if (inventoryOptionListRow) {
const { optionnum } = inventoryOptionListRow
return {
productcode: productCode,
storenum: productInventoryRow.storeId,
partnum: productInventoryRow.inventoryId,
value: tag.value,
optionnum,
lastupdate: mysql.raw('NOW()'),
}
} else {
console.debug(`No matching inventoryoptionlist row found for inventoryRow.tag.label ${tag.label} of inventory id ${productInventoryRow.inventoryId}`)
return
}
}
})
return inventoryOptionRows.filter(row => row !== undefined)
}
async function inventoryOptionMultiRowUpsertQuery(htpConnection, objectArray) {
// Get columns
const columns = Object.keys(objectArray[0])
const values = objectArray.map(obj => Object.values(obj))
const queryOptions = {
sql: `INSERT INTO inventoryoption (??) VALUES ? ON DUPLICATE KEY UPDATE ?? = values(??)`,
values: [columns, values, 'value', 'value']
}
return await query(htpConnection, queryOptions)
}
// Main multi-row upsert function
export async function upsertInventoryOptionRows(connection, productInventoryPartUseRows, productCode) {
// Get the relevant typenums to find existing optionlist rows
const allTypeNums = productInventoryPartUseRows.map(function(row) {
return row.typeNum
})
const uniqueTypeNums = _.uniqWith(allTypeNums, _.isEqual)
// Get a list of inventoryoptionlist rows
const inventoryOptionListData = await getInventoryOptionList(connection, productCode, uniqueTypeNums)
const inventoryOptionRows = productInventoryPartUseRows.map(row => convertManyInventoryOptionValue(inventoryOptionListData, row, productCode)).flat()
// const inventoryOptionMultiRowUpsertResponse= await inventoryOptionMultiRowUpsertQuery(connection, inventoryOptionRows)
if (inventoryOptionRows.length) {
const inventoryOptionMultiRowUpsertResponse = await inventoryOptionMultiRowUpsertQuery(connection, inventoryOptionRows)
return (`affected rows: inventoryoption ${inventoryOptionMultiRowUpsertResponse?.affectedRows}`)
} else {
return (`affected rows: inventoryoption none`)
}
}
async function queryInventoryOptionList(connection, productCode, typeNum, option) {
const queryOptions = {
sql: `SELECT * FROM inventoryoptionlist WHERE productcode = ? AND typenum = ? AND inventoryoptionlist.option = ?`,
values: [productCode, typeNum, option]
}
return await queryFirst(connection, queryOptions)
}
// Deletes a specific row
export async function deleteInventoryOptionRow(connection, productCode, partNum, typeNum, option, value) {
// Need inventoryoptionlist data to get optionnum from inventoryoptionlist for a delete
const queryResult = await queryInventoryOptionList(connection, productCode, typeNum, option)
if (queryResult?.optionnum) {
try {
const queryOptions = {
sql: `DELETE FROM inventoryoption WHERE productcode = ? AND partnum = ? AND optionnum = ? AND value = ?`,
values: [ productCode, partNum, queryResult.optionnum, value ]
}
const deleteResponse = await query(connection, queryOptions)
return {
success: true,
request: '',
data: `inventoryoption delete affected rows: ${deleteResponse?.affectedRows}`
}
} catch (err) {
return {
success: false,
request: '',
data: `error deleting inventoryoption rows: ${err}`
}
}
} else {
return {
success: false,
request: '',
data: `no matching inventoryoptionlist value for typenum: ${typeNum} option: ${option}`
}
}
}
// Deletes all rows for an inventory item
export async function deleteInventoryOptionRows(connection, productCode, storeNum, partNum) {
// Get columns
try {
const queryOptions = {
sql: `DELETE FROM inventoryoption WHERE productcode = ? AND storenum = ? AND partnum = ?`,
values: [productCode, storeNum, partNum]
}
const deleteResponse = await query(connection, queryOptions)
return `inventoryoption delete affected rows: ${deleteResponse?.affectedRows}`
} catch (err) {
return `error deleting inventoryoption rows: ${err}`
}
}
// Bulk insert function
export async function insertInventoryOptionRows(connection, productInventoryPartUseRows, productCode) {
// grab all the relevant inventoryoptionlist values
// Get the relevant typenums to find existing optionlist rows
const allTypeNums = productInventoryPartUseRows.map(function(row) {
return row.typeNum
})
const uniqueTypeNums = _.uniqWith(allTypeNums, _.isEqual)
// Get a list of inventoryoptionrows
const inventoryOptionListData = await getInventoryOptionList(connection, productCode, uniqueTypeNums)
const inventoryOptionRows = productInventoryPartUseRows.map(row => convertManyInventoryOptionValue(inventoryOptionListData, row, productCode)).flat()
if (inventoryOptionRows.length > 0) {
const columns = Object.keys(inventoryOptionRows[0])
const values = inventoryOptionRows.map(obj => Object.values(obj))
const response = await multiRowInsertQuery(connection, 'inventoryoption', columns, values)
return (`affected rows: inventoryoption ${response?.affectedRows}`)
} else {
return (`affected rows: inventoryoption none`)
}
}
async function getInventoryOptionList(connection, productCode, typeNums) {
const queryData = [typeNums]
const queryOptions = {
sql: "SELECT * FROM inventoryoptionlist WHERE productcode = ? AND sourcetypenum IN ?",
values: [productCode, queryData]
}
return await query(connection, queryOptions)
}
import { multiRowInsertQuery, query, queryFirst } from './shared.js'
import _ from "lodash"
import mysql from 'mysql2/promise'
// Get partlistauthorityid and partuse from HTP using inventorytype name from universal inventory object
export async function getPartUseInfoQuery(connection, inventoryObjectRow, productCode) {
return await queryFirst(connection, {
sql: `SELECT partuse.*, partlistauthoritymap.partlistauthorityid
FROM partuse
LEFT JOIN partlistauthoritymap
ON partuse.productcode = partlistauthoritymap.productcode and
partuse.typenum = partlistauthoritymap.sourceid
WHERE partuse.productcode = ? AND part = ?`,
values: [productCode, inventoryObjectRow.inventoryType], //only inventorytype.name is provided
})
}
async function getExistingInventoryOptionList(connection, productCode, uniqueTypeNums) {
const queryData = [uniqueTypeNums]
// product.typeNum => htp.sourcetypenum
const queryOptions = {
sql: "SELECT optionnum, typenum, sourcetypenum, `option`, `rank`, public FROM inventoryoptionlist WHERE productcode = ? AND sourcetypenum IN ?",
values: [productCode, queryData],
}
const result = await query(connection, queryOptions)
// Create a list to compare against
const resultCache = result.map(function (row) {
return {
optionnum: row.optionnum,
typenum: row.typenum,
sourcetypenum: row.sourcetypenum,
option: row.option,
rank: row.rank,
productcode: productCode,
public: row.public // default is true so we should be safe
//lastupdate: mysql.raw('NOW()'),
}
})
return resultCache
}
async function inventoryOptionListMultiRowUpsertQuery(htpConnection, optionListArray) {
const newOptionListRows = optionListArray.map(obj => ({ ...obj, lastupdate: mysql.raw('NOW()') }))
const columns = Object.keys(newOptionListRows[0])
const values = optionListArray.map(obj => Object.values(obj))
const queryOptions = {
sql: 'INSERT INTO inventoryoptionlist (??) VALUES ? ON DUPLICATE KEY UPDATE `rank` = values(`rank`), lastupdate = values(lastupdate), public = values(public)',
values: [columns, values]
}
return await query(htpConnection, queryOptions)
}
function convertManyInventoryOptionList(productInventoryRow, productCode) {
//10 valid tags
const inventoryOptionListRows = productInventoryRow.tags.map(function (tag) {
// This only checks that both a value and label exist
if (tag.value && tag.label) {
return {
sourcetypenum: productInventoryRow.typeNum,
typenum: productInventoryRow.partListAuthorityId || productInventoryRow.typeNum,
option: tag.label,
rank: tag.rank,
productcode: productCode,
// TODO: Confirm this is correct or not
public: tag.public ? tag.public : 'True',
//lastupdate: mysql.raw('NOW()'),
}
}
})
// This gets rid of undefined rows, usually caused by empty strings
const filteredRows = inventoryOptionListRows.filter(row => row !== undefined)
return filteredRows
}
// Only a 5x difference in speed compared to the previous filter + find method but always tests faster
function filterExistingOptionListRows(newRows, existingRows) {
// Create a Map for lookups
const existingRowsMap = new Map()
// Populate the Map with composite keys
existingRows.forEach(row => {
const key = `${row.typenum}|${row.option}|${row.sourcetypenum}`
existingRowsMap.set(key, true)
})
// Filter newRows using the Map for lookups
return newRows.filter(newRow => {
const key = `${newRow.typenum}|${newRow.option}|${newRow.sourcetypenum}`
return !existingRowsMap.has(key)
})
}
// Old method, keeping here in case there are any side effects of the new Map method
// that testing didn't catch.
function filterExistingOptionListRowsOld(newRows, existingRows) {
const uniqueRows = newRows.filter(function(newRow) {
return !existingRows.find(function(existingRow) {
return existingRow.typenum === newRow.typenum && existingRow.option === newRow.option && existingRow.sourcetypenum === newRow.sourcetypenum
})
})
return uniqueRows
}
function matchExistingFileListRows(newRows, existingRows) {
const updatedRows = []
// Find matching rows, add optionnum and return updated object
//const testRows = newRows.map(row => addOptionNum(existingRows, row))
//or
for (const row of newRows) {
const existingRowMatch = existingRows.find(element => row.typenum === element.typenum && row.option === element.option)
if (existingRowMatch) {
const {optionnum} = existingRowMatch
row.optionnum = optionnum
updatedRows.push(row)
}
}
return updatedRows
}
// New function that takes the entire array of inventory rows with partuse data
// Option, rank, and public are the possible changeable values
export async function upsertInventoryOptionListRows(connection, inventoryRows, productCode) {
const responses = []
// Get the tags from all of the inventoryRows, there will be some duplicates
const optionListRows = inventoryRows.map(row => convertManyInventoryOptionList(row, productCode)).flat()
//Remove duplicates
const uniqueFilteredRows = _.uniqWith(optionListRows, _.isEqual)
// Get the relevant typenums to find existing optionlist rows
const allTypeNums = inventoryRows.map(function(row) {
return row.typeNum
})
const uniqueTypeNums = _.uniqWith(allTypeNums, _.isEqual)
// Get existing optionlist rows
const existingOptionList = await getExistingInventoryOptionList(connection, productCode, uniqueTypeNums)
// Create list of new and unique items for upsert
const uniqueOptionListRows = filterExistingOptionListRows(uniqueFilteredRows, existingOptionList)
// Upsert probably won't work as optionnum + productcode is the primary key
// If option changes, it will add a new one
if (uniqueOptionListRows.length) {
// Add lastupdate
const newOptionListRows = uniqueOptionListRows.map(obj => ({ ...obj, lastupdate: mysql.raw('NOW()') }))
const columns = Object.keys(newOptionListRows[0])
const values = newOptionListRows.map(obj => Object.values(obj))
const response = await multiRowInsertQuery(connection, 'inventoryoptionlist', columns, values)
responses.push(`affected rows: new inventoryoptionlist ${response?.affectedRows}`)
} else {
responses.push(`affected rows: new inventoryoptionlist none`)
}
const rowsWithOptionNum = matchExistingFileListRows(uniqueFilteredRows, existingOptionList)
if (rowsWithOptionNum.length) {
const newOptionListRows = rowsWithOptionNum.map(obj => ({ ...obj, lastupdate: mysql.raw('NOW()') }))
// Need to do a custom upsert query
const upsertResponse = await inventoryOptionListMultiRowUpsertQuery(connection, newOptionListRows)
responses.push(`affected rows: existing inventoryoptionlist ${upsertResponse?.affectedRows}`)
}
return responses
}
export async function deleteInventoryOptionListRow(connection, productCode, typeNum, option, rank) {
const queryOptions = {
sql: `DELETE FROM inventoryoptionlist WHERE productcode = ? AND typenum = ? AND inventoryoptionlist.option = ?`,
values: [productCode, typeNum, option]
}
try {
const result = await query(connection, queryOptions)
return {
success: true,
request: '',
data: `inventoryoptionlist delete affected rows: ${result.affectedRows}`
}
} catch (err) {
return {
success: false,
request: '',
data: err
}
}
}
// Bulk synch insert
export async function insertInventoryOptionListRows(connection, productInventoryRows, productCode) {
const responses = []
// Convert rows to insertable objects
const optionListRows = productInventoryRows.map(row => convertManyInventoryOptionList(row, productCode)).flat()
// Remove duplicates
const uniqueFilteredRows = _.uniqWith(optionListRows, _.isEqual)
// Get the relevant typenums to find existing optionlist rows
const allTypeNums = productInventoryRows.map(function(row) {
return row.typeNum
})
const uniqueTypeNums = _.uniqWith(allTypeNums, _.isEqual)
// Get list of existing rows from db to compare to
const existingOptionListRows = await getExistingInventoryOptionList(connection, productCode, uniqueTypeNums)
// Create a list of new items to be inserted
const uniqueOptionListRows = filterExistingOptionListRows(uniqueFilteredRows, existingOptionListRows)
// Add lastupdate to rows
// TODO: this can be done as part of the conversion, its a leftover from a failed unique comparison func
if (uniqueOptionListRows.length) {
const newOptionListRows = uniqueOptionListRows.map(obj => ({ ...obj, lastupdate: mysql.raw('NOW()') }))
const columns = Object.keys(newOptionListRows[0])
const values = newOptionListRows.map(obj => Object.values(obj))
const response = await multiRowInsertQuery(connection, 'inventoryoptionlist', columns, values)
responses.push(`affected rows: inventoryoptionlist ${response?.affectedRows}`)
} else {
responses.push(`affected rows: inventoryoptionlist none`)
}
return responses
}
import { multiRowUpsertQuery, multiRowInsertQuery, query } from './shared.js'
import pProps from 'p-props'
// TODO: remove insert functions...everything should work with upserts
// TODO: upserts may not be working correctly and instead inserting new rows every time
// Called by inventory row delete
export async function deleteInventorySourceData(connection, productCode, partNum) {
try {
const queryOptions = {
sql: `DELETE FROM inventorysource WHERE productcode = ? AND partnum = ?`,
values: [productCode, partNum, storeId]
}
const deleteResponse = await query(connection, queryOptions)
return `inventorysource delete affected rows: ${deleteResponse?.affectedRows}`
} catch (err) {
return `error deleting inventorysource row: ${err}`
}
}
// Called when an inventory source row is deleted
export async function deleteInventorySourceRow(connection, productCode, inventorySourceId) {
try {
const queryOptions = {
sql: `DELETE FROM inventorysource WHERE productcode = ? AND inventorysourceid`,
values: [productCode, inventorySourceId]
}
const deleteResponse = await query(connection, queryOptions)
return {
success: true,
data: `inventorysource delete affected rows: ${deleteResponse?.affectedRows}`
}
} catch (err) {
return {
success: false,
data: `error deleting inventorysource row: ${err}`
}
}
}
// #things that can change: quantity, quantityallocated, date? status?
// #Compare new and existing rows by
// #inventorystoreid, inventoryid, documenttype, documentid, documentstoreid, documentlineid
// #if any match, update those rows after grabbing the htpinventorysourceid
// #if any new rows don't match, insert those as new
// Finds existing rows to be updated
function filterExistingInventorySourceRows(newRows, existingRows) {
// For each new row, find the matching existing row
const updateRows = []
for (const newRow of newRows) {
const matchingRow = existingRows.find(function(existingRow) {
return existingRow.inventorystoreid === newRow.inventorystoreid && existingRow.inventoryid === newRow.inventoryid
&& newRow.documenttype === existingRow.documenttype && newRow.documentid == existingRow.documentid &&
newRow.documentstoreid == existingRow.documentstoreid && newRow.documentlineid == existingRow.documentlineid
})
// If exists, get the htpinventorysourceid and add it to updateRows
if (matchingRow) {
newRow.htpinventorysourceid = matchingRow.htpinventorysourceid
updateRows.push(newRow)
}
}
return updateRows
}
// Finds new rows for insert
function filterNewInventorySourceRows(newRows, existingRows) {
const newInventorySourceRows = newRows.filter(function(newRow) {
return !existingRows.find(function(existingRow) {
return existingRow.inventorystoreid === newRow.inventorystoreid && existingRow.inventoryid === newRow.inventoryid
&& newRow.documenttype === existingRow.documenttype && newRow.documentid == existingRow.documentid &&
newRow.documentstoreid == existingRow.documentstoreid && newRow.documentlineid == existingRow.documentlineid
})
})
return newInventorySourceRows
}
async function queryExistingInventorySourceRows(connection, inventoryIds, productCode) {
const queryOptions = {
sql: `SELECT * FROM inventorysource WHERE productcode = ? AND inventoryid IN (?)`,
values: [productCode, inventoryIds]
}
return await query(connection, queryOptions)
}
// Called by inventory upsert
export async function upsertInventorySourceRows(connection, productInventoryRows, productCode) {
const responses = []
// Flatten the rows then create source map array, add productcode and partnum
const convertedInventorySourceRows = productInventoryRows.flatMap(inventoryRow =>
inventoryRow.openSources.map(sourceRow => ({
inventorystoreid: sourceRow.inventoryStoreId,
quantity: sourceRow.quantity,
quantityallocated: sourceRow.quantityAllocated,
documenttype: sourceRow.documentType,
documentstoreid: sourceRow.documentStoreId,
documentlineid: sourceRow.documentLineId,
date: sourceRow.date,
status: sourceRow.status,
productcode: productCode,
inventoryid: inventoryRow.inventoryId,
}))
)
// Filter out new rows for insert
if (convertedInventorySourceRows.length) {
// Get the inventoryids and query existing rows
const inventoryIds = productInventoryRows.map(row => row.inventoryId )
const existingInventorySourceRows = await queryExistingInventorySourceRows(connection, inventoryIds, productCode)
//things that can change: quantity, quantityallocated, date? status?
// Find new rows for insert
const newInventorySourceRows = filterNewInventorySourceRows(convertedInventorySourceRows, existingInventorySourceRows)
// Find existing rows for possible update
const updateInventorySourceRows = filterExistingInventorySourceRows(convertedInventorySourceRows, existingInventorySourceRows)
//const uniqueRows = filterExistingInventorySourceRows(convertedInventorySourceRows, existingInventorySourceRows)
let promisesObj = {}
if (newInventorySourceRows.length) {
const columns = Object.keys(newInventorySourceRows[0])
const values = newInventorySourceRows.map(obj => Object.values(obj))
promisesObj.insertResponse = multiRowInsertQuery(connection, "inventorysource", columns, values)
}
if (updateInventorySourceRows.length) {
promisesObj.updateResponse = multiRowUpsertQuery(connection, 'inventorysource', updateInventorySourceRows)
}
if (Object.keys(promisesObj).length > 0) {
const results = await pProps(promisesObj)
if (results.insertResponse) {
responses.push(`new inventorysource affected rows: ${results.insertResponse?.affectedRows}`)
}
if (results.updateResponse) {
responses.push(`updated inventorysource affected rows: ${results.updateResponse?.affectedRows}`)
}
} else {
responses.push(`inventorysource affected rows: none`)
}
} else {
responses.push(`inventory source affected rows: none`)
}
return responses
}
import { multiRowUpsertQuery, query } from './shared.js'
import mysql from 'mysql2/promise'
// TODO: Change naming to invmaster to match others
function convertVehicle(vehicleRow, productCode) {
return {
stocknum: vehicleRow.id,
// Original trigger doesn't use stocknumber
store: vehicleRow.storeId,
vinnum: vehicleRow.vin,
make: vehicleRow.make,
model: vehicleRow.model,
year: vehicleRow.year,
mileage: vehicleRow.mileage,
bodystyle: vehicleRow.bodyStyle,
extcolor: vehicleRow.colors[1].name,
extcolorcode: vehicleRow.colors[1].code,
intcolor: vehicleRow.colors[0].name,
intcolorcode: vehicleRow.colors[0].code,
status: vehicleRow.status,
dateentered: vehicleRow.dateEntered,
trackingnum: vehicleRow.tagNumber,
damage: vehicleRow.description,
productcode: productCode,
lastupdate: mysql.raw('NOW()'),
titlenum: vehicleRow.title?.number || '',
titlestatus: vehicleRow.title?.status || '',
titletype: vehicleRow.title?.type || ''
}
}
// Multiple row insert
export async function upsertVehicleRows(connection, vehicleArr, productCode) {
const responses = []
try {
// Convert to HTP insertable object
const convertedRows = vehicleArr.map(row => convertVehicle(row, productCode))
const multiRowUpsertResponse = await multiRowUpsertQuery(connection, 'invmaster', convertedRows)
responses.push(`invmaster affected rows: ${multiRowUpsertResponse.affectedRows}`)
return {
success: true,
request: '',
data: responses
}
} catch (err) {
return {
success: false,
request: '',
data: err
}
}
}
export async function deleteVehicle(connection, productCode, stockNum, store) {
const responses = []
try {
const queryOptions = {
sql: `DELETE FROM invmaster WHERE productcode = ? AND stocknum = ? AND store = ?`,
values: [productCode, stockNum, store],
}
const deleteResponse = await query(connection, queryOptions)
responses.push(`invmaster delete affected rows: ${deleteResponse?.affectedRows}`)
return {
success: true,
request: '',
data: responses
}
} catch (err) {
return {
success: false,
request: '',
data: err
}
}
}
import { multiRowInsertQuery, multiRowUpsertQuery, query } from './shared.js'
import mysql from 'mysql2/promise'
// Convert universal model object to HTP insertable row for mnfcrmod
function convertMnfcrmod(modelRow, productCode) {
return {
typenum: modelRow.inventoryTypeId,
pmanufacturer: modelRow.manufacturerName?.substring(0, 24) ?? '',
pmodel: modelRow.name,
firstyear: modelRow.firstYear,
lastyear: modelRow.lastYear,
//weight: ,
productcode: productCode,
active: modelRow.active,
// Following are required but not in object
sourcetypenum: modelRow.inventoryTypeId,
category: 'Used',
intcode: '',
lastupdate: mysql.raw('NOW()'),
}
}
// Convert universal model object to HTP insertable row for model
function convertModel(modelRow, productCode) {
return {
Model: modelRow.name?.substring(0, 29) ?? '',
Make: modelRow.manufacturerName?.substring(0, 24) ?? '',
FirstYear: modelRow.firstYear,
LastYear: modelRow.lastYear,
Series: modelRow.series,
Active: modelRow.active,
lastupdate: mysql.raw('NOW()'),
productcode: productCode,
}
}
async function queryExistingMnfcrmod(connection, typenums, pmanufacturers, pmodels, productCode) {
const queryOptions = {
sql: `SELECT * FROM mnfcrmod WHERE productcode = ? AND typenum IN (?) AND pmanufacturer IN (?) AND pmodel IN (?)`,
values: [productCode, typenums, pmanufacturers, pmodels]
}
return await query(connection, queryOptions)
}
/*
Looks like we can only update the first/last year and active columns
Leaving out mnfcrmod.name will find the nearest match of type and manufacturer but only the first of the .names gets added
If a name changes, it won't delete or change the existing row, only create a new one and there's no current way to fix that
*/
async function multiRowMnfcrModUpsertQuery(htpConnection, objectArray) {
// Get columns
const columns = Object.keys(objectArray[0])
const values = objectArray.map(obj => Object.values(obj))
// Create query string addition for all of the columns to be updated
// const queryStringAdd = columns.map(row => `?? = VALUES(??)`)
const queryOptions = {
sql: 'INSERT INTO mnfcrmod (??) VALUES ? ON DUPLICATE KEY UPDATE firstyear = VALUES(firstyear), lastyear = VALUES(lastyear), `active` = VALUES(`active`)',
values: [columns, values],
}
return await query(htpConnection, queryOptions)
}
// Look for existing rows and add htpmnfcrmodid if exists
function filterMnfcrmodRows(mnfcrmodArr, existingRowsArr) {
const updateRows = []
mnfcrmodArr.forEach(row => {
const matchingRow = existingRowsArr.find(element => element.typenum === row.typenum && element.sourcetypenum === row.typenum && element.pmanufacturer === row.pmanufacturer && row.pmodel === element.pmodel)
if (matchingRow) {
row.htpmnfcrmodid = matchingRow.htpmnfcrmodid
updateRows.push(row)
} else {
row.htpmnfcrmodid = undefined
updateRows.push(row)
}
})
return updateRows
}
/*
Unable to run a simple ON DUPLICATE KEY UPDATE on all rows as we get multiple copies
We'll have to check for existing mnfcrmod rows and add the primary key to the object then update
or insert if there's no match.
*/
async function upsertMnfcrmodRows(connection, mnfcrmodArr, productCode) {
// Convert to HTP rows
const convertedMnfcrmodRows = mnfcrmodArr.map(row => convertMnfcrmod(row, productCode))
// Create all the columns needed to build query to check for existing rows
const typenums = convertedMnfcrmodRows.map(row => row.typenum)
const pmanufacturers = convertedMnfcrmodRows.map(row => row.pmanufacturer)
const pmodels = convertedMnfcrmodRows.map(row => row.pmodel)
// Query for existing rows
const existingRowsResponse = await queryExistingMnfcrmod(connection, typenums, pmanufacturers, pmodels, productCode)
// Filter for matching rows and add key if exists
const updateRows = filterMnfcrmodRows(convertedMnfcrmodRows, existingRowsResponse)
// Run upsert query
const upsertResponse = await multiRowMnfcrModUpsertQuery(connection, updateRows)
return (`mnfcrmod affected rows: ${upsertResponse?.affectedRows}`)
}
export async function upsertMnfcrmodModelRows(connection, modelArr, productCode) {
const responses = []
// Need to filter into mnfcrmod vs model rows
const nonVehicleUnitRows = modelArr.filter(row => row.vehicleUnit === 'False')
const vehicleUnitRows = modelArr.filter(row => row.vehicleUnit === 'True')
try {
if (nonVehicleUnitRows.length) {
// Convert
const mnfcrmodUpsertResponse = await upsertMnfcrmodRows(connection, nonVehicleUnitRows, productCode)
responses.push(mnfcrmodUpsertResponse)
}
if (vehicleUnitRows.length) {
// Convert rows to HTP
const convertedModelRows = vehicleUnitRows.map(row => convertModel(row, productCode))
const modelUpsertResponse = await multiRowUpsertQuery(connection, 'model', convertedModelRows)
responses.push(`model affected rows: ${modelUpsertResponse?.affectedRows}`)
}
return {
success: true,
request: '',
data: responses
}
} catch (err) {
return {
success: false,
request: '',
data: err
}
}
}
async function deleteMnfcrmod(connection, typeNum, manufacturerName, modelName, firstYear, lastYear, productCode ) {
const queryOptions = {
sql: `DELETE FROM mnfcrmod WHERE productcode = ? AND typenum = ? AND pmanufacturer = ? AND pmodel = ? AND firstyear = ? AND lastyear = ?`,
values: [ productCode, typeNum, manufacturerName, modelName, firstYear, lastYear ]
}
return await query(connection, queryOptions)
}
async function deleteModel(connection, manufacturerName, modelName, firstYear, lastYear, productCode) {
const queryOptions = {
sql: `DELETE FROM model WHERE productcode = ? AND Make = ? AND Model = ? AND FirstYear = ? AND LastYear = ?`,
values: [ productCode, manufacturerName, modelName, firstYear, lastYear]
}
return await query(connection, queryOptions)
}
export async function deleteMnfcrmodModelRow(connection, typeNum, manufacturerName, modelName, firstYear, lastYear, vehicleUnit, productCode ) {
const responses = []
// Get manufacturer name
// Get vehicleunit true/false
try {
if (vehicleUnit === 'False') {
// delete from mnfcrmodel
const deleteResult = await deleteMnfcrmod(connection, typeNum, manufacturerName, modelName, firstYear, lastYear, productCode )
responses.push(`mnfcrmod delete affected rows: ${deleteResult?.affectedRows}`)
} else if (vehicleUnit === 'True') {
// delete from Model
const deleteResult = await deleteModel(connection, manufacturerName, modelName, firstYear, lastYear, productCode)
responses.push(`Model delete affected rows: ${deleteResult?.affectedRows}`)
}
return {
success: true,
request: '',
data: responses
}
} catch (err) {
return {
success: false,
request: '',
data: err
}
}
}
export async function insertMnfcrmodModelRows(connection, modelArr, productCode) {
const responses = []
// Need to filter into mnfcrmod vs model rows
const nonVehicleUnitRows = modelArr.filter(row => row.vehicleUnit === 'False')
const vehicleUnitRows = modelArr.filter(row => row.vehicleUnit === 'True')
try {
if (nonVehicleUnitRows.length) {
// Convert rows to HTP mnfcrmod
const convertedNonVehicleRows = nonVehicleUnitRows.map(row => convertMnfcrmod(row, productCode))
const nonVehicleColumns = Object.keys(convertedNonVehicleRows[0])
const nonVehicleValues = convertedNonVehicleRows.map(obj => Object.values(obj))
const mnfcrmodInsertResponse = await multiRowInsertQuery(connection, 'mnfcrmod', nonVehicleColumns, nonVehicleValues)
responses.push(`mnfcrmod affected rows: ${mnfcrmodInsertResponse?.affectedRows}`)
}
if (vehicleUnitRows.length) {
// Convert rows to HTP model
const convertedModelRows = vehicleUnitRows.map(row => convertModel(row, productCode))
const vehicleColumns = Object.keys(convertedModelRows[0])
const vehicleValues = convertedModelRows.map(obj => Object.values(obj))
const modelInsertResponse = await multiRowInsertQuery(connection, 'model', vehicleColumns, vehicleValues)
responses.push(`model affected rows: ${modelInsertResponse?.affectedRows}`)
}
return {
success: true,
request: '',
data: responses
}
} catch (err) {
return {
success: false,
request: '',
data: err
}
}
}
import { multiRowUpsertQuery, query } from './shared.js'
function convertPartListAuthorityMap(inventoryTypeRow, productCode) {
return {
partlisttype: 'inventorytype',
productcode: productCode,
sourceid: inventoryTypeRow.inventoryTypeId,
partlistauthorityid: inventoryTypeRow.partListAuthorityId,
}
}
export async function upsertPartListAuthorityMapRows(connection, inventoryTypeArr, productCode) {
const partListRows = inventoryTypeArr.filter(row => row.partListAuthorityId)
if (partListRows.length) {
try {
const convertedRows = partListRows.map(row => convertPartListAuthorityMap(row, productCode))
const queryResponse = await multiRowUpsertQuery(connection, 'partlistauthoritymap', convertedRows)
return {
success: true,
request: '',
data: queryResponse.affectedRows || 'none'
}
} catch (err) {
return {
success: false,
request: '',
data: err
}
}
} else {
return {
success: true,
request: '',
data: 'none'
}
}
}
export async function deletePartListAuthorityMapRow(connection, productCode, inventoryType, partListAuthorityId) {
try {
const queryOptions = {
sql: `DELETE FROM partlistauthoritymap WHERE productcode = ? AND partlisttype = 'inventorytype' AND sourceid = ? AND partlistauthorityid = ?`,
values: [productCode, inventoryType, partListAuthorityId],
}
const deleteResponse = await query(connection, queryOptions)
//responses.push(`partlistauthoritymap affected rows: ${deleteResponse?.affectedRows}`)
return `partlistauthoritymap delete affected rows: ${deleteResponse?.affectedRows}`
} catch (err) {
return `error deleting partlistauthoritymap rows: ${err}`
}
}
import mysql from 'mysql2/promise'
import pProps from 'p-props'
import { upsertPartListAuthorityMapRows, deletePartListAuthorityMapRow } from './partListAuthority.js'
import { multiRowUpsertQuery, query } from './shared.js'
function convertInventoryType(row, productCode) {
return {
typenum: row.inventoryTypeId,
part: row.name,
set: row.typeSetId,
used: row.active, //boolean
manmod: row.useManufacturer,
productcode: productCode,
lastupdate: mysql.raw('NOW()'),
// more fields are excluded as they're part of a unified tags object
}
}
// Multiple row upsert
export async function upsertPartUseRows(connection, inventoryTypeArr, productCode) {
const responses = []
try {
// Convert to HTP insertable object
const convertedRows = inventoryTypeArr.map(row => convertInventoryType(row, productCode))
// Use pProps to run both database operations in parallel
const results = await pProps({
partUse: multiRowUpsertQuery(connection, 'partuse', convertedRows),
partListAuthorityMap: upsertPartListAuthorityMapRows(connection, inventoryTypeArr, productCode)
})
// Process results
responses.push(
`partUse affected rows: ${results.partUse.affectedRows}`,
)
if (results.partListAuthorityMap.success) {
responses.push(
`partlistauthoritymap affected rows: ${results.partListAuthorityMap.data}`,
)
} else {
responses.push(
`partlistauthoritymap error: ${results.partListAuthorityMap.data}`,
)
}
return ({
success: true,
request: '',
data: responses,
})
} catch (err) {
return {
success: false,
request: '',
data: err,
}
}
}
export async function deletePartUseRow(connection, productCode, inventoryType, partListAuthorityId) {
const responses = []
// Partuse delete query
const queryOptions = {
sql: `DELETE FROM partuse WHERE productcode = ? AND typenum = ?`,
values: [productCode, inventoryType]
}
try {
const partuseDeleteResult = await query(connection, queryOptions)
responses.push(`partuse delete affected rows: ${partuseDeleteResult?.affectedRows}`)
// Delete partlistauthoritymap row if exists
if (partListAuthorityId) {
const partListAuthorityDeleteResponse = await deletePartListAuthorityMapRow(connection, productCode, inventoryType, partListAuthorityId)
responses.push(partListAuthorityDeleteResponse)
}
return {
success: true,
request: '',
data: responses
}
} catch (err) {
return {
success: false,
request: '',
data: err
}
}
}
import { multiRowUpsertQuery, query } from './shared.js'
function convertSellPriceContract(row, productCode ) {
return {
productcode: productCode,
sellpriceclassid: row.sellPriceClassId,
name: row.name,
parentsellpriceclassid: row.parentSellPriceClassId
}
}
export async function upsertSellPriceClassRows(connection, sellPriceClassArr, productCode) {
const responses = []
try {
const convertedRows = sellPriceClassArr.map(row => convertSellPriceContract(row, productCode))
const multiRowUpsertResponse = await multiRowUpsertQuery(connection, 'sellpriceclass', convertedRows)
responses.push(`sellpriceclass affected rows: ${multiRowUpsertResponse.affectedRows}`)
return {
success: true,
data: responses
}
} catch (err) {
return {
success: false,
data: err
}
}
}
// Do we need parentsellpriceclassid too?
export async function deleteSellPriceClassRow(connection, productCode, sellPriceClassId) {
const responses = []
try {
const queryOptions = {
sql: `DELETE FROM sellpriceclass WHERE productcode = ? AND sellpriceclassid = ?`,
values: [productCode, sellPriceClassId],
}
const deleteResponse = await query(connection, queryOptions)
responses.push(`sellpriceclass delete affected rows: ${deleteResponse?.affectedRows}`)
return {
success: true,
data: responses
}
} catch (err) {
return {
success: false,
data: err
}
}
}
export async function query(connection, queryOptions) {
// mysql2/promise returns [rows, fields]
// Both Pool and Connection have .query() method
// Pool.query() automatically manages connection get/release
const [rows] = await connection.query(queryOptions);
return rows;
}
export async function queryFirst(connection, queryOptions) {
// mysql2/promise returns [rows, fields]
// Both Pool and Connection have .query() method
// Pool.query() automatically manages connection get/release
const [rows] = await connection.query(queryOptions);
return rows[0] || null;
}
export async function write(connection, queryOptions) {
// mysql2/promise returns [ResultSetHeader, fields] for write operations
// ResultSetHeader contains: affectedRows, insertId, warningStatus, etc.
// Both Pool and Connection have .query() method
// Pool.query() automatically manages connection get/release
const [result] = await connection.query(queryOptions);
return result;
}
export async function upsertQuery(connection, table, row) {
const queryOptions = {
sql: "INSERT INTO ?? SET ? ON DUPLICATE KEY UPDATE ?",
values: [ table, row, row ],
}
return await query(connection, queryOptions)
}
export async function multiRowUpsertQuery(htpConnection, table, objectArray) {
// Get columns
const columns = Object.keys(objectArray[0])
const values = objectArray.map(obj => Object.values(obj))
// Duplicate the columns so it can be passed in query values
const doubleColumns = columns.flatMap(i => [ i, i ])
// Create query string addition for all of the columns to be updated
const queryStringAdd = columns.map(row => `?? = VALUES(??)`)
const queryOptions = {
sql: `INSERT INTO ?? (??) VALUES ? ON DUPLICATE KEY UPDATE${ queryStringAdd}`,
values: [ table, columns, values, ...doubleColumns ],
}
return await query(htpConnection, queryOptions)
}
export async function multiRowInsertQuery(connection, table, columns, columnValues) {
const queryOptions = {
sql: `INSERT IGNORE INTO ?? (??) VALUES ?`,
values: [ table, columns, columnValues ],
}
return await query(connection, queryOptions)
}
import fs from 'fs';
// Memory tracking utilities
function logMemoryUsage(label) {
const memoryUsage = process.memoryUsage();
console.log(`${label}:`, {
rss: `${Math.round(memoryUsage.rss / 1024 / 1024)} MB`,
heapTotal: `${Math.round(memoryUsage.heapTotal / 1024 / 1024)} MB`,
heapUsed: `${Math.round(memoryUsage.heapUsed / 1024 / 1024)} MB`,
external: `${Math.round(memoryUsage.external / 1024 / 1024)} MB`
});
// Write to a CSV for later analysis
appendToCSV(`${label},${memoryUsage.rss},${memoryUsage.heapTotal},${memoryUsage.heapUsed},${memoryUsage.external}`);
}
function appendToCSV(line) {
fs.appendFileSync('memory-usage.csv', line + '\n');
}
// Initialize CSV file with headers
function initCSV() {
fs.writeFileSync('memory-usage.csv', 'Operation,RSS,HeapTotal,HeapUsed,External\n');
}
// Generate realistic test data
function generateInventoryTestData(size) {
const inventoryArr = [];
for (let i = 0; i < size; i++) {
inventoryArr.push({
inventoryId: `PART${i}`,
storeId: `STORE${i % 5}`,
inventoryType: `TYPE${i % 10}`,
vehicle: {
id: `VEH${i}`,
vin: `VIN${i}`,
year: 2020 + (i % 5),
make: `Make${i % 10}`,
model: `Model${i % 20}`
},
description: `Test part ${i}`,
interchange: {
number: `INT${i}`,
subNumber: `SUB${i}`
},
status: 'Active',
retailPrice: 100 + i,
wholesalePrice: 50 + i,
deplete: 0,
availableQuantity: 10,
sellableQuantity: 8,
tags: [
{ label: `Color`, value: `Value${i % 5}` },
{ label: `Size`, value: `Size${i % 3}` },
{ label: `Material`, value: `Material${i % 4}` }
],
cost: 25 + i,
manufacturer: { name: `Manufacturer${i % 10}` },
model: { name: `ModelName${i % 15}` },
parentManufacturer: { name: `ParentManufacturer${i % 8}` },
parentModel: { name: `ParentModel${i % 12}` },
taxable: 1,
oemNumber: `OEM${i}`,
condition: `Used`,
side: `Left`,
tagNumber: `TAG${i}`,
upc: `UPC${i}`,
category: `Category${i % 5}`,
shippingWeight: { value: 5 + (i % 10) },
public: 1,
displayOnline: 1,
notes: `Test notes for part ${i}`,
shippingDimensions: {
length: 10 + i,
width: 5 + i,
height: 3 + i
},
retailCore: 20,
location: `LOC${i}`,
webSaleClass: `Class${i % 3}`,
vendorId: `VENDOR${i % 5}`,
jobberPrice: 75 + i,
distributorPrice: 60 + i,
averageCost: 30 + i,
sellPriceClassId: i % 3,
dateEntered: new Date().toISOString().slice(0, 19).replace('T', ' '),
dateModified: new Date().toISOString().slice(0, 19).replace('T', ' '),
dateSold: null
});
}
return inventoryArr;
}
// Simulate operations without actually calling the real functions
async function simulateUpsert(data) {
console.log(`simulateUpsert: Processing ${data.length} items`);
// Process the data in a way similar to the real function
// but without actually calling the database
for (const item of data) {
// Do some processing to simulate memory usage
const processed = JSON.parse(JSON.stringify(item));
processed.processed = true;
// No need for timeout in the loop - it slows things down too much
}
console.log(`simulateUpsert: Completed processing ${data.length} items`);
}
async function simulateInsert(data) {
console.log(`simulateInsert: Processing ${data.length} items`);
// Similar to simulateUpsert but with different processing
for (const item of data) {
const processed = JSON.parse(JSON.stringify(item));
processed.inserted = true;
// No need for timeout in the loop
}
console.log(`simulateInsert: Completed processing ${data.length} items`);
}
async function simulateDelete(data) {
console.log(`simulateDelete: Processing ${data.length} items`);
// Simulate delete operation
for (const item of data) {
const id = item.inventoryId;
const store = item.storeId;
// Simulate some processing
const deleteInfo = { id, store, deleted: true };
// No need for timeout in the loop
}
console.log(`simulateDelete: Completed processing ${data.length} items`);
}
// Main test function
async function runMemoryTest() {
initCSV();
console.log("Starting memory leak detection test...");
try {
const iterations = 10; // Run 10 iterations
const batchSizes = [100, 500, 1000]; // Test with different data sizes
// Test each operation type with different batch sizes
for (const size of batchSizes) {
console.log(`\n=== Testing with batch size: ${size} ===\n`);
// Run multiple iterations to detect memory growth patterns
for (let i = 0; i < iterations; i++) {
console.log(`\n--- Iteration ${i+1}/${iterations} ---`);
// Generate fresh test data for this iteration
const testData = generateInventoryTestData(size);
// Force garbage collection if running with --expose-gc
if (global.gc) {
global.gc();
logMemoryUsage(`Iteration ${i+1}, Size ${size}, After GC`);
}
// Test upsert operation
logMemoryUsage(`Iteration ${i+1}, Size ${size}, Before upsert`);
console.log(`Starting upsert simulation for iteration ${i+1}, size ${size}`);
await simulateUpsert(testData);
console.log(`Completed upsert simulation for iteration ${i+1}, size ${size}`);
logMemoryUsage(`Iteration ${i+1}, Size ${size}, After upsert`);
// Test insert operation
logMemoryUsage(`Iteration ${i+1}, Size ${size}, Before insert`);
console.log(`Starting insert simulation for iteration ${i+1}, size ${size}`);
await simulateInsert(testData);
console.log(`Completed insert simulation for iteration ${i+1}, size ${size}`);
logMemoryUsage(`Iteration ${i+1}, Size ${size}, After insert`);
// Test delete operation (for a subset of the data)
const deleteSubset = testData.slice(0, Math.min(3, size));
logMemoryUsage(`Iteration ${i+1}, Size ${size}, Before delete`);
console.log(`Starting delete simulation for iteration ${i+1}, size ${size}`);
await simulateDelete(deleteSubset);
console.log(`Completed delete simulation for iteration ${i+1}, size ${size}`);
logMemoryUsage(`Iteration ${i+1}, Size ${size}, After delete`);
// Shorter sleep between iterations
await new Promise(resolve => setTimeout(resolve, 10));
console.log(`Completed iteration ${i+1}/${iterations} for batch size ${size}`);
}
}
console.log("\nMemory leak detection test completed.");
console.log("Check memory-usage.csv for detailed memory usage data.");
} catch (error) {
console.error("Error during memory test:", error);
}
}
// Create a simple visualization function to analyze results in the terminal
function analyzeResults() {
console.log("\n=== Memory Usage Analysis ===\n");
try {
const data = fs.readFileSync('memory-usage.csv', 'utf8')
.split('\n')
.filter(line => line.trim())
.map(line => line.split(','));
const headers = data.shift();
// Group by batch size and operation type
const groupedData = {};
data.forEach(row => {
const operation = row[0];
const heapUsed = parseFloat(row[3]);
if (operation.includes('Size')) {
const size = operation.match(/Size (\d+)/)[1];
const type = operation.includes('Before') ? 'Before' : 'After';
const op = operation.includes('upsert') ? 'upsert' :
operation.includes('insert') ? 'insert' : 'delete';
const key = `${size}-${op}-${type}`;
if (!groupedData[key]) {
groupedData[key] = [];
}
groupedData[key].push(heapUsed);
}
});
// Calculate averages and detect potential leaks
console.log("Average Heap Usage (MB) by Operation:");
console.log("------------------------------------");
Object.keys(groupedData).forEach(key => {
const values = groupedData[key];
const avg = values.reduce((sum, val) => sum + val, 0) / values.length;
console.log(`${key}: ${avg.toFixed(2)} MB`);
});
// Look for memory growth patterns
console.log("\nMemory Growth Analysis:");
console.log("----------------------");
const batchSizes = [...new Set(Object.keys(groupedData).map(k => k.split('-')[0]))];
const operations = ['upsert', 'insert', 'delete'];
batchSizes.forEach(size => {
operations.forEach(op => {
const beforeKey = `${size}-${op}-Before`;
const afterKey = `${size}-${op}-After`;
if (groupedData[beforeKey] && groupedData[afterKey]) {
const beforeVals = groupedData[beforeKey];
const afterVals = groupedData[afterKey];
// Check for consistent growth
let growthCount = 0;
for (let i = 1; i < Math.min(beforeVals.length, afterVals.length); i++) {
if (beforeVals[i] > beforeVals[i-1]) {
growthCount++;
}
}
const growthPercentage = (growthCount / (beforeVals.length - 1)) * 100;
if (growthPercentage > 70) {
console.log(`POTENTIAL LEAK: ${op} with size ${size} shows ${growthPercentage.toFixed(0)}% growth pattern`);
}
// Check for memory not being released
const avgDiff = afterVals.reduce((sum, val, i) => sum + (val - beforeVals[i]), 0) / afterVals.length;
console.log(`${op} with size ${size}: Average memory difference: ${avgDiff.toFixed(2)} MB`);
}
});
});
} catch (error) {
console.error("Error analyzing results:", error);
}
}
// Run the test and then analyze results
async function main() {
await runMemoryTest();
analyzeResults();
}
// Execute the main function
main().catch(console.error);
// Test to confirm that the Map implementation of convertManyInventoryOptionValue behaves the same as the original implementation
import assert from 'assert'
import mysql from 'mysql2/promise'
// Original implementation of convertManyInventoryOptionValue
function originalConvertManyInventoryOptionValue(inventoryOptionListData, productInventoryRow, productCode) {
const tags = productInventoryRow.tags
const inventoryOptionRows = tags.map(function (tag) {
if (tag.value && tag.label) {
// find optionnum
const inventoryOptionListRow = inventoryOptionListData.find(element => tag.label === element.option && productInventoryRow.typeNum === element.sourcetypenum && (productInventoryRow.partListAuthorityId ? productInventoryRow.partListAuthorityId === element.typenum : productInventoryRow.typeNum === element.typenum))
if (inventoryOptionListRow) {
const { optionnum } = inventoryOptionListRow
return {
productcode: productCode,
storenum: productInventoryRow.storeId,
partnum: productInventoryRow.inventoryId,
value: tag.value,
optionnum,
lastupdate: mysql.raw('NOW()'),
}
} else {
console.debug(`No matching inventoryoptionlist row found for inventoryRow.tag.label ${tag.label} of inventory id ${productInventoryRow.inventoryId}`)
return
}
}
})
return inventoryOptionRows.filter(row => row !== undefined)
}
// New Map-based implementation of convertManyInventoryOptionValue
function newConvertManyInventoryOptionValue(inventoryOptionListData, productInventoryRow, productCode) {
// Create a Map for O(1) lookups instead of O(n) with .find()
const optionListMap = new Map()
// Build the lookup map with composite keys for faster access
inventoryOptionListData.forEach(element => {
// Create a unique key combining the fields we match on
const key = `${element.option}|${element.sourcetypenum}|${element.typenum}`
optionListMap.set(key, element)
})
const tags = productInventoryRow.tags
const inventoryOptionRows = []
// Process each tag
for (const tag of tags) {
if (tag.value && tag.label) {
// Create lookup keys - one for each possible match scenario
const typeNumKey = productInventoryRow.partListAuthorityId
? `${tag.label}|${productInventoryRow.typeNum}|${productInventoryRow.partListAuthorityId}`
: `${tag.label}|${productInventoryRow.typeNum}|${productInventoryRow.typeNum}`
// Get the matching row using O(1) lookup
const inventoryOptionListRow = optionListMap.get(typeNumKey)
if (inventoryOptionListRow) {
const { optionnum } = inventoryOptionListRow
inventoryOptionRows.push({
productcode: productCode,
storenum: productInventoryRow.storeId,
partnum: productInventoryRow.inventoryId,
value: tag.value,
optionnum,
lastupdate: mysql.raw('NOW()'),
})
} else {
console.debug(`No matching inventoryoptionlist row found for inventoryRow.tag.label ${tag.label} of inventory id ${productInventoryRow.inventoryId}`)
}
}
}
return inventoryOptionRows
}
// Mock data for testing
const mockInventoryOptionListData = [
{
optionnum: 1,
option: 'Color',
sourcetypenum: 'type1',
typenum: 'type1',
rank: 1,
productcode: 'PROD123'
},
{
optionnum: 2,
option: 'Size',
sourcetypenum: 'type1',
typenum: 'type1',
rank: 2,
productcode: 'PROD123'
},
{
optionnum: 3,
option: 'Material',
sourcetypenum: 'type2',
typenum: 'auth1',
rank: 3,
productcode: 'PROD123'
},
{
optionnum: 4,
option: 'Weight',
sourcetypenum: 'type2',
typenum: 'type2',
rank: 4,
productcode: 'PROD123'
}
]
// Test cases
const testCases = [
// Case 1: Basic case with matching tags
{
productInventoryRow: {
typeNum: 'type1',
storeId: 'store1',
inventoryId: 'inv1',
tags: [
{ label: 'Color', value: 'Red' },
{ label: 'Size', value: 'Large' }
]
},
productCode: 'PROD123',
description: 'Basic case with matching tags'
},
// Case 2: With partListAuthorityId
{
productInventoryRow: {
typeNum: 'type2',
partListAuthorityId: 'auth1',
storeId: 'store1',
inventoryId: 'inv2',
tags: [
{ label: 'Material', value: 'Cotton' }
]
},
productCode: 'PROD123',
description: 'Case with partListAuthorityId'
},
// Case 3: Mixed case with some matching and some non-matching tags
{
productInventoryRow: {
typeNum: 'type1',
storeId: 'store2',
inventoryId: 'inv3',
tags: [
{ label: 'Color', value: 'Blue' },
{ label: 'NonExistent', value: 'Value' }
]
},
productCode: 'PROD123',
description: 'Mixed case with some matching and some non-matching tags'
},
// Case 4: Case with empty or invalid tags
{
productInventoryRow: {
typeNum: 'type2',
storeId: 'store2',
inventoryId: 'inv4',
tags: [
{ label: 'Weight', value: '' },
{ value: 'NoLabel' },
{ label: 'NoValue' }
]
},
productCode: 'PROD123',
description: 'Case with empty or invalid tags'
},
// Case 5: No matching tags
{
productInventoryRow: {
typeNum: 'type3',
storeId: 'store3',
inventoryId: 'inv5',
tags: [
{ label: 'NonExistent1', value: 'Value1' },
{ label: 'NonExistent2', value: 'Value2' }
]
},
productCode: 'PROD123',
description: 'No matching tags'
}
]
// Helper function to compare results
function compareResults(original, newResult, description) {
console.log(`\nTesting: ${description}`)
// Check if arrays have the same length
assert.strictEqual(
original.length,
newResult.length,
`Arrays should have the same length: original=${original.length}, new=${newResult.length}`
)
if (original.length === 0) {
console.log('Both implementations returned empty arrays as expected.')
return
}
// Sort both arrays by optionnum to ensure consistent order for comparison
const sortedOriginal = [...original].sort((a, b) => a.optionnum - b.optionnum)
const sortedNew = [...newResult].sort((a, b) => a.optionnum - b.optionnum)
// Check each object in the arrays
for (let i = 0; i < sortedOriginal.length; i++) {
const originalObj = sortedOriginal[i]
const newObj = sortedNew[i]
// Compare each property except lastupdate which is a function
for (const key in originalObj) {
if (key !== 'lastupdate') {
assert.strictEqual(
originalObj[key],
newObj[key],
`Property ${key} should be the same at index ${i}: original=${originalObj[key]}, new=${newObj[key]}`
)
} else {
// For lastupdate, just check that both are mysql.raw functions
assert.strictEqual(
typeof originalObj[key],
typeof newObj[key],
`Property ${key} should be the same type at index ${i}`
)
}
}
}
console.log('āœ… Both implementations produce identical results for this case.')
}
// Run the tests
function runTests() {
console.log('Running tests to compare original and Map implementations of convertManyInventoryOptionValue...')
// Test each case
testCases.forEach(testCase => {
const originalResult = originalConvertManyInventoryOptionValue(
mockInventoryOptionListData,
testCase.productInventoryRow,
testCase.productCode
)
const newResult = newConvertManyInventoryOptionValue(
mockInventoryOptionListData,
testCase.productInventoryRow,
testCase.productCode
)
compareResults(originalResult, newResult, testCase.description)
})
console.log('\nāœ… All tests passed! Both implementations produce identical results.')
// Performance test
console.log('\nRunning performance test...')
// Create larger test data
const largeInventoryOptionListData = []
for (let i = 0; i < 1000; i++) {
largeInventoryOptionListData.push({
optionnum: i,
option: `Option${i % 100}`,
sourcetypenum: `type${i % 10}`,
typenum: `type${i % 10}`,
rank: i,
productcode: 'PROD123'
})
}
const largeProductInventoryRow = {
typeNum: 'type5',
storeId: 'store1',
inventoryId: 'inv1',
tags: []
}
// Add 100 tags to the product inventory row
for (let i = 0; i < 100; i++) {
largeProductInventoryRow.tags.push({
label: `Option${i}`,
value: `Value${i}`
})
}
// Measure performance of original implementation
console.time('Original implementation')
originalConvertManyInventoryOptionValue(largeInventoryOptionListData, largeProductInventoryRow, 'PROD123')
console.timeEnd('Original implementation')
// Measure performance of new implementation
console.time('Map implementation')
newConvertManyInventoryOptionValue(largeInventoryOptionListData, largeProductInventoryRow, 'PROD123')
console.timeEnd('Map implementation')
}
// Execute the tests
runTests()
// Test to confirm that the flatMap implementation behaves the same as the original reduce implementation
import assert from 'assert';
// Mock data that mimics the structure expected by the upsertInventorySourceRows function
const mockProductInventoryRows = [
{
inventoryId: 'inv001',
openSources: [
{
inventoryStoreId: 'store1',
quantity: 10,
quantityAllocated: 5,
documentType: 'order',
documentStoreId: 'docStore1',
documentLineId: 'line1',
date: '2025-05-13',
status: 'active'
},
{
inventoryStoreId: 'store2',
quantity: 15,
quantityAllocated: 7,
documentType: 'order',
documentStoreId: 'docStore1',
documentLineId: 'line2',
date: '2025-05-13',
status: 'active'
}
]
},
{
inventoryId: 'inv002',
openSources: [
{
inventoryStoreId: 'store3',
quantity: 20,
quantityAllocated: 10,
documentType: 'order',
documentStoreId: 'docStore2',
documentLineId: 'line3',
date: '2025-05-13',
status: 'active'
}
]
},
{
inventoryId: 'inv003',
openSources: [] // Empty array to test edge case
}
];
const mockProductCode = 'PROD123';
// Original reduce implementation
function originalReduceImplementation(productInventoryRows, productCode) {
return productInventoryRows.reduce((sourceRowArray, inventoryRow) => {
inventoryRow.openSources.forEach(sourceRow => {
sourceRowArray.push({
inventorystoreid: sourceRow.inventoryStoreId,
quantity: sourceRow.quantity,
quantityallocated: sourceRow.quantityAllocated,
documenttype: sourceRow.documentType,
documentstoreid: sourceRow.documentStoreId,
documentlineid: sourceRow.documentLineId,
date: sourceRow.date,
status: sourceRow.status,
productcode: productCode,
inventoryid: inventoryRow.inventoryId,
});
});
return sourceRowArray;
}, []);
}
// New flatMap implementation
function newFlatMapImplementation(productInventoryRows, productCode) {
return productInventoryRows.flatMap(inventoryRow =>
inventoryRow.openSources.map(sourceRow => ({
inventorystoreid: sourceRow.inventoryStoreId,
quantity: sourceRow.quantity,
quantityallocated: sourceRow.quantityAllocated,
documenttype: sourceRow.documentType,
documentstoreid: sourceRow.documentStoreId,
documentlineid: sourceRow.documentLineId,
date: sourceRow.date,
status: sourceRow.status,
productcode: productCode,
inventoryid: inventoryRow.inventoryId,
}))
);
}
// Run the test
function runTest() {
console.log('Running test to compare reduce and flatMap implementations...');
const reduceResult = originalReduceImplementation(mockProductInventoryRows, mockProductCode);
const flatMapResult = newFlatMapImplementation(mockProductInventoryRows, mockProductCode);
// Check if arrays have the same length
assert.strictEqual(
reduceResult.length,
flatMapResult.length,
`Arrays should have the same length: reduce=${reduceResult.length}, flatMap=${flatMapResult.length}`
);
// Check if each object in the arrays has the same properties and values
for (let i = 0; i < reduceResult.length; i++) {
const reduceObj = reduceResult[i];
const flatMapObj = flatMapResult[i];
// Compare each property
for (const key in reduceObj) {
assert.strictEqual(
reduceObj[key],
flatMapObj[key],
`Property ${key} should be the same at index ${i}: reduce=${reduceObj[key]}, flatMap=${flatMapObj[key]}`
);
}
}
// Deep equality check for the entire arrays
assert.deepStrictEqual(
reduceResult,
flatMapResult,
'Both implementations should produce identical results'
);
console.log('āœ… Test passed! Both implementations produce identical results.');
console.log('Original reduce result:', JSON.stringify(reduceResult, null, 2));
console.log('New flatMap result:', JSON.stringify(flatMapResult, null, 2));
}
// Execute the test
runTest();