@isoftdata/universal-object-htp-utility
Advanced tools
+52
| 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 | ||
| } | ||
| } | ||
| } |
+137
| 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 | ||
| } | ||
| } | ||
| } | ||
+73
| 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; | ||
| }>; |
+204
| 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); | ||
| } |
+155
| 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 {}; |
+105
| 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' |
+335
| 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 | ||
| } |
+82
| 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 | ||
| } | ||
| } | ||
| } |
+218
| 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}` | ||
| } | ||
| } | ||
+91
| 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 | ||
| } | ||
| } | ||
| } |
+55
| 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 | ||
| } | ||
| } | ||
| } |
+69
| 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"] | ||
| } |
+156
| // 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" | ||
| } | ||
| } |
-52
| 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 | ||
| } | ||
| } | ||
| } |
-138
| 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 | ||
| } | ||
| } | ||
| } | ||
-72
| 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 | ||
| } | ||
| } | ||
| } |
-123
| 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, | ||
| // } | ||
-348
| 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 | ||
| } | ||
| } | ||
| } |
-475
| 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 | ||
| } |
-82
| 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 | ||
| } | ||
| } | ||
| } |
-218
| 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}` | ||
| } | ||
| } | ||
-91
| 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 | ||
| } | ||
| } | ||
| } |
-58
| 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(); |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
No tests
QualityPackage does not have any tests. This is a strong signal of a poorly maintained or low quality package.
318714
132.21%91
264%7239
134.04%1
-50%4
Infinity%4
33.33%1
Infinity%