Initial revision

Signed-off-by: James Ketrenos <james_eikona@ketrenos.com>
This commit is contained in:
James Ketrenos 2021-04-26 17:55:27 -07:00
commit 33131716ce
9 changed files with 2948 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
node_modules
cache
whiskies.db

2
LICENSE Normal file
View File

@ -0,0 +1,2 @@
Copyright (C) 2021 James Ketrenos
All Rights Reserved

474
graphql/whisky.js Normal file
View File

@ -0,0 +1,474 @@
const graphql = require("graphql");
const sqlite3 = require('sqlite3').verbose();
const database = new sqlite3.Database("whiskies.db");
const createWhiskiesTable = () => {
const query = `
CREATE TABLE IF NOT EXISTS Whiskies (
code text UNIQUE PRIMARY KEY,
description text,
lastSeen text,
size integer,
price real
)`;
return database.run(query);
}
const createLocationsTable = () => {
const query = `
CREATE TABLE IF NOT EXISTS Locations (
code text UNIQUE PRIMARY KEY,
city text,
address text,
latitude real,
longitude real,
phone text
)`;
return database.run(query);
}
const createInventoriesTable = () => {
const query = `
CREATE TABLE IF NOT EXISTS Inventories (
id integer PRIMARY KEY,
whisky integer,
location integer,
quantity integer,
updated text
)`;
return database.run(query);
}
createWhiskiesTable();
createInventoriesTable();
createLocationsTable();
const WhiskyType = new graphql.GraphQLObjectType({
name: "Whisky",
fields: () => ({
code: { type: graphql.GraphQLString },
description: { type: graphql.GraphQLString },
lastSeen: { type: graphql.GraphQLString },
size: { type: graphql.GraphQLInt },
price: { type: graphql.GraphQLFloat },
inventories: { type: graphql.GraphQLList(InventoryType) }
})
});
const LocationType = new graphql.GraphQLObjectType({
name: "Location",
fields: () => ({
code: { type: graphql.GraphQLString },
address: { type: graphql.GraphQLString },
city: { type: graphql.GraphQLString },
phone: { type: graphql.GraphQLString },
longitude: { type: graphql.GraphQLFloat },
latitude: { type: graphql.GraphQLFloat },
inventories: { type: graphql.GraphQLList(InventoryType) }
})
});
const InventoryType = new graphql.GraphQLObjectType({
name: "Inventory",
fields: {
id: { type: graphql.GraphQLID },
location: { type: LocationType },
whisky: { type: WhiskyType },
quantity: { type: graphql.GraphQLInt },
updated: { type: graphql.GraphQLString }
}
});
const buildWhiskies = (rows) => {
const whiskies = {};
rows.forEach(row => {
let whisky = {};
if (!(row.code in whiskies)) {
whisky = {
description: row.description,
price: row.price,
size: row.size,
code: row.code,
inventories: []
}
whiskies[row.code] = whisky;
} else {
whisky = whiskies[row.code];
}
if (row.quantity) {
whisky.inventories.push({
location: {
code: row.location,
city: row.city,
address: row.address,
phone: row.phone,
latitude: row.latitude,
longitude: row.longitude
},
quantity: row.quantity,
updated: row.updated
});
}
});
const results = [];
for (let key in whiskies) {
results.push(whiskies[key]);
}
return results;
};
const buildLocations = (rows) => {
const locations = {};
rows.forEach(row => {
let location = {};
if (!(row.code in locations)) {
location = {
code: row.code,
address: row.address,
city: row.city,
phone: row.phone,
latitude: row.latitude,
longitude: row.longitude,
inventories: []
};
locations[row.code] = location;
} else {
location = locations[row.code];
}
if (row.quantity) {
location.inventories.push({
whisky: {
code: row.whisky,
description: row.description,
price: row.price,
size: row.size,
},
quantity: row.quantity,
updated: row.updated
});
}
});
const results = [];
for (let key in locations) {
results.push(locations[key]);
}
return results;
};
const buildInventories = (rows) => {
const inventories = [];
rows.forEach(row => {
const inventory = {
whisky: { code: row.whisky, description: row.description, price: row.price, size: row.size },
location: { code: row.location, address: row.address, city: row.city, phone: row.phone, latitude: row.latitude, longitude: row.longitude },
quantity: row.quantity,
updated: row.updated
};
inventories.push(inventory);
});
return inventories;
};
var queryType = new graphql.GraphQLObjectType({
name: 'Query',
fields: {
Whiskies: {
type: graphql.GraphQLList(WhiskyType),
resolve: (root, args, context, info) => {
return new Promise((resolve, reject) => {
database.all(
"SELECT w.*,l.code AS 'location',l.address,l.city,l.phone,l.latitude,l.longitude,i.quantity,i.updated " +
"FROM Whiskies AS w " +
"LEFT JOIN Inventories AS i ON w.code=i.whisky "+
"LEFT JOIN Locations AS l ON l.code=i.location " +
";", function(err, rows) {
if (err) { console.error(err); return reject(null); }
resolve(buildWhiskies(rows));
});
});
}
},
Whisky: {
type: WhiskyType,
args: {
code: { type: new graphql.GraphQLNonNull(graphql.GraphQLString) }
},
resolve: (root, {code}, context, info) => {
return new Promise((resolve, reject) => {
database.all(
"SELECT w.*,l.code AS 'location',l.address,l.city,l.phone,l.latitude,l.longitude,i.quantity,i.updated "+
"FROM Whiskies AS w " +
"LEFT JOIN Inventories AS i ON w.code=i.whisky "+
"LEFT JOIN Locations AS l ON l.code=i.location " +
"WHERE w.code = (?);", [code], function(err, rows) {
if (err) { console.error(err); return reject(null); }
resolve(buildWhiskies(rows)[0]);
});
});
}
},
Locations: {
type: graphql.GraphQLList(LocationType),
resolve: (root, args, context, info) => {
console.log(`Locations resolve`);
return new Promise((resolve, reject) => {
database.all(
"SELECT l.*,w.code AS 'whisky',w.description,w.price,w.size,i.quantity,i.updated " +
"FROM Locations AS l " +
"LEFT JOIN Inventories AS i ON l.code=i.location " +
"LEFT JOIN Whiskies AS w ON w.code=i.whisky " +
";", function(err, rows) {
if (err) { console.error(err); return reject(null); }
resolve(buildLocations(rows));
});
});
}
},
Location: {
type: LocationType,
args: {
code: { type: new graphql.GraphQLNonNull(graphql.GraphQLString) }
},
resolve: (root, {code}, context, info) => {
console.log(`Location resolve`);
return new Promise((resolve, reject) => {
database.all(
"SELECT l.*,w.code AS 'whisky',w.description,w.price,w.size,i.quantity,i.updated " +
"FROM Locations AS l " +
"INNER JOIN Inventories AS i ON l.code=i.location " +
"INNER JOIN Whiskies AS w ON w.code=i.whisky " +
"WHERE code = (?);", [code], function(err, rows) {
if (err) { console.error(err); return reject(null); }
resolve(buildLocations(rows)[0]);
});
});
}
},
Inventories: {
type: graphql.GraphQLList(InventoryType),
resolve: (root, args, context, info) => {
console.log(`Inventories resolve`);
return new Promise((resolve, reject) => {
database.all(
"SELECT * FROM Inventories AS i " +
"INNER JOIN Whiskies AS w ON w.code=i.whisky " +
"INNER JOIN Locations AS l ON l.code=i.location;", function(err, rows) {
if (err) { console.error(err); return reject(null); }
resolve(buildInventories(rows));
});
});
}
},
Inventory: {
type: InventoryType,
args: {
location: { type: new graphql.GraphQLNonNull(graphql.GraphQLString) },
whisky: { type: new graphql.GraphQLNonNull(graphql.GraphQLString) },
},
resolve: (root, {location, whisky}, context, info) => {
console.log(`Inventory resolve`, root, id, context, info);
return new Promise((resolve, reject) => {
database.all(
"SELECT * FROM Inventories AS i " +
"INNER JOIN Whiskies AS w ON w.code=i.whisky " +
"INNER JOIN Locations AS l ON l.code=i.location " +
"WHERE i.whisky = (?) AND i.location = (?);", [whisky, location], function(err, rows) {
if (err) { console.error(err); return reject(null); }
resolve(buildInventories(rows)[0]);
});
});
}
}
}
});
var mutationType = new graphql.GraphQLObjectType({
name: 'Mutation',
fields: {
createWhisky: {
type: WhiskyType,
args: {
description: { type: new graphql.GraphQLNonNull(graphql.GraphQLString) },
code: { type: new graphql.GraphQLNonNull(graphql.GraphQLString) },
lastSeen: { type: new graphql.GraphQLNonNull(graphql.GraphQLString) },
size: { type: new graphql.GraphQLNonNull(graphql.GraphQLInt) },
price: { type: new graphql.GraphQLNonNull(graphql.GraphQLFloat) }
},
resolve: (root, {description, code, lastSeen, size, price}) => {
return new Promise((resolve, reject) => {
database.run('INSERT INTO Whiskies (description, code, lastSeen, size, price) VALUES (?,?,?,?,?);',
[description, code, lastSeen, size, price], (err) => {
if (err) { console.error(err); return reject(null); }
database.get("SELECT last_insert_rowid() AS id", (_err, row) => {
if (_err) { console.error(_err); return reject(null); }
resolve({ description, code, lastSeen, size, price });
});
});
})
}
},
updateWhisky: {
type: graphql.GraphQLString,
args: {
description: { type: new graphql.GraphQLNonNull(graphql.GraphQLString) },
code: { type: new graphql.GraphQLNonNull(graphql.GraphQLString) },
lastSeen: { type: new graphql.GraphQLNonNull(graphql.GraphQLString) },
size: { type: new graphql.GraphQLNonNull(graphql.GraphQLInt) },
price: { type: new graphql.GraphQLNonNull(graphql.GraphQLFloat) }
},
resolve: (root, {code, description, lastSeen, size, price}) => {
return new Promise((resolve, reject) => {
database.run('UPDATE Whiskies SET description = (?), lastSeen = (?), size = (?), price = (?) WHERE code = (?);',
[description, lastSeen, size, price, code], (err) => {
if (err) { console.error(err); return reject(err); }
resolve(`Whisky #${code} updated`);
});
})
}
},
deleteWhisky: {
type: graphql.GraphQLString,
args: {
code: { type: new graphql.GraphQLNonNull(graphql.GraphQLString) }
},
resolve: (root, {code}) => {
return new Promise((resolve, reject) => {
database.run('DELETE from Whiskies WHERE code =(?);', [code], (err) => {
if (err) { console.error(err); return reject(err); }
resolve(`Whisky #${code} deleted`);
});
})
}
},
createLocation: {
type: LocationType,
args: {
code: { type: new graphql.GraphQLNonNull(graphql.GraphQLString) },
address: { type: new graphql.GraphQLNonNull(graphql.GraphQLString) },
city: { type: new graphql.GraphQLNonNull(graphql.GraphQLString) },
phone: { type: new graphql.GraphQLNonNull(graphql.GraphQLString) },
longitude: { type: new graphql.GraphQLNonNull(graphql.GraphQLFloat) },
latitude: { type: new graphql.GraphQLNonNull(graphql.GraphQLFloat) }
},
resolve: (root, {code, address, city, phone, longitude, latitude}) => {
return new Promise((resolve, reject) => {
database.run('INSERT INTO Locations (code, address, city, phone, longitude, latitude) VALUES (?,?,?,?,?,?);',
[code, address, city, phone, longitude, latitude], (err) => {
if (err) { console.error(err); return reject(null); }
resolve({ code, address, city, phone, longitude, latitude });
});
})
}
},
updateLocation: {
type: graphql.GraphQLString,
args: {
code: { type: new graphql.GraphQLNonNull(graphql.GraphQLString) },
address: { type: new graphql.GraphQLNonNull(graphql.GraphQLString) },
city: { type: new graphql.GraphQLNonNull(graphql.GraphQLString) },
phone: { type: new graphql.GraphQLNonNull(graphql.GraphQLString) },
longitude: { type: new graphql.GraphQLNonNull(graphql.GraphQLFloat) },
latitude: { type: new graphql.GraphQLNonNull(graphql.GraphQLFloat) }
},
resolve: (root, {code, address, city, phone, longitude, latitude}) => {
return new Promise((resolve, reject) => {
database.run('UPDATE Locations SET address = (?), city = (?), phone = (?), longitude = (?), latitude = (?) WHERE code = (?);',
[address, city, phone, longitude, latitude, code], (err) => {
if (err) { console.error(err); return reject(null); }
resolve(`Location #${code} updated`);
});
})
}
},
deleteLocation: {
type: graphql.GraphQLString,
args: {
code: { type: new graphql.GraphQLNonNull(graphql.GraphQLString) }
},
resolve: (root, {code}) => {
return new Promise((resolve, reject) => {
database.run('DELETE from Locations WHERE code = (?);', [code], (err) => {
if (err) { console.error(err); return reject(null); }
resolve(`Location #${code} deleted`);
});
})
}
},
createInventory: {
type: InventoryType,
args: {
location: { type: new graphql.GraphQLNonNull(graphql.GraphQLString) },
whisky: { type: new graphql.GraphQLNonNull(graphql.GraphQLString) },
quantity: { type: new graphql.GraphQLNonNull(graphql.GraphQLInt) },
updated: { type: new graphql.GraphQLNonNull(graphql.GraphQLString) }
},
resolve: (root, {location, whisky, quantity, updated}) => {
console.log('createInventory');
return new Promise((resolve, reject) => {
database.run('INSERT INTO Inventories (location, whisky, quantity, updated) VALUES (?,?,?,?);',
[location, whisky, quantity, updated], (err) => {
if (err) { console.error(err); return reject(null); }
database.get("SELECT last_insert_rowid() as id", (_err, row) => {
if (_err) { console.error(_err); return reject(null); }
database.all(
"SELECT * FROM Inventories AS i " +
"INNER JOIN Whiskies AS w ON w.code=i.whisky " +
"INNER JOIN Locations AS l ON l.code=i.location " +
"WHERE i.whisky = (?) AND i.location = (?);", [whisky, location], function(err, rows) {
if (err) { console.error(err); return reject(null); }
resolve(buildInventories(rows)[0]);
});
});
});
})
}
},
updateInventory: {
type: graphql.GraphQLString,
args: {
location: { type: new graphql.GraphQLNonNull(graphql.GraphQLString) },
whisky: { type: new graphql.GraphQLNonNull(graphql.GraphQLString) },
quantity: { type: new graphql.GraphQLNonNull(graphql.GraphQLInt) },
updated: { type: new graphql.GraphQLNonNull(graphql.GraphQLString) }
},
resolve: (root, {location, whisky, quantity, updated}) => {
console.log('updateInventory');
return new Promise((resolve, reject) => {
database.run('UPDATE Inventories SET quantity = (?), updated = (?) WHERE location = (?) AND whisky = (?);',
[ quantity, updated, location, whisky], (err) => {
if (err) { console.error(err); return reject(null); }
resolve(`Inventory for ${location} of ${whisky} updated to ${quantity}:${updated}`);
});
})
}
},
deleteInventory: {
type: graphql.GraphQLString,
args: {
location: { type: new graphql.GraphQLNonNull(graphql.GraphQLString) },
whisky: { type: new graphql.GraphQLNonNull(graphql.GraphQLString) },
},
resolve: (root, {location, whisky}) => {
console.log('deleteInventory');
return new Promise((resolve, reject) => {
database.run('DELETE from Inventories WHERE location = (?) AND whisky = (?);', [location, whisky], (err) => {
if (err) { console.error(err); return reject(err); }
resolve(`Deleted inventory for ${location} of ${whisky}`);
});
})
}
}
}
});
const schema = new graphql.GraphQLSchema({
query: queryType,
mutation: mutationType
});
module.exports = {
schema
}

94
index.js Normal file
View File

@ -0,0 +1,94 @@
const express = require('express');
const { graphqlHTTP } = require("express-graphql");
const schema = require("./graphql/whisky.js");
const app = express();
const cors = require("cors");
app.use(cors());
app.use("/graphql", graphqlHTTP({ schema: schema.schema, graphiql: true}));
/*
mutation {
createWhisky(
description: "Eagle Rare",
code:"4017B",
size: 750,
price: 59.95,
lastSeen: "2021-04-17") {
description,
code,
size,
price,
lastSeen
}
}
mutation {
createLocation(
address: "11820 SW Lanewood",
city: "Portland",
phone: "503 501 8281",
longitude: -110.0343,
latitude: 45) {
address,
city,
phone,
longitude,
latitude
}
}
mutation {
createInventory(
location: 1,
whisky: 1,
quantity: 5,
updated: "2021-04-21") {
location,
whisky,
quantity,
updated
}
}
query {
Whisky (id:1) {
description
price
code
size
inventories {
location {
address
phone
}
quantity
updated
}
}
Inventories {
location {
address
}
quantity
updated
}
Locations {
address
inventories {
whisky {
description
price
code
}
updated
quantity
}
}
}
*/
app.listen(4000, () => {
console.log("GraphQL server running at http://localhost:4000.");
});

29
monkey.js Normal file
View File

@ -0,0 +1,29 @@
/* monkey-patch console.log to prefix with file/line-number */
function lineLogger(logFn) {
let cwd = process.cwd(),
cwdRe = new RegExp("^[^/]*" + cwd.replace("/", "\\/") + "\/([^:]*:[0-9]*).*$");
function getErrorObject() {
try {
throw Error();
} catch (err) {
return err;
}
}
let err = getErrorObject(),
caller_line = err.stack.split("\n")[4],
args = [caller_line.replace(cwdRe, "$1 -")];
/* arguments.unshift() doesn't exist... */
for (var i = 1; i < arguments.length; i++) {
args.push(arguments[i]);
}
logFn.apply(this, args);
}
console.log = lineLogger.bind(console, console.log);
console.warn = lineLogger.bind(console, console.warn);
console.error = lineLogger.bind(console, console.error);

1830
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

25
package.json Normal file
View File

@ -0,0 +1,25 @@
{
"name": "whisky",
"version": "1.0.0",
"description": "OLCC Whisky REST API",
"main": "index.js",
"scripts": {
"start": "node index.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "SEE LICENSE IN LICENSE",
"dependencies": {
"cors": "^2.8.5",
"express": "^4.17.1",
"express-graphql": "^0.12.0",
"form-data": "^4.0.0",
"graphql": "^15.5.0",
"graphql-request": "^3.4.0",
"http-proxy-agent": "^4.0.1",
"jsdom": "^16.5.3",
"moment": "^2.29.1",
"node-fetch": "^2.6.1",
"sqlite3": "^5.0.2"
}
}

1
whisky-business Submodule

@ -0,0 +1 @@
Subproject commit 439573c403e02f20f60afc6b47c751766cf1ca6e

490
whisky.js Normal file
View File

@ -0,0 +1,490 @@
const fs = require("fs").promises,
fetch = require("node-fetch"),
FormData = require("form-data"),
{ JSDOM } = require("jsdom"),
HttpsProxyAgent = require('http-proxy-agent'),
{ request, GraphQLClient, gql } = require("graphql-request"),
moment = require("moment");
/* Any search URL to oregonliquorsearch that has an expired cookie
* will take you back to the main "verify you are 21" page
*
* If you hit the server too frequently, or perhaps too many times
* with the same cookie, it will ban your IP for an indeterminate
* amount of time. It might be a scaled value; I've been blocked
* for an hour before or several days other times.
*/
const cookies = [];
const client = new GraphQLClient("http://localhost:4000/graphql", { headers: {} });
const getWhiskyInfo = async (whisky) => {
const cacheFile = `cache/${whisky.code}`;
return fs.stat(cacheFile).then(async stat => {
if (moment().subtract(1, 'day').isBefore(stat.mtime)) {
console.log(`${cacheFile} is newer than one day ${moment(stat.mtime).calendar()}`);
} else {
console.log(`${cacheFile} is older than one day ${moment(stat.mtime).calendar()}`);
await fs.unlink(cacheFile);
}
return fs.readFile(cacheFile);
})
.catch(async (error) => {
if (criticalFailure) throw error;
console.log(`Loading new location data for ${whisky.code}`);
const url = `http://oregonliquorsearch.com/servlet/FrontController` +
`?radiusSearchParam=10` +
`&productSearchParam=${whisky.code}` +
`&locationSearchParam=97225` +
`&btnSearch=Search` +
`&view=global` +
`&action=search`;
await rateLimit();
return fetch(url, {
headers: {
cookie: cookies
}
})
.then(response => response.text())
.then(dom => {
try {
fs.writeFile(cacheFile, dom);
} catch (_error) {
if (criticalFailure) throw _error;
console.log(`Write failed: ${_error}`);
}
return dom;
});
}).then(dom => {
dom = new JSDOM(dom);
const headers = [];
const rows = dom.window.document.querySelectorAll('table.list tr');
const olccLocations = [];
rows.forEach(row => {
const th = row.querySelectorAll('table.list th');
if (th.length) {
th.forEach(header => headers.push(header.textContent.trim()));
return;
}
const tds = row.querySelectorAll('td'), location = {};
tds.forEach((td, index) => {
const span = td.querySelector('span.link');
let value = (span ? span.textContent : td.textContent).trim();
if (parseFloat(value) == value) {
value = parseFloat(value);
}
location[headers[index]] = value;
});
olccLocations.push(location);
});
return olccLocations;
})
.then(async (olccLocations) => {
console.log(`Updating locations for ${whisky.code}`);
const whiskyInventories = []; /* Locations OLCC reports for this whisky */
await Promise.all(olccLocations.map(async (olccLocation) => {
let location = locations.find(item => item.code == olccLocation['Store No']);
let operation;
if (location == null) { /* New location */
console.log(`NEW location: ${olccLocation['Store No']} ${olccLocation['Address']} ${olccLocation['Location']}`);
location = {
code: olccLocation['Store No'].toString(),
address: olccLocation['Address'],
city: olccLocation['Location'],
phone: olccLocation['Telephone'],
longitude: -110.5,
latitude: 45
};
operation = gql`
mutation CreateLocation($code: String!, $address: String!, $city: String!, $phone: String!, $longitude: Float!, $latitude: Float!) {
createLocation(code: $code, address: $address, city: $city, phone: $phone, longitude: $longitude, latitude: $latitude) {
code
}
}`;
} else {
operation = gql`
mutation UpdateLocation($code: String!, $address: String!, $city: String!, $phone: String!, $longitude: Float!, $latitude: Float!) {
updateLocation(code: $code, address: $address, city: $city, phone: $phone, longitude: $longitude, latitude: $latitude)
}`;
}
try {
const data = await client.request(operation, location);
if (data.createLocation && data.createLocation.code) {
locations.push(location);
}
whiskyInventories.push({
location,
updated: moment().format("YYYY-MM-DD"),
quantity: olccLocation['Qty']
});
} catch (error) {
console.error(JSON.stringify(error, null, 2));
console.log(location);
process.exit(1);
}
}));
/* Manage Inventories table */
/* Delete entries on whisky which do not exist in the new OLCC locations for this whisky, or update quantity for those that do */
await Promise.all(whisky.inventories.map(async (inventory) => {
const update = whiskyInventories.find(item => item.location.code == inventory.location.code);
let operation, variables;
if (update) {
Object.assign(inventory, update);
operation = gql`
mutation UpdateInventory($location: String!, $whisky: String!, $quantity: Int!, $updated: String!) {
updateInventory(location: $location, whisky: $whisky, quantity: $quantity, updated: $updated)
}`;
//console.log("update", JSON.stringify(inventory));
variables = {
whisky: whisky.code,
location: update.location.code,
quantity: update.quantity,
updated: moment().format("YYYY-MM-DD")
};
} else {
operation = gql`
mutation DeleteInventory($location: String!, $whisky: String!) {
deleteInventory(location: $location, whisky: $whisky)
}`;
console.log("delete", JSON.stringify(inventory));
variables = {
whisky: whisky.code,
location: inventory.location.code,
};
}
try {
await client.request(operation, variables);
} catch (error) {
console.error(JSON.stringify(error, null, 2));
console.log(JSON.stringify(variables, null, 2), update);
process.exit(1);
}
}));
/* If an entry exists in the OLCC locations for this whisky, and not on the whisky, then add a new Inventory entry */
await Promise.all(whiskyInventories.map(async (olccInventory) => {
const update = whisky.inventories.find(item => item.location.code == olccInventory.location.code);
if (!update) {
const operation = gql`
mutation CreateInventory($location: String!, $whisky: String!, $quantity: Int!, $updated: String!) {
createInventory(location: $location, whisky: $whisky, quantity: $quantity, updated: $updated) {
quantity
}
}`;
try {
console.log(JSON.stringify(olccInventory, null, 2));
const data = await client.request(operation, {
whisky: whisky.code,
location: olccInventory.location.code,
quantity: olccInventory.quantity,
updated: moment().format("YYYY-MM-DD")
});
//console.log(`New inventory: ${JSON.stringify(data, null, 2)}`);
whisky.inventories.push(data.createInventory.inventory);
} catch (error) {
console.log('there');
console.error(JSON.stringify(error, null, 2));
console.log(operation);
process.exit(1);
}
}
}));
whisky.inventories = whiskyInventories;
return whisky.inventories;
});
};
console.log(`Trying from "cache"...`);
let lastFetch = undefined;
const rateLimit = async () => {
if (!lastFetch) {
lastFetch = Date.now();
return;
}
const delay = (lastFetch + (Math.random() * 5000 + 2500)) - Date.now();
if (delay > 0) {
// console.log(`Rate limiting for ${delay}ms`);
await wait(delay);
}
lastFetch = Date.now();
}
const wait = async (timeout) => {
return new Promise((resolve, reject) => {
setTimeout(resolve, timeout);
});
};
let criticalFailure = false;
let whiskies = [],
locations = [];
fs.stat("cache")
.catch((error) => {
if (criticalFailure) throw error;
fs.mkdir("cache");
return fs.stat("cache");
})
.then(async (stats) => {
if (!stats.isDirectory()) {
criticalFailure = true;
throw new Error("cache is not a directory or can not be created");
}
let query = gql` {
Whiskies {
code
description
price
size
lastSeen
inventories {
location {
code
address
city
longitude
latitude
}
quantity
updated
}
}
}`;
try {
whiskies = (await client.request(query)).Whiskies;
} catch (error) {
console.error(error);
process.exit(-1);
}
query = gql` {
Locations {
code
address
city
phone
longitude
latitude
}
}`;
try {
locations = (await client.request(query)).Locations;
} catch (error) {
console.error(error);
process.exit(-1);
}
})
.catch(error => console.error(error))
.then(async () => {
/* Attempt to read the whisky cache data.
* If it is older than 1 day, fetch new data */
const stat = await fs.stat("cache/whiskies");
if (moment().subtract(1, 'day').isBefore(stat.mtime)) {
console.log(`cache/whiskies is newer than one day ${moment(stat.mtime).calendar()}`);
} else {
console.log(`cache/whiskies is older than one day ${moment(stat.mtime).calendar()}`);
await fs.unlink('cache/whiskies');
}
const data = await fs.readFile("cache/whiskies");
return fs.readFile("cache/cookies")
.then(cookieData => {
JSON.parse(cookieData).forEach(cookie => cookies.push(cookie))
})
.then(() => {
return data;
});
}).catch(async (error) => {
if (criticalFailure) throw error;
console.log(`Cache or cookies not found--loading fresh whisky list and cookies.`);
await rateLimit();
return fetch("http://oregonliquorsearch.com/", {
// agent: new HttpsProxyAgent('http://3.22.0.212:8080')
}).then((response) => {
response.headers.raw()['set-cookie']
.forEach((header) => {
header
.split(';')
.forEach(cookie => {
cookies.push(cookie);
});
});
fs.writeFile("cache/cookies", JSON.stringify(cookies));
})
.then(async () => {
/* if (!cookies.JSESSIONID) {
throw "JSESSIONID not found";
}*/
console.log(`Submitting age verification...`);
const formData = new FormData();
formData.append("btnSubmit", "I'm 21 or older");
await rateLimit();
return fetch("http://oregonliquorsearch.com/servlet/WelcomeController", {
method: "POST",
body: formData,
headers: {
cookie: cookies
}
})
.then((response) => {
if (response.status != 200) {
throw "Error fetching resource from oregonliquorsearch";
}
});
}).then(async () => {
/* Search for all WHISKY types */
console.log(`Searching for all whisky types...`);
await rateLimit();
return fetch("http://oregonliquorsearch.com/servlet/FrontController" +
"?view=browsecategoriesallsubcategories" +
"&action=select" +
"&category=DOMESTIC%20WHISKEY", {
headers: {
cookie: cookies
}
})
.then(response => response.text());
})
.then(dom => {
try {
// console.log(`Writing to "cache"...`);
fs.writeFile("cache/whiskies", dom);
} catch(_error) {
if (criticalFailure) throw _error;
console.log(`Write failed: ${_error}`);
}
return dom;
});
}).then(dom => {
dom = new JSDOM(dom);
const headers = [];
const rows = dom.window.document.querySelectorAll('table.list tr');
const olccWhiskies = [];
rows.forEach(row => {
const th = row.querySelectorAll('table.list th a');
if (th.length) {
th.forEach(header => headers.push(header.textContent.trim()));
return;
}
const tds = row.querySelectorAll('td'), whisky = {};
tds.forEach((td, index) => {
let value = td.textContent.trim();
switch (headers[index]) {
case 'Size':
if (value.match(/^.* ML/)) {
value = parseFloat(value) / 1000.;
} else if (value.match(/^.* L/)) {
value = parseFloat(value);
} else if (value == 'LITER') {
value = 1.;
}
break;
case 'Proof':
if (parseFloat(value) == value) {
value = parseFloat(value);
}
break;
case 'Case Price':
case 'Bottle Price':
if (value.match(/^\$/)) {
value = parseFloat(value.replace(/^\$/, ""));
}
break;
case 'Item Code':
const anchor = td.querySelector('a');
if (anchor) {
whisky.url = `http://oregonliquorsearch.com${anchor.href}`;
}
value = td.querySelector('span.link').textContent.trim();
break;
case 'Description':
case 'Age':
default:
break;
}
whisky[headers[index]] = value;
});
olccWhiskies.push(whisky);
});
return olccWhiskies;
}).then(async (olccWhiskies) => {
await Promise.all(olccWhiskies.map(async (olccWhisky) => {
let whisky = whiskies.find(item => item.code == olccWhisky['Item Code']);
let operation;
if (whisky == null) { /* New whisky */
console.log(`NEW whisky: ${olccWhisky['Item Code']} ${olccWhisky['Description']}`);
whisky = {
description: olccWhisky['Description'],
code: olccWhisky['Item Code'].toString(),
size: Math.floor(1000 * olccWhisky['Size']),
proof: olccWhisky['Proof'],
age: olccWhisky['Age'],
price: olccWhisky['Bottle Price'],
};
operation = gql`
mutation CreateWhisky($description: String!, $code: String!, $size: Int!, $price: Float!, $lastSeen: String!) {
createWhisky(description: $description, code: $code, size: $size, price: $price, lastSeen: $lastSeen) {
code
}
}`;
} else {
operation = gql`
mutation UpdateWhisky($description: String!, $code: String!, $size: Int!, $price: Float!, $lastSeen: String!) {
updateWhisky(description: $description, code: $code, size: $size, price: $price, lastSeen: $lastSeen)
}`;
}
whisky.lastSeen = moment().format("YYYY-MM-DD");
try {
const data = await client.request(operation, whisky);
if (whisky.code === undefined) {
whisky.code = data;
}
} catch (error) {
console.error(JSON.stringify(error, null, 2));
console.error(operation);
console.error(variables);
process.exit(1);
}
}));
console.log(`OLCC currently carries ${whiskies.length} domestic whiskies.`);
whiskies = whiskies
.filter(a => a.code == '2170B')
.filter(a => a.price >= 10.)// && a['Bottle Price'] <= 100.)
.sort((a, b) => b.price - a.price);
const max = Math.min(5, whiskies.length);
console.log(`${max} whiskies > $10:`);
for (let i = 0; i < max; i++) {
console.log(`${i+1}. \$${whiskies[i].price} - ${whiskies[i].description}`);
const _inventories = await getWhiskyInfo(whiskies[i]);
_inventories.sort((a, b) => b.quantity - a.quantity);
/* Create or Update location */
/* Update location inventory */
_inventories.forEach((inventory) => {
console.log(` ${inventory.quantity} at ${inventory.location.address} in ${inventory.location.city}, OR`);
});
}
});