JP2LJ6B34S4IS6MPKF5LMMYUWIYMGU3JCJRRR4PTZ4IWM653JCXAC
SNP2LVW26GG6H6BDZ5S5UWPWHKSYID4WTXDL47I2KLQDLG6CW7CQC
Y4CGGXO7OJRSU36XZRIUVTGX73BXSKUY566ZWXIFO3UP7Y6JXXUQC
UT6G4EVANUMS7LLFOS5FTYL4SG4UMQG67ORLV4EDMKGNT3TC2E3QC
INVJWVQDQWH57ORSOR72VWASSMDHCXD35QJQ5H7XN67PSERNUIUAC
6LLLHD2T6WZHLCEDV6RGXJHEPPF5EVJNUAPWYEEE3OXXZ6ZG3VWQC
XLCCD2M74PXBCAR7SHZ5SQTLSATKRQBZRB36TBM27DJO63KFECVAC
AYQJCODFIHXK2UNLA55D3Y2IDJDP5BMQHUA7UNGDHBZUPDBCY74QC
G2ULKN4AA66PYPRK24JBAD4SDGIHUAK5RX4A4ZMJUI6XLYXSBWGAC
HFOZWZPGUIVA7QUO7VTPZ2R5ADEDGG7EGL3E5TFUUYRNAOVMHIDQC
BZUGIKIQSVUH7WZF4MQMPGP5E6WLTCRARMEG3HOT644W5BKOW7IQC
TZJ5KFZ6XNQJFNDDMYGPURKC7JIC4E3DDB64T7D6PPC6YQRIJ6SQC
CMOE5TZ3HY6BG6KB56HMIJ6BU5J6D3NE7VFK2NSAX2XEXAPEKJQQC
T4XGM2U2W7W3SM424W6JNEIJWOJQ5ILZWU45JPZUKTZMK77ZARKQC
BXGNP4GARIQA5LT3AN2F22HHGJHLUOL3YKBWWJ6LQQKUWLPYB7OAC
WYTMZJFYVKHR4QH7AV5JUNWXT6NAC5NNQNPZCQSDI6LGI7DVXFYAC
5RSETDN4QCQLU7Z55EFXSW27CBEW6HD2MVD2MKB6WDBXIKXFHKNQC
ZI2RJOZ2HXBHX7L54BTSDKESY6NG5KBJLVHCNY7K7ZLGIUOIHQUQC
E4HFJ4L4PAGV7R7EXVG2B2CWDGGW7XWU4D37VURZ66HZ3ILHCFUAC
NDMM44EV2XD5RP7J4Q25ZX52LUP7WOMTKO3C2PDXW7IWFEVWUW6QC
EGNRBQUMOZHAF5B7KCMTAPMB4H3USK4MK33Q6T5V26D6HOJNW6LQC
JQCVFJKLLXVEUEJ72U5MLVC4BIHNMMYI6DT2ZECXDO5XCPHORWDQC
5XHBBUBSPTNNPEZZVELCS5AEAKQVHEMLAI2URKBMMBUGWTPARFVQC
2FPZGFF5PCGNV45HOB6TROMJFPURHPXM7YSPWNXCHFJS7EICVKQAC
7XVW32MMBBIGWQZ3QRD365TBIJU4DYN6T4VO2E46WFDCFHGO2BMAC
72DUSPYXB64ECTFQWWD7XSY4YJUNRCFZT22AS4BDRIIUZP33CZGAC
NBNPI4ATCXHIMKSXW5VRS3KM4WJZZMOPD7H4QTDNOQFRSQYFSIBAC
GQ2TJSGOGNH4PKDNN5ZWITLOE6YNAWO6TCBCQV4SN7ZRWEB6IZ5AC
63VXWIHIAKGK7J4VTNRUAG2V32N2QUSWFELB6GD34S54FGRWAPCQC
JAAJEH57DRFXCZGAPOQLI4MN7N57OG52E7TQCC4FFJSOPDX4M65QC
OJD2AWBJJRUXYVF5BBYWSRRIE5A7LKJDL6YQCQZ3APEBIB6GATFAC
NAAQ2HMF7NXGJDRPXATVAGGZWL5MAIQWF4K5S23VFYBPOPZUXJZAC
47JKAGWEDSPFXKMFOVMTEYGUFFS5SEJMNZIZPJJAWJ5U3CH5YGMQC
76HUJPOPXCN2NMOWLI6JOY7HR35EL4IBHMES3DBJVCFZRKKPGQSAC
QQAJ5O5EW6ASWLXQ627Y2F6YGMYKCM4WS37LTZYOMNPPFDPMNBUAC
2ZHTBPOJQI4FS3OQJXUDBD62HVFPGSKI633IXXKAGPGQLURMKVPAC
CLMZIURF2TXSMQ6YZYXR5CHGMRW6V64YFQLRCP3H3I3XB2KKOJ6AC
VO5G3FF3NDOKCIF2OU7G257R3XIPTFDCAMDTOVKHKOB5Z3I55KDQC
4WREYORZT3SWUXADWHSS6B4PQSP3QSLH77CGNKRH6IRKUMX4TARAC
MPLKJQVEPZB7GEWAKZOOIKBKWHTNQX3LLESCQEG4PE3CUHQEYUBAC
Y4W4MACPKH3IVO6JIQYGN4V5LR6EM6RPZEJ4RHH6ZBZUDA63AGRQC
7RKEQK2KPXJ2MEHPVSY7ROMHW6FTTKCXSMZV5MEXPFZKESZQYUUAC
SE6MCCXTIXSGMAH5EL7EPDXIQKV6THCCY2H4OQJJQOHCUQAWE5VQC
UC5U5CJBD5NP545XAXEIODLEEUSRIFUVTG5NO55SNMTAGV7ZOCMAC
S7XXN4JDMHFSWRHFEFK6VZ6N5UAXNA2R3BYPLQYAF5NZR3HVMOAQC
5SAL2YA2IIYRBZSNM6SBKHSD6UFP67GDOR5J6W2WXREYLJZEFJ6AC
Z7XOXXDJ7T6E4ZCP2AK5YSXWE6TCDCLZEXAZPZZVLOY4WCFVZPHAC
MM3HQWNEUZVOGQ3WWBTECBY6F47SN4U43I5UNDOAFWO3R7U5XPBAC
OQJOHZCSRMKF26K3QJPBDH4BH26GW2NIYJARTTMGLJ3WJWWUQVRAC
Q4J4JHWWARLZQBYRRR33FRSFZK5BQMWGBGYQ22MLLVUELI7FYHRQC
UXLEHDHRPKDY6DXD2Q64ULMVTPOPMQW6VAOPHG5K5HX24YPGG3MQC
X2VJ3NUGYJVYW3QCHNX6HX2M4QTV6TPKRVHVTYH6FPXMN2DDTLMAC
LWG4EVVTDO3IO46PTT5LGO7A2KSFPGZXSRNI7G3ZMXIAZACBB4IQC
P7PFZMZQ5S7GZRJVYE6ACAIDKVTD6YHOOGBHZ52MBSGAIDDGJTBQC
6QCQLOKDENPPQQ4ZIWHSZSBUCO7TDYPMUE4JPIVF7AYQXGL5QRTQC
DCYC55MAVFDM43TEEUMAHLPOHNQ3EJ5PEVCA7H6ZTKNQHJZK7XJAC
BPHFBV52V4OA6WLKXGFK7Q4IM5EUS2SKAW5ZGSA6K3NJFJJVATIAC
RGMMX4YQP6NPCPFAGZ5SRITSPJQW4F77S6RJLYJMS3D732LUGDPQC
A3FTXJ6OBPF426MSYBNR7IPLUOZFWQ5PMKOIGXM7TV4SX6GJ7T2QC
Q3A33UTWKXZATZKX3LQ42MUAWFTWDZDMNVMLKWGC3QF6L6HBLUQAC
54TMOAIBD5UBRSWPGW45YZO4VMVKSP7YYKIQ7H4LWSVCN2E3QA4QC
PQTLWOFEMHL6BXIVUAYBHF5Z4F4CK7AD2QS4NOWGAFGV63EWKJNAC
PEVQ4WY77RA6HMHM2C4HLJ5YKRLHYCAD7MDIWD6POZTJZMMMBVGAC
EDVJECDIUUOSU4TS2DCL25MJDR5RQG7PQVTYEM6R3B4SFGKSX6MAC
7CC5KHF4RWFFO4DG2DLKPIQDTOBODUPKQ4KZZDRAQQUFVDNC5MRQC
Y6HOXJX6YHDL2Y3L53QZFA4LHKXXS6WAG4TZGBKQGMT35VQLAN3QC
UCWCMBNLNMCMNJ2QRKZSFABEHKSOO7BWVLLEJCOPJPZ7QZCECQUAC
BUCBGGRUTH7HNAADSU24U4BPHPUY7QYB3KRZPIIDUERZTL3YNTZQC
UCY5PVFCT2QIOGR47KFS4KBF2ZPHRBOAIOH6PJEVWCHLMBRBLOMQC
NF2TA3BN5FXWG6LFHHI3IDBVRPMLVRNUUZBE3NWIBASMSKEQPC4QC
ONTV36PSUYFGQS4OJVRW4JO3SA6XMTUN3G4HIO6YVCOLUV5DXXPAC
VIGS24FLOPPTBLIBZIB2LFMYCDZ3AO4FBXYADAKQMJFNPM4JHAGQC
OGAHYK56XUYXXLLBRO5BPMBURNE673CQYGX4LBGLF5VWDY572B2AC
WZKS2NSIL7VST55EQ7QEF2YG2X3L2Y6W54AIG463ZRH462QR2NWQC
5FCMFAPUTCCO2JILRWLC4PHOSHYFCQNH3D7PKLA267MHVZADOX2AC
CPNWTEF4ZZHBAB565BALMX5QO2CR3Q32YUHE3FMWJO3O25DZNU5AC
data: [row objects] || row object || id to splice || {id: integer, property_that_has_changed: new_value, other_property_that_has_changed: new_value}
data: ([row objects] || row object) || row object || id to splice || {id: integer, property_that_has_changed: new_value, other_property_that_has_changed: new_value}
why: "string" || undefined
create or replace function s(anyarray) returns anyarray as $$
select array_agg(distinct n order by n) from unnest($1) as t(n);
$$ language sql immutable;
create type product_type as enum ('flower', 'preroll', 'cartridge', 'disposable', 'oil', 'spray', 'capsule', 'tea', 'gummy', 'mint', 'chocolate', 'baked');
create unique index upi_store_tax_2 on store_tax (store_id, name) where types is null;
create unique index upi_store_tax_3 on store_tax (store_id, name, s(types)) where types is not null;
create or replace function remove_duplicates_in_array() returns trigger as $$ begin
if new.types is not null then
new.types = s(new.types);
if array_length(new.types, 1) = array_length(enum_range(null::product_type), 1) then
new.types = null;
end if;
end if;
return new;
end $$ language plpgsql volatile;
drop trigger if exists remove_duplicates_in_array_trigger on store_tax;
create trigger remove_duplicates_in_array_trigger before insert or update on store_tax for each row execute function remove_duplicates_in_array();
);--not done. more research needed on best practices, email reset, sign in count, IP address, etc. survey?
);--not done. more research needed on best practices, sign in count, IP address, etc. survey?
create table user_store (
user_store_id serial primary key,
user_id integer references usr on delete cascade,
store_id integer references store on delete cascade,
unique(user_id, store_id),
owner boolean not null--otherwise just worker
);
create table user_address (
user_store_id serial primary key,
user_id integer references usr on delete cascade,
address citext not null,
address2 citext not null,
city citext not null,
region citext not null,
country citext not null,
postal_code citext not null,
longitude decimal not null,
latitude decimal not null,
osrm_hint_foot text not null,
osrm_hint_car text not null,
unique(user_id, address, city, region, country, postal_code)
);
left join lateral (
select coalesce(sum(rate), 0) as tax
from store_tax
where
menu_item.store_id = store_tax.store_id and
(types is null or poop.type = any(types))
) as t on true
left join lateral (
select (coalesce(sum(quantity), 0)) as reserved
from line_item inner join cart using (cart_id)
where
status <= 'ready' and
line_item.menu_item_id = menu_item.menu_item_id
) as w on true
select coalesce(sum(line_item.quantity), 0)
into strict held
from cart inner join line_item using (cart_id)
where status <= 'ready' and cart.cart_id != new.cart_id and line_item.menu_item_id = new.menu_item_id;
select stock
into strict _stock
from menu_item
where menu_item.menu_item_id = new.menu_item_id;
--raise exception 'Value: %, %, %, %', proposed_gram_equivalency_line_item, gram_equivalency_in_cart, held, stock;
elsif _stock - held < new.quantity then
raise exception 'out of stock';
else
return new;
end if;
if not (old.cart_id is not null and old.cart_id != new.cart_id and old.menu_item_id = new.menu_item_id and old.quantity = new.quantity) then
select coalesce(sum(line_item.quantity), 0)
into strict held
from cart inner join line_item using (cart_id)
where status <= 'ready' and cart.cart_id != new.cart_id and line_item.menu_item_id = new.menu_item_id;
select stock
into strict _stock
from menu_item
where menu_item.menu_item_id = new.menu_item_id;
if _stock - held < new.quantity then
raise exception 'out of stock';
end if;
create or replace function decrement_stock() returns trigger as $$ begin
if old.status = 'ready' and new.status = 'paid' then
update menu_item
set stock = greatest(stock - quantity, 0)
from line_item
where
menu_item.menu_item_id = line_item.menu_item_id and
line_item.cart_id = new.cart_id;
end if;
return new;
end $$ language plpgsql volatile;
drop trigger if exists decrement_stock_trigger on cart;
create trigger decrement_stock_trigger before update on cart for each row execute function decrement_stock();
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/@googlemaps/google-maps-services-js/-/google-maps-services-js-2.6.0.tgz",
"integrity": "sha512-+eVnmvMxfp++yGiwewSUyNtAy0Rd6E2b+Fs9IlzWERq0J8jsX4U/s9LoG0qc+yNEYZGh0GX6fNYVGpYyuZ4+Yw==",
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@googlemaps/google-maps-services-js/-/google-maps-services-js-3.1.2.tgz",
"integrity": "sha512-RmCyqs9L0KiqTHmmx31NLwtZ3dqjX1w/zD/NuHHah+gCVEj427ZuyVFxgcXUm38ttUukpBLtLlxT036JYek1jA==",
}
},
"@sendgrid/client": {
"version": "7.2.1",
"resolved": "https://registry.npmjs.org/@sendgrid/client/-/client-7.2.1.tgz",
"integrity": "sha512-QizSa+qKnq5xquMyeB2x0l2JNH3jvg3zHmGt0Ghz+DLUGpW8i1m/zvrqSZ0wdcGVO4/2lW3WFN0drv1+jOxVgA==",
"requires": {
"@sendgrid/helpers": "^7.2.0",
"axios": "^0.19.2"
"@types/mime-types": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.0.tgz",
"integrity": "sha1-nKUs2jY/aZxpRmwqbM2q2RPqenM="
"@sendgrid/helpers": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/@sendgrid/helpers/-/helpers-7.2.0.tgz",
"integrity": "sha512-Ps8rc3xFpaQvIWoW1zlWi9G7QnxYwXFOeq0915DzXf5knoiOgXdFk/XBXbCFjr2lf/JIzjgFUtAV/dN8Z8jzHw==",
"requires": {
"chalk": "^2.0.1",
"deepmerge": "^4.2.2"
}
},
"@sendgrid/mail": {
"version": "7.2.1",
"resolved": "https://registry.npmjs.org/@sendgrid/mail/-/mail-7.2.1.tgz",
"integrity": "sha512-LBAdwL+c6HmN2B/Q24LFy2do/IzBFeFoF05xBqgELxp8lMUwis4fbqgo09Zyj1CY0qHN+CfWcUfLNvHfVCh8oQ==",
"requires": {
"@sendgrid/client": "^7.2.1",
"@sendgrid/helpers": "^7.2.0"
}
"version": "13.13.4",
"resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.4.tgz",
"integrity": "sha512-x26ur3dSXgv5AwKS0lNfbjpCakGIduWU1DU91Zz58ONRWrIKGunmZBNv4P7N+e27sJkiGDsw/3fT4AtsqQBrBA==",
"version": "14.0.14",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.0.14.tgz",
"integrity": "sha512-syUgf67ZQpaJj01/tRTknkMNoBBLWJOBODF0Zm4NrXmiSuxjymFrxnTu1QVYRubhVkRcZLYZG8STTwJRdVm/WQ==",
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.1.2.tgz",
"integrity": "sha512-waNHE7tQBBn+2qXucI8HY0o2Y0OBPWldWOWsZwY71JcCm4SvrPnWdceFfB5NIXSqE8Ewq6VR/Qt5b1i69P6KCQ==",
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.1.3.tgz",
"integrity": "sha512-wn8fw19xKZwdGPO47jivonaHRTd+nGOMP1z11sgGeQzDy2xd5FG0R67dIMcKHDE2cJ5y+YXV30XVGUBPRSY7Hg==",
"version": "6.12.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.0.tgz",
"integrity": "sha512-D6gFiFA0RRLyUbvijN74DWAjXSFxWKaWP7mldxkVhyhAV3+SWA9HEJPHQ2c9soIeTFJqcSdFDGFgdqs1iUU2Hw==",
"version": "6.12.2",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.2.tgz",
"integrity": "sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ==",
"version": "github:boogerlad/node-argon2#1bb1fbbd8cdbedc7ba325012ece1054b21532581",
"from": "github:boogerlad/node-argon2",
"version": "0.26.2",
"resolved": "https://registry.npmjs.org/argon2/-/argon2-0.26.2.tgz",
"integrity": "sha512-Tk9I/r3KIHCIHU5x2UawKsPi+g7MByAYnUZghXztQDXRp/997P31wa4qvdvokTaFBpsu6jOZACd+2qkBGGssRA==",
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.9.1.tgz",
"integrity": "sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug=="
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.10.0.tgz",
"integrity": "sha512-3YDiu347mtVtjpyV3u5kVqQLP242c06zwDOgpeRnybmXlYYsLbtTrUBUm8i8srONt+FWobl5aibnU1030PeeuA=="
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-4.0.1.tgz",
"integrity": "sha512-hSIZHkUxIDS5zA2o00Kf2O5RfVbQ888n54xQoF/eIaquU4uaLxK8vhhBdktd0B3n2MjkcAWzv4mnhogykBKOUQ==",
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.0.0.tgz",
"integrity": "sha512-jB0yCBl4W/kVHM2whjfyqnxTmOHkCX4kHEa5nYKSoGeYe8YrjTYTc87/6bwt1g8cmV0QrbhKriETg9jWtcREhg==",
"node-addon-api": "^2.0.0",
"node-pre-gyp": "0.14.0"
"node-addon-api": "^3.0.0",
"node-pre-gyp": "0.15.0"
},
"dependencies": {
"node-addon-api": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.0.0.tgz",
"integrity": "sha512-sSHCgWfJ+Lui/u+0msF3oyCgvdkhxDbkCS6Q8uiJquzOimkJBvX6hl5aSSA7DR1XbMpdM8r7phjcF63sF4rkKg=="
},
"node-pre-gyp": {
"version": "0.15.0",
"resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.15.0.tgz",
"integrity": "sha512-7QcZa8/fpaU/BKenjcaeFF9hLz2+7S9AqyXFhlH/rilsQ/hPZKK32RtR5EQHJElgu+q5RfbJ34KriI79UWaorA==",
"requires": {
"detect-libc": "^1.0.2",
"mkdirp": "^0.5.3",
"needle": "^2.5.0",
"nopt": "^4.0.1",
"npm-packlist": "^1.1.6",
"npmlog": "^4.0.2",
"rc": "^1.2.7",
"rimraf": "^2.6.1",
"semver": "^5.3.0",
"tar": "^4.4.2"
}
}
"chalk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
"requires": {
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
"supports-color": "^5.3.0"
}
},
"version": "1.21.0",
"resolved": "https://registry.npmjs.org/cloudinary/-/cloudinary-1.21.0.tgz",
"integrity": "sha512-am8wpHbHl8bcpy9oGSlWrpWLNQ9szkW/jmhcJdEpMjaL23BYt05V1frWyrXDlo8Jt7aCo5NE6EO0CM9Zaynd5g==",
"version": "1.22.0",
"resolved": "https://registry.npmjs.org/cloudinary/-/cloudinary-1.22.0.tgz",
"integrity": "sha512-qQhSVqGyOWtGbPc3JrwyUXJEZpdqsEzBFSf5+gTo7gbKbXdzEysLagigoSlp6JYI7YW2oEbz4aeZlgN2ucOkzQ==",
"color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"requires": {
"color-name": "1.1.3"
}
},
"color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
},
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.0.tgz",
"integrity": "sha512-i42GQ498yibjdvIhivUsRslx608whtGoFIhF26Z7O4MYncBxp8CwalOs1lnHy21A9sIohWO2+uiE4SRtC9JXDg==",
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
"integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==",
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz",
"integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA=="
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
}
},
"find-up": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
"requires": {
"locate-path": "^5.0.0",
"path-exists": "^4.0.0"
"version": "2.4.5",
"resolved": "https://registry.npmjs.org/mime/-/mime-2.4.5.tgz",
"integrity": "sha512-3hQhEUF027BuxZjQA3s7rIv/7VCQPa27hN9u9g87sEkWaKwQPuXOkVKtOeiyUrnWqTDiOs8Ed2rwg733mB0R5w=="
"version": "2.4.6",
"resolved": "https://registry.npmjs.org/mime/-/mime-2.4.6.tgz",
"integrity": "sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA=="
"version": "1.43.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.43.0.tgz",
"integrity": "sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ=="
"version": "1.44.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz",
"integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg=="
"version": "2.1.26",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.26.tgz",
"integrity": "sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==",
"version": "2.1.27",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz",
"integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==",
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0="
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
"integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/needle/-/needle-2.4.1.tgz",
"integrity": "sha512-x/gi6ijr4B7fwl6WYL9FwlCvRQKGlUNvnceho8wxkwXqN8jvVmmmATTmZPRRG7b/yC1eode26C2HO9jl78Du9g==",
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/needle/-/needle-2.5.0.tgz",
"integrity": "sha512-o/qITSDR0JCyCKEQ1/1bnUXMmznxabbwi/Y4WwJElf+evwJNFNwIDMCCt5IigFVxgeGBJESLohGtIS9gEzo1fA==",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.0.tgz",
"integrity": "sha512-ASCL5U13as7HhOExbT6OlWJJUV/lLzL2voOSP1UVehpRD8FbSrSDjfScK/KwAvVTI5AS6r4VwbOMlIqtvRidnA=="
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.2.tgz",
"integrity": "sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA=="
}
},
"p-limit": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
"requires": {
"p-try": "^2.0.0"
}
},
"p-locate": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
"requires": {
"p-limit": "^2.2.0"
"version": "8.0.3",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.0.3.tgz",
"integrity": "sha512-fvcNXn4o/iq4jKq15Ix/e58q3jPSmzOp6/8C3CaHoSR/bsxdg+1FXfDRePdtE/zBb3++TytvOrS1hNef3WC/Kg==",
"version": "8.2.1",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.2.1.tgz",
"integrity": "sha512-DKzffhpkWRr9jx7vKxA+ur79KG+SKw+PdjMb1IRhMiKI9zqYUGczwFprqy+5Veh/DCcFs1Y6V8lRLN5I1DlleQ==",
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-0.1.3.tgz",
"integrity": "sha1-2hhHsglA5C7hSSvq9l1J2RskXfc="
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.2.3.tgz",
"integrity": "sha512-I/KCSQGmOrZx6sMHXkOs2MjddrYcqpza3Dtsy0AjIgBr/bZiPJRK9WhABXN1Uy1UDazRbi9gZEzO2sAhL5EqiQ=="
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.1.1.tgz",
"integrity": "sha512-kYH6S0mcZF1TPg1F9boFee2JlCSm2oqnlR2Mz2Wgn1psQbEBNVeNTJCw2wCK48QsctwvGUzbxLMg/lYV6hL/3A=="
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.2.1.tgz",
"integrity": "sha512-BQDPWUeKenVrMMDN9opfns/kZo4lxmSWhIqo+cSAF7+lfi9ZclQbr9vfnlNaPr8wYF3UYjm5X0yPAhbcgqNOdA=="
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.2.2.tgz",
"integrity": "sha512-r8hGxHOk3ccMjjmhFJ/QOSVW5A+PP84TeRlEwB/cQ9Zu+bvtZg8Z59Cx3AMfVQc9S0Z+EG+HKhicF1W1GN5Eqg=="
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.2.4.tgz",
"integrity": "sha512-/8L/G+vW/VhWjTGXpGh8XVkXOFx1ZDY+Yuz//Ab8CfjInzFkreI+fDG3WjCeSra7fIZwAFxzbGptNbm8xSXenw=="
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.7.0.tgz",
"integrity": "sha512-5NsSEDv8zY70ScRnOTn7bK7eanl2MvFrOrS/R6x+dBt5g1ghnj9Zv90kO8GwT8gxcu2ANyFprnFYB85IogIJOQ=="
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz",
"integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ=="
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-3.0.2.tgz",
"integrity": "sha512-5jS/POFVDW9fqb76O8o0IBpXOnq+Na8ocGMggYtnjCRBRqmAFvX0csmwgLOHkYnQ/vCBcBPYlOq0Pp60z1850Q==",
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-5.0.0.tgz",
"integrity": "sha512-JnZcgRQnfowRSJoSHteKU7G9fP/YYGB/juPn8m4jNqtzvR0h8GOoFmdjTBesJFfzhYkPU1FosHXnBVUB++xgaA==",
"version": "6.12.1",
"resolved": "https://registry.npmjs.org/query-string/-/query-string-6.12.1.tgz",
"integrity": "sha512-OHj+zzfRMyj3rmo/6G8a5Ifvw3AleL/EbcHMD27YA31Q+cO5lfmQxECkImuNVjcskLcvBRVHNAB3w6udMs1eAA==",
"version": "6.13.1",
"resolved": "https://registry.npmjs.org/query-string/-/query-string-6.13.1.tgz",
"integrity": "sha512-RfoButmcK+yCta1+FuU8REvisx1oEzhMKwhLUNcepQTPGcNMp1sIqjnfCtfnvGSQZQEhaBHvccujtWoUV3TTbA==",
},
"dependencies": {
"minimist": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
}
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/retry-axios/-/retry-axios-2.1.2.tgz",
"integrity": "sha512-MVNOizOgcM+dXK/kJCMnHPCGjp2Z0o3Ngznh7SfJkIVrqemHmDcgUEAbEHs3RyR3lbJtSyamq9k3gmgGSaow1g=="
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/retry-axios/-/retry-axios-2.2.1.tgz",
"integrity": "sha512-vSZzu29W48+/mVPOq+gwKe+qYWcppiKODXi4FN9q0iN+4s2z10/l1LDS4Ju4vReTyuvKOoqmHqY0rOAmuFjI/Q=="
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz",
"integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg=="
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.0.1.tgz",
"integrity": "sha512-6tzWDMeroL87uF/+lin46k+Q+46rAJ0SyPGz7OW7wTgblI273hsBqk2C1j0/xNadNLKDTUL9BukSjB7cwgmlPA==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.0.tgz",
"integrity": "sha512-9uW5iDvrIMCVpvasdFHW0wJPez0K4JnMZtsuIeDI7HyMGJNxmDZDOCQROr7lXyS+iL/QMpj07qcjGYTSdRFXUg==",
"version": "github:uNetworking/uWebSockets.js#116a85f9668dcb03e7799db06dc8275bc43c0963",
"from": "github:uNetworking/uWebSockets.js#v17.4.0"
"version": "github:uNetworking/uWebSockets.js#63c610cefde1ef71c27cba6dd1072495977ee34d",
"from": "github:uNetworking/uWebSockets.js#v18.3.0"
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.2.tgz",
"integrity": "sha512-pZMVAofMrrHX6Ik39hCk470kulCbmZ2SWfQLPmTWqfJV/oUm0gn1CblvHdUu4+54Je6Jq34x8kY6XjTy6dMkOg==",
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz",
"integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==",
"version": "7.2.5",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.2.5.tgz",
"integrity": "sha512-C34cIU4+DB2vMyAbmEKossWq2ZQDr6QEyuuCzWrM9zfw1sGc0mYiJ0UnG9zzNykt49C2Fi34hvr2vssFQRS6EA=="
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.3.0.tgz",
"integrity": "sha512-iFtXzngZVXPGgpTlP1rBqsUK82p9tKqsWRPg5L56egiljujJT3vGAYnHANvFxBieXrTFavhzhxW52jnaWV+w2w=="
"@sendgrid/mail": "^7.2.0",
"argon2": "github:boogerlad/node-argon2",
"bcrypt": "^4.0.1",
"@sendgrid/mail": "^7.2.1",
"argon2": "^0.26.2",
"bcrypt": "^5.0.0",
const request = require('request-promise-native');
const { Pool } = require('pg');
const pool = new Pool();
function isLoggedIn(ws) {
return ws.user_ID !== undefined;
}
function is(ws, type) {
return ws.user_type === type;
}
function desensitize(user) {
delete user.webportal_hash;
delete user.passphrase_hash;
delete user.token_hash;
delete user.token_expiry;
Object.keys(user).forEach((key) => (user[key] == null) && delete user[key]);
}
let authenticated = new Map(/*<user_ID, websocket>*/);//when we want to allow multiple sockets with same user, change to set<websockets>. have to figure out how token is supposed to work
//maybe login takes remember_me, and if not present, don't generate / persist new token??
const authenticated = new Map(/*<user_ID, websocket>*/);
setInterval(() => pool.query('select invalidate_stale_carts()'), 1000);
const { normalize, Big } = require('./scrape/common.js');
async function is(ws, store, owner) {//ws->user_ID || undefined, store_id integer || undefined, owner boolean || undefined
//https://stackoverflow.com/questions/354070/sql-join-where-clause-vs-on-clause
return (await pool.query(`select god or (owner is not null and owner >= $1) as t from usr left join user_store on usr.user_id = user_store.user_id and store_id = $2 where usr.user_id = $3`, [Boolean(owner), store, ws.user_ID])).rows?.[0]?.t;
}
setInterval(() => pool.query('select invalidate_stale_carts()'), 1000);
const app = uWS[(ssl ? 'SSL' : '') + 'App']({key_file_name: process.env.SSLKEY, cert_file_name: process.env.SSLCERT})
const app = require('uWebSockets.js')[(ssl ? 'SSL' : '') + 'App']({key_file_name: process.env.SSLKEY, cert_file_name: process.env.SSLCERT})
console.log('WebSocket opened');
pool.query(`select * from get_stores()`).then(r => {
for(let i = 0; i < r.rows.length; ++i) {
for(const prop in r.rows[i]) {
if(r.rows[i][prop] === null) {
delete r.rows[i][prop];
}
res.upgrade(
{q: qs.parse(req.getQuery())},
req.getHeader('sec-websocket-key'),
req.getHeader('sec-websocket-protocol'),
req.getHeader('sec-websocket-extensions'),
context
);
} else {
res.close();
}
},
open: async ws => {
console.log('WebSocket opened');
pool.query(`select * from get_stores()`).then(r => {
for(let i = 0; i < r.rows.length; ++i) {
for(const prop in r.rows[i]) {
if(r.rows[i][prop] === null) {
delete r.rows[i][prop];
ws.subscribe('store');
}
ws.subscribe('store');
ws.send(JSON.stringify({
what: 'store',
how: 'replace',
data: r.rows
}));
});
let fuck;
let hack;
if(ws.q.token) {
let user = (await pool.query('select user_id, email, god, json_object_agg(store_id, owner) filter (where store_id is not null) as acl from usr left join user_store using (user_id) where token_hash = $1 and now() < token_expiry group by user_id', [crypto.createHash('BLAKE2b512').update(Buffer.from(ws.q.token, 'base64')).digest()])).rows[0];//possible timing attack?
if(user === undefined) {
fuck = JSON.stringify({
what: 'user',
how: 'replace',
data: null,
why: 'invalid token'
});
} else {
ws.user_ID = user.user_id;
delete user.user_id;
hack = user;
let old = authenticated.get(ws.user_ID);
authenticated.set(ws.user_ID, ws);
if(old) {
old.end();
} else {
ws.publish('user/authenticated', JSON.stringify({
what: 'user/authenticated',//tbd
how: 'replace',
data: authenticated.size
}));
}
}
}
if(ws.q.otp && (ws.q.token && fuck || !ws.q.token)) {
//click link && original tab closed(sharedworker dead, so no preexisting connection)
//good thing this is a fallback to token. magic links won't invalidate remember me :)
let token = await randomBytes(128);
let user = (await pool.query(
`with u as (
update usr
set
otp_hash = null,
token_hash = $1
where
otp_hash = $2 and
now() < otp_expiry
returning
user_id,
email,
god
)
select u.*, json_object_agg(store_id, owner) filter (where store_id is not null) as acl
from u left join user_store using (user_id)
group by user_id`,
[
crypto.createHash('BLAKE2b512').update(token).digest(),
crypto.createHash('BLAKE2b512').update(Buffer.from(parameters.otp, 'base64')).digest()
]
)).rows[0];//possible timing attack?
if(user === undefined) {
})
let parameters = qs.parse(req.getQuery());
if(parameters.token) {
let user = (await pool.query('select * from usr where token_hash = $1 and now() < token_expiry', [crypto.createHash('BLAKE2b512').update(Buffer.from(parameters.token, 'base64')).digest()])).rows[0];//possible timing attack?
if(user === undefined) {
ws.send(JSON.stringify({
response_ID: 'firth',
data: 'invalid token'
}));
if(fuck) {
ws.send(fuck);
}
} else {
ws.user_ID = user.user_id;
delete user.user_id;
user.token = token.toString('base64');
hack = user;
let old = authenticated.get(ws.user_ID);
authenticated.set(ws.user_ID, ws);
if(old) {
old.end();
ws.user_ID = user.user_id;
ws.user_type = user.type;
desensitize(user);
ws.send(JSON.stringify({
response_ID: 'firth',
data: user
}));
//disallow multiple sockets with same credentials
let old = authenticated.get(user.user_id);
if(old !== undefined) {
delete old.user_ID;
delete old.user_type;
old.send('{"what":"logout"}');
}
authenticated.set(user.user_id, ws);
} else if(parameters.otp) {
//click link && original tab closed(sharedworker dead, so no preexisting connection)
//debating whether or not I should implement this...
//good thing this is a fallback to token. magic links won't invalidate remember me :)
}
} else if(fuck) {
ws.send(fuck);
}
++clients;
ws.publish('user/count', JSON.stringify({
what: 'user/count',//tbd
//how: 'update',
data: clients
}));
if(hack && Array.isArray(ws.q.carts) && (ws.q.carts = ws.q.carts.map(Number)).every(Number.isInteger)) {
const client = await pool.connect();
let m = (await client.query(`
with f as (
select distinct on (store_id) cart_id, store_id
from cart
where cart_id = any($2) and user_id is null and status = 'active'
),
r as (
update cart
set user_id = $1
where
cart_id in (select cart_id from f) and
not exists(select 1 from cart as c where c.user_id = $1 and c.store_id = cart.store_id and status <= 'ready')
returning cart_id, store_id
)
select * from f except select * from r
`, [ws.user_ID, ws.q.carts])).rows;
//todo: set based merge cart or cursor plpgsql function. this is ugly
for(let i = 0; i < m.length; ++i) {
let c = (await client.query({
text: `select cart_id from cart where user_id = $1 and store_id = $2 and status = 'active'`,
values: [ws.user_ID, m[i].store_id],
rowMode: 'array'
})).rows?.[0]?.[0];
if(c) {
let li = (await client.query({
text: 'select line_item_id, menu_item_id, quantity from line_item where cart_id = $1',
values: [m[i].cart_id],
rowMode: 'array'
})).rows;
let later = [];
for(let j = 0; j < li.length; ++j) {
try {
await client.query('savepoint wow');
await client.query(`update line_item set cart_id = $1 where line_item_id = $2`, [c, li[j][0]]);
} catch(e) {
await client.query('rollback to wow');
if(e.constraint === 'line_item_cart_id_menu_item_id_key') {
later.push(li[j]);
} else if(e.message === 'exceeded 30g limit') {
later = [];
break;
} else {
console.error(e);
}
}
}
for(let j = 0; j < later.length; ++j) {
try {
await client.query('savepoint wow');
await client.query('update line_item set quantity = quantity + $1 where cart_id = $2 and menu_item_id = $3', [later[j][2], c, later[j][1]]);
} catch(e) {
await client.query('rollback to wow');
if(e.message === 'exceeded 30g limit') {
break;
} else {
console.error(e);
}
}
}
} else {
console.log(`failed to merge cart. reservation in progress with store ${m[i].store_id}`);
}
++clients;
ws.publish('user/count', JSON.stringify({
what: 'user/count',//tbd
//how: 'update',
data: clients
await client.query('delete from cart where cart_id = any($1)', [m]);
await client.query('commit');
client.release();
}
delete ws.q;
if(hack) {
ws.send(JSON.stringify({
what: 'user',
how: 'replace',
data: hack
let user = (await pool.query('update usr set otp_hash = null, token_hash = $1 where otp_hash = $2 and now() < otp_expiry returning user_id, email, type, comt, akt1, cyp2c9, cyp2c19_2, cyp2c19_3, cyp2c19_17', [crypto.createHash('BLAKE2b512').update(token).digest(), crypto.createHash('BLAKE2b512').update(Buffer.from(parameters.otp, 'base64')).digest()])).rows[0];//possible timing attack?
let user = (await pool.query(
`with u as (
update usr
set
otp_hash = null,
token_hash = $1
where
otp_hash = $2 and
now() < otp_expiry
returning
user_id,
email,
god
)
select u.*, json_object_agg(store_id, owner) filter (where store_id is not null) as acl
from u left join user_store using (user_id)
group by user_id`,
[
crypto.createHash('BLAKE2b512').update(token).digest(),
crypto.createHash('BLAKE2b512').update(Buffer.from(parameters.otp, 'base64')).digest()
]
)).rows[0];//possible timing attack?
ws.user_type = user.type;
desensitize(user);
if(Array.isArray(parameters.carts) && (parameters.carts = parameters.carts.map(Number)).every(Number.isInteger)) {
const client = await pool.connect();
let m = (await client.query(`
with f as (
select distinct on (store_id) cart_id, store_id
from cart
where cart_id = any($2) and user_id is null and status = 'active'
),
r as (
update cart
set user_id = $1
where
cart_id in (select cart_id from f) and
not exists(select 1 from cart as c where c.user_id = $1 and c.store_id = cart.store_id and status <= 'ready')
returning cart_id, store_id
)
select * from f except select * from r
`, [user.user_id, parameters.carts])).rows;
//todo: set based merge cart or cursor plpgsql function. this is ugly
for(let i = 0; i < m.length; ++i) {
let c = (await client.query({
text: `select cart_id from cart where user_id = $1 and store_id = $2 and status = 'active'`,
values: [user.user_id, m[i].store_id],
rowMode: 'array'
})).rows?.[0]?.[0];
if(c) {
let li = (await client.query({
text: 'select line_item_id, menu_item_id, quantity from line_item where cart_id = $1',
values: [m[i].cart_id],
rowMode: 'array'
})).rows;
let later = [];
for(let j = 0; j < li.length; ++j) {
try {
await client.query('savepoint wow');
await client.query(`update line_item set cart_id = $1 where line_item_id = $2`, [c, li[j][0]]);
} catch(e) {
await client.query('rollback to wow');
if(e.constraint === 'line_item_cart_id_menu_item_id_key') {
later.push(li[j]);
} else if(e.message === 'exceeded 30g limit') {
later = [];
break;
} else {
console.error(e);
}
}
}
for(let j = 0; j < later.length; ++j) {
try {
await client.query('savepoint wow');
await client.query('update line_item set quantity = quantity + $1 where cart_id = $2 and menu_item_id = $3', [later[j][2], c, later[j][1]]);
} catch(e) {
await client.query('rollback to wow');
if(e.message === 'exceeded 30g limit') {
break;
} else {
console.error(e);
}
}
}
} else {
console.log(`failed to merge cart. reservation in progress with store ${m[i].store_id}`);
}
}
await client.query('delete from cart where cart_id = any($1)', [m]);
await client.query('commit');
client.release();
}
delete user.user_id;
//disallow multiple sockets with same credentials
let old = authenticated.get(user.user_id);
if(old !== undefined) {
delete old.user_ID;
delete old.user_type;
old.send('{"what":"logout"}');
let old = authenticated.get(ws.user_ID);
authenticated.set(ws.user_ID, ws);
if(old) {
old.end();
} else {
ws.publish('user/authenticated', JSON.stringify({
what: 'user/authenticated',//tbd
how: 'replace',
data: authenticated.size
}));
let newUser = await pool.query('insert into usr (email, passphrase_hash) values ($1, $2) returning user_id, email, type', [parameters.email, await argon2.hash(parameters.passphrase)]);//technically, we only need the id??
let token = await randomBytes(128);
let user = (await pool.query(`
with i as (
insert into usr
(email, passphrase_hash, token_hash)
values ($1, $2, $3)
returning user_id
), u as (
update cart
set (user_id) = (select user_id from i)
where cart_id = any($4) and user_id is null and status = 'active'
)
select user_id from i`,
[
parameters.email,
await argon2.hash(parameters.passphrase),
crypto.createHash('BLAKE2b512').update(token).digest(),
parameters.carts
])).rows[0];
ws.user_ID = user.user_id;
let user = (await pool.query('select * from usr where email = $1', [parameters.email])).rows[0];
let user = (await pool.query('select user_id, webportal_hash, passphrase_hash, god, json_object_agg(store_id, owner) filter (where store_id is not null) as acl from usr left join user_store using (user_id) where email = $1 group by user_id', [parameters.email])).rows[0];
await pool.query('update usr set token_hash = $1 where user_id = $2', [crypto.createHash('BLAKE2b512').update(token).digest(), user.user_id]);//update to blake3 once it's available in openSSL
desensitize(user);
user.token = token.toString('base64');//yuck, since json can't send binary, need to base64 encode. fyi base64 is smaller than hex(base16)
const client = await pool.connect();
await client.query('update usr set token_hash = $1 where user_id = $2', [crypto.createHash('BLAKE2b512').update(token).digest(), user.user_id]);//update to blake3 once it's available in openSSL
if(Array.isArray(parameters.carts) && (parameters.carts = parameters.carts.map(Number)).every(Number.isInteger)) {
let m = (await client.query(`
with f as (
select distinct on (store_id) cart_id, store_id
from cart
where cart_id = any($2) and user_id is null and status = 'active'
),
r as (
update cart
set user_id = $1
where
cart_id in (select cart_id from f) and
not exists(select 1 from cart as c where c.user_id = $1 and c.store_id = cart.store_id and status <= 'ready')
returning cart_id, store_id
)
select * from f except select * from r
`, [user.user_id, parameters.carts])).rows;
//todo: set based merge cart or cursor plpgsql function. this is ugly
for(let i = 0; i < m.length; ++i) {
let c = (await client.query({
text: `select cart_id from cart where user_id = $1 and store_id = $2 and status = 'active'`,
values: [user.user_id, m[i].store_id],
rowMode: 'array'
})).rows?.[0]?.[0];
if(c) {
let li = (await client.query({
text: 'select line_item_id, menu_item_id, quantity from line_item where cart_id = $1',
values: [m[i].cart_id],
rowMode: 'array'
})).rows;
let later = [];
for(let j = 0; j < li.length; ++j) {
try {
await client.query('savepoint wow');
await client.query(`update line_item set cart_id = $1 where line_item_id = $2`, [c, li[j][0]]);
} catch(e) {
await client.query('rollback to wow');
if(e.constraint === 'line_item_cart_id_menu_item_id_key') {
later.push(li[j]);
} else if(e.message === 'exceeded 30g limit') {
later = [];
break;
} else {
console.error(e);
}
}
}
for(let j = 0; j < later.length; ++j) {
try {
await client.query('savepoint wow');
await client.query('update line_item set quantity = quantity + $1 where cart_id = $2 and menu_item_id = $3', [later[j][2], c, later[j][1]]);
} catch(e) {
await client.query('rollback to wow');
if(e.message === 'exceeded 30g limit') {
break;
} else {
console.error(e);
}
}
}
} else {
console.log(`failed to merge cart. reservation in progress with store ${m[i].store_id}`);
}
}
await client.query('delete from cart where cart_id = any($1)', [m]);
}
await client.query('commit');
client.release();
//yuck, since json can't send binary, need to base64 encode. fyi base64 is smaller than hex(base16)
//disallow multiple sockets with same credentials
let old = authenticated.get(user.user_id);
if(old !== undefined) {
delete old.user_ID;
delete old.user_type;
old.send('{"what":"logout"}');
let old = authenticated.get(ws.user_ID);
authenticated.set(ws.user_ID, ws);
if(old) {
old.end();
} else {
ws.publish('user/authenticated', JSON.stringify({
what: 'user/authenticated',//tbd
how: 'replace',
data: authenticated.size
}));
await pool.query('update usr set token_hash = $1, passphrase_hash = $3, webportal_hash = null where user_id = $2', [crypto.createHash('BLAKE2b512').update(token).digest(), user.user_id, await argon2.hash(parameters.passphrase)]);//update to blake3 once it's available in openSSL
desensitize(user);
user.token = token.toString('base64');//yuck, since json can't send binary, need to base64 encode. fyi base64 is smaller than hex(base16)
const client = await pool.connect();
await client.query('update usr set token_hash = $1, passphrase_hash = $3, webportal_hash = null where user_id = $2', [crypto.createHash('BLAKE2b512').update(token).digest(), user.user_id, await argon2.hash(parameters.passphrase)]);//update to blake3 once it's available in openSSL
if(Array.isArray(parameters.carts) && (parameters.carts = parameters.carts.map(Number)).every(Number.isInteger)) {
let m = (await client.query(`
with f as (
select distinct on (store_id) cart_id, store_id
from cart
where cart_id = any($2) and user_id is null and status = 'active'
),
r as (
update cart
set user_id = $1
where
cart_id in (select cart_id from f) and
not exists(select 1 from cart as c where c.user_id = $1 and c.store_id = cart.store_id and status <= 'ready')
returning cart_id, store_id
)
select * from f except select * from r
`, [user.user_id, parameters.carts])).rows;
//todo: set based merge cart or cursor plpgsql function. this is ugly
for(let i = 0; i < m.length; ++i) {
let c = (await client.query({
text: `select cart_id from cart where user_id = $1 and store_id = $2 and status = 'active'`,
values: [user.user_id, m[i].store_id],
rowMode: 'array'
})).rows?.[0]?.[0];
if(c) {
let li = (await client.query({
text: 'select line_item_id, menu_item_id, quantity from line_item where cart_id = $1',
values: [m[i].cart_id],
rowMode: 'array'
})).rows;
let later = [];
for(let j = 0; j < li.length; ++j) {
try {
await client.query('savepoint wow');
await client.query(`update line_item set cart_id = $1 where line_item_id = $2`, [c, li[j][0]]);
} catch(e) {
await client.query('rollback to wow');
if(e.constraint === 'line_item_cart_id_menu_item_id_key') {
later.push(li[j]);
} else if(e.message === 'exceeded 30g limit') {
later = [];
break;
} else {
console.error(e);
}
}
}
for(let j = 0; j < later.length; ++j) {
try {
await client.query('savepoint wow');
await client.query('update line_item set quantity = quantity + $1 where cart_id = $2 and menu_item_id = $3', [later[j][2], c, later[j][1]]);
} catch(e) {
await client.query('rollback to wow');
if(e.message === 'exceeded 30g limit') {
break;
} else {
console.error(e);
}
}
}
} else {
console.log(`failed to merge cart. reservation in progress with store ${m[i].store_id}`);
}
}
await client.query('delete from cart where cart_id = any($1)', [m]);
}
await client.query('commit');
client.release();
//yuck, since json can't send binary, need to base64 encode. fyi base64 is smaller than hex(base16)
//disallow multiple sockets with same credentials
let old = authenticated.get(user.user_id);
if(old !== undefined) {
delete old.user_ID;
delete old.user_type;
old.send('{"what":"logout"}');
let old = authenticated.get(ws.user_ID);
authenticated.set(ws.user_ID, ws);
if(old) {
old.end();
} else {
ws.publish('user/authenticated', JSON.stringify({
what: 'user/authenticated',//tbd
how: 'replace',
data: authenticated.size
}));
case 'list_users':
if(isLoggedIn(ws)) {
if(is(ws, 'god')) {
let users = (await pool.query('select * from usr')).rows;//* is a bad idea... fix later
for(let i = 0; i < users.length; ++i) {
desensitize(users[i]);//shouldn't need this loop
}
ws.subscribe('user/#');//maybe for each subscribe call, call unsubscribeAll before (0.17.1 not released yet)?
//I suppose the only issue with over subscribe is waste of resources - and if user type changes
ws.send(JSON.stringify({
response_ID: request_ID,
data: users
}));
} else {
ws.send(JSON.stringify({
response_ID: request_ID,
data: 'forbidden'
case 'list_users': {
const auth = await is(ws);
if(auth) {
ws.subscribe('user/#');//if user type changes, need to auto unsubscribe somehow
ws.send(JSON.stringify({
response_ID: request_ID,
data: (await pool.query('select user_id, email, comt, akt1, cyp2c9, cyp2c19_2, cyp2c19_3, cyp2c19_17 from usr')).rows
}));
} else if(auth === undefined) {
if(ws.user_ID) {
authenticated.delete(ws.user_ID);
ws.publish('user/authenticated', JSON.stringify({
what: 'user/authenticated',//tbd
how: 'replace',
data: authenticated.size
await pool.query(`update usr set ${dawg.join(',')} where user_id = $1`, poop);
ws.send(JSON.stringify({
response_ID: request_ID
}));
if((await pool.query(`update usr set ${dawg.join(',')} where user_id = $1`, poop)).rowCount) {
ws.send(JSON.stringify({
response_ID: request_ID
}));
} else {
authenticated.delete(ws.user_ID);
ws.publish('user/authenticated', JSON.stringify({
what: 'user/authenticated',//tbd
how: 'replace',
data: authenticated.size
}));
delete ws.user_ID;
ws.send(JSON.stringify({
response_ID: request_ID,
data: 'unauthenticated'
}));
}
case 'create_store':
if(isLoggedIn(ws)) {
if(is(ws, 'god')) {
if(notmissing(parameters, ['name', 'address', 'city', 'region', 'country', 'postal_code'], ws, request_ID)) {
let params = [
parameters.name,
parameters.URL,
parameters.address,
parameters.address2 ? parameters.address2 : null,
parameters.city,
parameters.region,
parameters.country,
parameters.postal_code,
Boolean(parameters.delivery),
Boolean(parameters.pickup),
Boolean(parameters.prepayment),
parameters.phone ? parameters.phone : null
];
//I went through the effort of delaying gapi as long as I could and folding three inserts into one transaction, only for the unique constraint to goof me up. Likely won't happen often though, so accept the tradeoff.
let q = 1;
let query = '';
if(Array.isArray(parameters.taxes) && parameters.taxes.length) {
case 'create_store': {
const auth = await is(ws);
if(auth) {
if(notmissing(parameters, ['name', 'address', 'city', 'region', 'country', 'postal_code'], ws, request_ID)) {
let params = [
parameters.name,
parameters.URL,
parameters.address,
parameters.address2 ? parameters.address2 : null,
parameters.city,
parameters.region,
parameters.country,
parameters.postal_code,
Boolean(parameters.delivery),
Boolean(parameters.pickup),
Boolean(parameters.prepayment),
parameters.phone ? parameters.phone : null
];
//I went through the effort of delaying gapi as long as I could and folding three inserts into one transaction, only for the unique constraint to goof me up. Likely won't happen often though, so accept the tradeoff.
let q = 1;
let query = '';
if(Array.isArray(parameters.taxes) && parameters.taxes.length) {
query += `, insert${++q} as (
insert into store_tax (store_id, name, rate, types)
values ${parameters.taxes.map(tax => `((select store_id from insert1), $${params.push(tax.name)}, $${params.push(tax.rate)}, $${params.push(tax.types)})`).join(', ')}
)`;
}
if(Array.isArray(parameters.times)) {
let fuck = [];
for(let i = 0; i < parameters.times.length; ++i) {
try {
let ts = parameters.times[i].time_start.split(':');
ts = Number(parameters.times[i].day_start) * 1440 + Number(ts[0]) * 60 + Number(ts[1]);
let te = parameters.times[i].time_end.split(':');
te = Number(parameters.times[i].day_end) * 1440 + Number(te[0]) * 60 + Number(te[1]);
if(te <= ts) {
fuck.push(`(int4range($${params.push(ts)}, 10080)),(int4range(0, $${params.push(te)}))`);
//oops possibly 0,0 empty range. nvm good job range_agg
} else {
fuck.push(`(int4range($${params.push(ts)}, $${params.push(te)}))`);
}
} catch(e) {}
}
if(fuck.length) {
insert into store_tax (store_id, name, rate)
values ${parameters.taxes.map(tax => `((select store_id from insert1), $${params.push(tax.name)}, $${params.push(tax.rate)})`).join(', ')}
insert into store_time (store_id, open)
select store_id, unnest from insert1 cross join (select unnest(range_agg(column1, true, true)) from (values ${fuck.join(',')}) as ex) as idc
if(Array.isArray(parameters.times)) {
let fuck = [];
for(let i = 0; i < parameters.times.length; ++i) {
try {
let ts = parameters.times[i].time_start.split(':');
ts = Number(parameters.times[i].day_start) * 1440 + Number(ts[0]) * 60 + Number(ts[1]);
let te = parameters.times[i].time_end.split(':');
te = Number(parameters.times[i].day_end) * 1440 + Number(te[0]) * 60 + Number(te[1]);
if(te <= ts) {
fuck.push(`(int4range($${params.push(ts)}, 10080)),(int4range(0, $${params.push(te)}))`);
//oops possibly 0,0 empty range. nvm good job range_agg
} else {
fuck.push(`(int4range($${params.push(ts)}, $${params.push(te)}))`);
}
} catch(e) {}
}
if(fuck.length) {
query += `, insert${++q} as (
insert into store_time (store_id, open)
select store_id, unnest from insert1 cross join (select unnest(range_agg(column1, true, true)) from (values ${fuck.join(',')}) as ex) as idc
)`;
}
}
if(Array.isArray(parameters.images) && parameters.images.length) {
query += `, insert${++q} as (
insert into store_image (store_id, URL)
values ${parameters.images.map(image => `((select store_id from insert1), $${params.push(image)})`).join(', ')}
)`;
}
/*
two google maps api error handlers because their api is inconsistent as fuck
missing api key will return 'REQUEST_DENIED', but 200, so (2)
wrong parameters will return 'INVALID_REQUEST', so 400 (1) for geocode
but 200 (2) for timezone!! 400 throws automatically, but 200 need to be thrown manually
*/
let location;
try {
location = (await googleMapsClient.geocode({params: {address: `${parameters.address}, ${parameters.address2}, ${parameters.city}, ${parameters.region}, ${parameters.country}, ${parameters.postal_code}`, key: process.env.GAPI}})).data;
if(location.status === 'OK') {
location = location.results[0].geometry.location;
} else {
throw location;
if(Array.isArray(parameters.images) && parameters.images.length) {
query += `, insert${++q} as (
insert into store_image (store_id, URL)
values ${parameters.images.map(image => `((select store_id from insert1), $${params.push(image)})`).join(', ')}
)`;
} catch(e) {
if(e.response) {
console.error(e.response.data);//google maps api(1)
/*
two google maps api error handlers because their api is inconsistent as fuck
missing api key will return 'REQUEST_DENIED', but 200, so (2)
wrong parameters will return 'INVALID_REQUEST', so 400 (1) for geocode
but 200 (2) for timezone!! 400 throws automatically, but 200 need to be thrown manually
*/
let location;
try {
location = (await googleMapsClient.geocode({params: {address: `${parameters.address}, ${parameters.address2}, ${parameters.city}, ${parameters.region}, ${parameters.country}, ${parameters.postal_code}`, key: process.env.GAPI}})).data;
if(location.status === 'OK') {
location = location.results[0].geometry.location;
} else {
throw location;
}
} catch(e) {
if(e.response) {
console.error(e.response.data);//google maps api(1)
}
else if(e.status) {
console.error(e);//google maps api(2)
}
ws.send(JSON.stringify({
response_ID: request_ID,
data: 'google geocode failure'
}));//todo: status: 'ZERO_RESULTS' => "invalid address :("
break;
else if(e.status) {
console.error(e);//google maps api(2)
let hints = Promise.all([
request(`http://127.0.0.1:5000/nearest/v1/fuck/${location.lng},${location.lat}`).then(x => JSON.parse(x).waypoints[0].hint),
request(`http://127.0.0.1:5001/nearest/v1/fuck/${location.lng},${location.lat}`).then(x => JSON.parse(x).waypoints[0].hint)
]);
let tz;
try {
tz = (await googleMapsClient.timezone({params: {location, timestamp: 0, key: process.env.GAPI}})).data;
if(tz.status === 'OK') {
tz = tz.timeZoneId;
} else {
throw tz;
}
} catch(e) {
if(e.response) {
console.error(e.response.data);//google maps api(1)
}
else if(e.status) {
console.error(e);//google maps api(2)
}
ws.send(JSON.stringify({
response_ID: request_ID,
data: 'google timezone failure'
}));
break;
ws.send(JSON.stringify({
response_ID: request_ID,
data: 'google geocode failure'
}));//todo: status: 'ZERO_RESULTS' => "invalid address :("
break;
}
let hints = Promise.all([
request(`http://127.0.0.1:5000/nearest/v1/fuck/${location.lng},${location.lat}`).then(x => JSON.parse(x).waypoints[0].hint),
request(`http://127.0.0.1:5001/nearest/v1/fuck/${location.lng},${location.lat}`).then(x => JSON.parse(x).waypoints[0].hint)
]);
let tz;
try {
tz = (await googleMapsClient.timezone({params: {location, timestamp: 0, key: process.env.GAPI}})).data;
if(tz.status === 'OK') {
tz = tz.timeZoneId;
} else {
throw tz;
hints = await hints;
//promise then catch is fine since this is the last thing to do this function. don't need to break;
pool.query(
`with insert1 as (
insert into store (
name,
url,
address,
address2,
city,
region,
country,
postal_code,
delivery,
pickup,
prepayment,
phone,
longitude,
latitude,
osrm_hint_foot,
osrm_hint_car,
timezone
) values (
$1,
$2,
$3,
$4,
$5,
$6,
$7,
$8,
$9,
$10,
$11,
$12,
$${params.push(location.lng)},
$${params.push(location.lat)},
$${params.push(hints[0])},
$${params.push(hints[1])},
$${params.push(tz)}
)
returning store_id
)${query}
select store_id from insert1`,
params
).then(r => {
ws.publish('store', JSON.stringify({
what: 'store',
how: 'add',
data: {
store_id: r.rows[0].store_id,
//other relevant fields
}
}));
ws.send(JSON.stringify({
response_ID: request_ID
}));
}).catch(e => {
/* todo
Class 23 https://www.postgresql.org/docs/current/errcodes-appendix.html with e.code
e.table
https://stackoverflow.com/a/4108266
use implicit names in initialize.sql. then, can use e.constraint, remove e.table + _ from beginning, remove suffix to see which columns are affected
e.detail is fine for 23505, but 23514 just complains about the row as a whole. how can we get the specific details? https://stackoverflow.com/a/47972916 for python...
useful
code: '23505',
detail: 'Key (store, name)=(21, hst) already exists.',
useless
[message]: 'new row for relation "store_tax" violates check constraint "store_tax_rate_check"',
name: 'error',
length: 266,
severity: 'ERROR',
code: '23514',
detail: 'Failing row contains (15, 28, hst, 2, 2020-02-11 20:20:54.923496-05, infinity).',
what I want: rate > 0 and rate < 1
2 > 0 and 2 < 1
true and false
false
*/
console.log(e);
ws.send(JSON.stringify({
response_ID: request_ID,
data: 'db fuckup'
}));
});
} catch(e) {
if(e.response) {
console.error(e.response.data);//google maps api(1)
}
else if(e.status) {
console.error(e);//google maps api(2)
}
ws.send(JSON.stringify({
response_ID: request_ID,
data: 'google timezone failure'
}));
break;
} else {
ws.send(JSON.stringify({
response_ID: request_ID,
data: 'forbidden'
hints = await hints;
//promise then catch is fine since this is the last thing to do this function. don't need to break;
pool.query(
`with insert1 as (
insert into store (
name,
url,
address,
address2,
city,
region,
country,
postal_code,
delivery,
pickup,
prepayment,
phone,
longitude,
latitude,
osrm_hint_foot,
osrm_hint_car,
timezone
) values (
$1,
$2,
$3,
$4,
$5,
$6,
$7,
$8,
$9,
$10,
$11,
$12,
$${params.push(location.lng)},
$${params.push(location.lat)},
$${params.push(hints[0])},
$${params.push(hints[1])},
$${params.push(tz)}
)
returning store_id
)${query}
select * from get_stores((select store_id from insert1))`,
params
).then(r => {
ws.publish('store', JSON.stringify({
what: 'store',
how: 'add',
data: r.rows[0]
}));
ws.send(JSON.stringify({
response_ID: request_ID
}));
}).catch(e => {
/* todo
Class 23 https://www.postgresql.org/docs/current/errcodes-appendix.html with e.code
e.table
https://stackoverflow.com/a/4108266
use implicit names in initialize.sql. then, can use e.constraint, remove e.table + _ from beginning, remove suffix to see which columns are affected
e.detail is fine for 23505, but 23514 just complains about the row as a whole. how can we get the specific details? https://stackoverflow.com/a/47972916 for python...
useful
code: '23505',
detail: 'Key (store, name)=(21, hst) already exists.',
useless
[message]: 'new row for relation "store_tax" violates check constraint "store_tax_rate_check"',
name: 'error',
length: 266,
severity: 'ERROR',
code: '23514',
detail: 'Failing row contains (15, 28, hst, 2, 2020-02-11 20:20:54.923496-05, infinity).',
what I want: rate > 0 and rate < 1
2 > 0 and 2 < 1
true and false
false
*/
console.log(e);
ws.send(JSON.stringify({
response_ID: request_ID,
data: 'db fuckup'
}));
});
}
} else if(auth === undefined) {
if(ws.user_ID) {
authenticated.delete(ws.user_ID);
ws.publish('user/authenticated', JSON.stringify({
what: 'user/authenticated',//tbd
how: 'replace',
data: authenticated.size
case 'cloudinary_preset':
if(isLoggedIn(ws)) {
if(is(ws, 'god')) {// || is(ws, 'store owner')
ws.send(JSON.stringify({
response_ID: request_ID,
data: process.env.CLOUDINARY
}));
} else {
ws.send(JSON.stringify({
response_ID: request_ID,
data: 'forbidden'
case 'cloudinary_preset': {
//owner is not null if want to accept >= store worker as well. right now just >= store owner
const auth = (await pool.query(`select god or owner as t from usr left join user_store using (user_id) where user_id = $1 order by owner desc nulls last limit 1`, [ws.user_ID])).rows?.[0]?.t;
if(auth) {
ws.send(JSON.stringify({
response_ID: request_ID,
data: process.env.CLOUDINARY
}));
} else if(auth === undefined) {
if(ws.user_ID) {
authenticated.delete(ws.user_ID);
ws.publish('user/authenticated', JSON.stringify({
what: 'user/authenticated',//tbd
how: 'replace',
data: authenticated.size
}));//client side virtual event user god false
let r = (await pool.query({text: 'select store_id, owner from user_store where user_id = $1', values: [ws.user_ID], rowMode: 'array'})).rows;
ws.send(JSON.stringify({
what: 'user',
how: 'update',
data: r.length ? Object.fromEntries(r) : null
break;
case 'product_form':
if(isLoggedIn(ws)) {
if(is(ws, 'god')) {
const [strains, pgenum] = await Promise.all([
pool.query('select strain_id as id, display_name as name from strain').then(x => x.rows),
pool.query('select array_to_json(enum_range(null::species)) as species, array_to_json(enum_range(null::product_type)) as product_types').then(x => x.rows[0])
]);
ws.send(JSON.stringify({
response_ID: request_ID,
data: {strains, ...pgenum}
break;}
case 'product_form': {
//owner is not null if want to accept >= store worker as well. right now just >= store owner
const auth = (await pool.query(`select god or owner as t from usr left join user_store using (user_id) where user_id = $1 order by owner desc nulls last limit 1`, [ws.user_ID])).rows?.[0]?.t;
if(auth) {
const [strains, pgenum] = await Promise.all([
pool.query('select strain_id as id, display_name as name from strain').then(x => x.rows),
pool.query('select array_to_json(enum_range(null::species)) as species, array_to_json(enum_range(null::product_type)) as product_types').then(x => x.rows[0])
]);
ws.send(JSON.stringify({
response_ID: request_ID,
data: {strains, ...pgenum}
}));
} else if(auth === undefined) {
if(ws.user_ID) {
authenticated.delete(ws.user_ID);
ws.publish('user/authenticated', JSON.stringify({
what: 'user/authenticated',//tbd
how: 'replace',
data: authenticated.size
}));//client side virtual event user god false
let r = (await pool.query({text: 'select store_id, owner from user_store where user_id = $1', values: [ws.user_ID], rowMode: 'array'})).rows;
ws.send(JSON.stringify({
what: 'user',
how: 'update',
data: r.length ? Object.fromEntries(r) : null
break;
case 'create_product':
if(isLoggedIn(ws)) {
if(is(ws, 'god')) {
try {
let q = 1;
let [producer, brand, joined_name, display_name] = normalize(parameters.brand, parameters.name, parameters.type);
let params = [
producer,
brand,
joined_name,
display_name,
parameters.type,
parameters.species,
parameters.strain,
parameters.description,
parameters.terpenes
];
let query = `with insert1 as (insert into product (producer, brand, display_name, joined_name, type, species, strain_id, description, terpenes, show) values ($1, $2, $3, $4, $5, $6, $7, $8, $9, true) on conflict do nothing returning product_id)`;
if(Array.isArray(parameters.variants) && parameters.variants.length) {
if(parameters.type === 'flower') {
let values = parameters.variants.reduce((acc, v) => {
if(isFinite(v.quantity)) {
acc.push(`($${params.push(v.quantity)})`);
}
return acc;
}, []);
if(values.length) {
query += `, insert${++q} as (insert into variant (product_id, quantity) select product_id, x::numeric from insert1 cross join (values ${values.join(',')}) as t(x) on conflict do nothing)`;
break;}
case 'create_product': {
const auth = (await pool.query(`select god, owner from usr left join user_store using (user_id) where user_id = $1 order by owner desc nulls last limit 1`, [ws.user_ID])).rows?.[0];
if(auth === undefined) {
if(ws.user_ID) {
authenticated.delete(ws.user_ID);
ws.publish('user/authenticated', JSON.stringify({
what: 'user/authenticated',//tbd
how: 'replace',
data: authenticated.size
}));
delete ws.user_ID;
}
ws.send(JSON.stringify({
response_ID: request_ID,
data: 'unauthenticated'
}));//client side virtual event user null
} else if(auth.god || auth.owner) {
try {
let q = 1;
let [producer, brand, joined_name, display_name] = normalize(parameters.brand, parameters.name, parameters.type);
let params = [
producer,
brand,
joined_name,
display_name,
parameters.type,
parameters.species,
parameters.strain,
parameters.description,
parameters.terpenes,
auth.god
];
let query = `with insert1 as (insert into product (producer, brand, display_name, joined_name, type, species, strain_id, description, terpenes, show) values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) on conflict do nothing returning product_id)`;
if(Array.isArray(parameters.variants) && parameters.variants.length) {
if(parameters.type === 'flower') {
let values = parameters.variants.reduce((acc, v) => {
if(isFinite(v.quantity)) {
acc.push(`($${params.push(v.quantity)})`);
} else if(parameters.type === 'preroll' || parameters.type === 'tea' || parameters.type === 'gummy' || parameters.type === 'mint' || parameters.type === 'chocolate' || parameters.type === 'baked') {
let values = parameters.variants.reduce((acc, v) => {
if(isFinite(v.quantity) && Number.isInteger(Number(v.portions))) {
acc.push(`($${params.push(v.portions)}, $${params.push(v.quantity)})`);
}
return acc;
}, []);
if(values.length) {
query += `, insert${++q} as (insert into variant (product_id, portions, quantity) select product_id, x::smallint, y::numeric from insert1 cross join (values ${values.join(',')}) as t(x, y) on conflict do nothing)`;
return acc;
}, []);
if(values.length) {
query += `, insert${++q} as (insert into variant (product_id, quantity) select product_id, x::numeric from insert1 cross join (values ${values.join(',')}) as t(x) on conflict do nothing)`;
}
} else if(parameters.type === 'preroll' || parameters.type === 'tea' || parameters.type === 'gummy' || parameters.type === 'mint' || parameters.type === 'chocolate' || parameters.type === 'baked') {
let values = parameters.variants.reduce((acc, v) => {
if(isFinite(v.quantity) && Number.isInteger(Number(v.portions))) {
acc.push(`($${params.push(v.portions)}, $${params.push(v.quantity)})`);
} else if(parameters.type === 'cartridge' || parameters.type === 'disposable') {
let values = parameters.variants.reduce((acc, v) => {
if(isFinite(v.quantity)) {
acc.push(`($${params.push(v.quantity)}, ${v.flavor ? '$' + params.push(v.flavor) : 'null'})`);
}
return acc;
}, []);
if(values.length) {
query += `, insert${++q} as (insert into variant (product_id, quantity, flavor) select product_id, x::numeric, y from insert1 cross join (values ${values.join(',')}) as t(x, y) on conflict do nothing)`;
return acc;
}, []);
if(values.length) {
query += `, insert${++q} as (insert into variant (product_id, portions, quantity) select product_id, x::smallint, y::numeric from insert1 cross join (values ${values.join(',')}) as t(x, y) on conflict do nothing)`;
}
} else if(parameters.type === 'cartridge' || parameters.type === 'disposable') {
let values = parameters.variants.reduce((acc, v) => {
if(isFinite(v.quantity)) {
acc.push(`($${params.push(v.quantity)}, ${v.flavor ? '$' + params.push(v.flavor) : 'null'})`);
} else if (parameters.type === 'oil' || parameters.type === 'spray') {
let values = parameters.variants.reduce((acc, v) => {
if(isFinite(v.gram_equivalency) && Number.isInteger(Number(v.quantity))) {
acc.push(`($${params.push(v.quantity)}, $${params.push(v.gram_equivalency)})`);
}
return acc;
}, []);
if(values.length) {
query += `, insert${++q} as (insert into variant (product_id, quantity, gram_equivalency) select product_id, y::numeric, z::numeric from insert1 cross join (values ${values.join(',')}) as t(y, z) on conflict do nothing)`;
return acc;
}, []);
if(values.length) {
query += `, insert${++q} as (insert into variant (product_id, quantity, flavor) select product_id, x::numeric, y from insert1 cross join (values ${values.join(',')}) as t(x, y) on conflict do nothing)`;
}
} else if (parameters.type === 'oil' || parameters.type === 'spray') {
let values = parameters.variants.reduce((acc, v) => {
if(isFinite(v.gram_equivalency) && Number.isInteger(Number(v.quantity))) {
acc.push(`($${params.push(v.quantity)}, $${params.push(v.gram_equivalency)})`);
} else if(parameters.type === 'capsule') {
let values = parameters.variants.reduce((acc, v) => {
if(isFinite(v.gram_equivalency) && Number.isInteger(Number(v.portions))) {
acc.push(`($${params.push(v.portions)}, ${isFinite(v.quantity) ? '$' + params.push(v.quantity) : 'null'}, $${params.push(v.gram_equivalency)})`);
}
return acc;
}, []);
if(values.length) {
query += `, insert${++q} as (insert into variant (product_id, portions, quantity, gram_equivalency) select product_id, x::smallint, y::numeric, z::numeric from insert1 cross join (values ${values.join(',')}) as t(x, y, z) on conflict do nothing)`;
return acc;
}, []);
if(values.length) {
query += `, insert${++q} as (insert into variant (product_id, quantity, gram_equivalency) select product_id, y::numeric, z::numeric from insert1 cross join (values ${values.join(',')}) as t(y, z) on conflict do nothing)`;
}
} else if(parameters.type === 'capsule') {
let values = parameters.variants.reduce((acc, v) => {
if(isFinite(v.gram_equivalency) && Number.isInteger(Number(v.portions))) {
acc.push(`($${params.push(v.portions)}, ${isFinite(v.quantity) ? '$' + params.push(v.quantity) : 'null'}, $${params.push(v.gram_equivalency)})`);
return acc;
}, []);
if(values.length) {
query += `, insert${++q} as (insert into variant (product_id, portions, quantity, gram_equivalency) select product_id, x::smallint, y::numeric, z::numeric from insert1 cross join (values ${values.join(',')}) as t(x, y, z) on conflict do nothing)`;
}
if(Array.isArray(parameters.images) && parameters.images.length) {
query += `, insert${++q} as (insert into product_image (product_id, destination) select product_id, x from insert1 cross join (values ${parameters.images.map(i => `(${params.push(i)})`).join(',')}) as t(x) on conflict do nothing)`;
let product = (await pool.query(query + ' select product_id from insert1', params)).rows;
ws.send(JSON.stringify({
response_ID: request_ID,
data: product.length === 1 && Number.isInteger(Number(product[0].product_id)) ? undefined : 'product already exists'
}));
} catch(e) {
console.error(e);
ws.send(JSON.stringify({
response_ID: request_ID,
data: 'check log'
}));
}
if(Array.isArray(parameters.images) && parameters.images.length) {
query += `, insert${++q} as (insert into product_image (product_id, destination) select product_id, x from insert1 cross join (values ${parameters.images.map(i => `(${params.push(i)})`).join(',')}) as t(x) on conflict do nothing)`;
} else {
let product = (await pool.query(query + ' select product_id from insert1', params)).rows;
ws.send(JSON.stringify({
response_ID: request_ID,
data: product.length === 1 && Number.isInteger(Number(product[0].product_id)) ? undefined : 'product already exists'
}));
} catch(e) {
console.error(e);
}));//client side virtual event user god false
let r = (await pool.query({text: 'select store_id, owner from user_store where user_id = $1', values: [ws.user_ID], rowMode: 'array'})).rows;
ws.send(JSON.stringify({
what: 'user',
how: 'update',
data: r.length ? Object.fromEntries(r) : null
break;
case 'strains':
ws.send(JSON.stringify({
response_ID: request_ID,
data: (await pool.query(`select strain_id as id, display_name as name, s_species as species from strain`)).rows
}));
break;
break; }
case 'menu_item_loader':
if(isLoggedIn(ws)) {
if(is(ws, 'god')) {
ws.send(JSON.stringify({
response_ID: request_ID,
data: (await pool.query(`select * from menu_item_loader($1)`, [parameters.store])).rows
case 'menu_item_loader': {
const auth = await is(ws, parameters.store);
if(auth) {
ws.send(JSON.stringify({
response_ID: request_ID,
data: (await pool.query(`select * from menu_item_loader($1)`, [parameters.store])).rows
}));
} else if(auth === undefined) {
if(ws.user_ID) {
authenticated.delete(ws.user_ID);
ws.publish('user/authenticated', JSON.stringify({
what: 'user/authenticated',//tbd
how: 'replace',
data: authenticated.size
}));//client side virtual event user god false
let r = (await pool.query({text: 'select store_id, owner from user_store where user_id = $1', values: [ws.user_ID], rowMode: 'array'})).rows;
ws.send(JSON.stringify({
what: 'user',
how: 'update',
data: r.length ? Object.fromEntries(r) : null
break;
case 'menu_items_builder':
if(isLoggedIn(ws)) {
if(is(ws, 'god')) {
let shit;
if(Array.isArray(parameters.fresh) && parameters.fresh.length) {
let query = 'insert into menu_item values ';//since we're god, trusting parameters.store is fine. otherwise, need to check
let params = [];
for(let i = 0; i < parameters.fresh.length; ++i) {
query += `(default, $${i * 7 + 1}, $${i * 7 + 2}, $${i * 7 + 3}, $${i * 7 + 4}, $${i * 7 + 5}, $${i * 7 + 6}, $${i * 7 + 7}),`;
params.push(
parameters.fresh[i].variant,
parameters.store,
`[${parameters.fresh[i].min_cbd},${parameters.fresh[i].max_cbd}]`,
`[${parameters.fresh[i].min_thc},${parameters.fresh[i].max_thc}]`,
parameters.fresh[i].price,
parameters.fresh[i].stock,
Boolean(parameters.fresh[i].featured)
);
}
shit = (await pool.query({text: query.slice(0, -1) + ` on conflict (variant_id, store_id) do update set
cbd = excluded.cbd,
thc = excluded.thc,
price = excluded.price,
stock = excluded.stock,
featured = excluded.featured
where (menu_item.cbd, menu_item.thc, menu_item.price, menu_item.stock, menu_item.featured) is distinct from
(excluded.cbd, excluded.thc, excluded.price, excluded.stock, excluded.featured) returning menu_item_id
`,
values: params,
rowMode: 'array'
})).rows.flat();//risky: https://stackoverflow.com/questions/5439293
break;}
case 'menu_items_builder': {
const auth = await is(ws, parameters.store);
if(auth) {
let shit;
if(Array.isArray(parameters.fresh) && parameters.fresh.length) {
let query = 'insert into menu_item values ';
let params = [];
for(let i = 0; i < parameters.fresh.length; ++i) {
query += `(default, $${i * 7 + 1}, $${i * 7 + 2}, $${i * 7 + 3}, $${i * 7 + 4}, $${i * 7 + 5}, $${i * 7 + 6}, $${i * 7 + 7}),`;
params.push(
parameters.fresh[i].variant,
parameters.store,
`[${parameters.fresh[i].min_cbd},${parameters.fresh[i].max_cbd}]`,
`[${parameters.fresh[i].min_thc},${parameters.fresh[i].max_thc}]`,
parameters.fresh[i].price,
parameters.fresh[i].stock,
Boolean(parameters.fresh[i].featured)
);
if(Array.isArray(parameters.updated)) {
//whenever store_owner lands: update menu_item set where store = (select store from usr where id = $1) and id = $2 or keep store in ws?
for(let i = 0; i < parameters.updated.length; ++i) {
if(Number.isInteger(Number(parameters.updated[i].menu_item))) {
let params = [parameters.store, parameters.updated[i].menu_item];
let what = [];
if(isFinite(parameters.updated[i].price)) {
what.push(`price = $${params.push(parameters.updated[i].price)}`);
}
if(Number.isInteger(Number(parameters.updated[i].stock))) {
what.push(`stock = $${params.push(parameters.updated[i].stock)}`);
}
if(typeof parameters.updated[i].featured === 'boolean') {
what.push(`featured = $${params.push(parameters.updated[i].featured)}`);
}
if(isFinite(parameters.updated[i].min_cbd) && isFinite(parameters.updated[i].max_cbd)) {
what.push(`cbd = $${params.push(`[${parameters.updated[i].min_cbd},${parameters.updated[i].max_cbd}]`)}`);
} else if(isFinite(parameters.updated[i].min_cbd)) {
what.push(`cbd = numrange($${params.push(parameters.updated[i].min_cbd)}, upper(cbd), '[]')`);
} else if(isFinite(parameters.updated[i].max_cbd)) {
what.push(`cbd = numrange(lower(cbd), $${params.push(parameters.updated[i].max_cbd)}, '[]')`);
}
if(isFinite(parameters.updated[i].min_thc) && isFinite(parameters.updated[i].max_thc)) {
what.push(`thc = $${params.push(`[${parameters.updated[i].min_thc},${parameters.updated[i].max_thc}]`)}`);
} else if(isFinite(parameters.updated[i].min_thc)) {
what.push(`thc = numrange($${params.push(parameters.updated[i].min_thc)}, upper(thc), '[]')`);
} else if(isFinite(parameters.updated[i].max_thc)) {
what.push(`thc = numrange(lower(thc), $${params.push(parameters.updated[i].max_thc)}, '[]')`);
}
if(what.length > 0) {
await pool.query(`update menu_item set ${what.join(', ')} where store_id = $1 and menu_item_id = $2`, params);
}
shit = (await pool.query({text: query.slice(0, -1) + ` on conflict (variant_id, store_id) do update set
cbd = excluded.cbd,
thc = excluded.thc,
price = excluded.price,
stock = excluded.stock,
featured = excluded.featured
where (menu_item.cbd, menu_item.thc, menu_item.price, menu_item.stock, menu_item.featured) is distinct from
(excluded.cbd, excluded.thc, excluded.price, excluded.stock, excluded.featured) returning menu_item_id
`,
values: params,
rowMode: 'array'
})).rows.flat();//risky: https://stackoverflow.com/questions/5439293
}
if(Array.isArray(parameters.updated)) {
for(let i = 0; i < parameters.updated.length; ++i) {
if(Number.isInteger(Number(parameters.updated[i].menu_item))) {
let params = [parameters.store, parameters.updated[i].menu_item];
let what = [];
if(isFinite(parameters.updated[i].price)) {
what.push(`price = $${params.push(parameters.updated[i].price)}`);
}
if(Number.isInteger(Number(parameters.updated[i].stock))) {
what.push(`stock = $${params.push(parameters.updated[i].stock)}`);
}
if(typeof parameters.updated[i].featured === 'boolean') {
what.push(`featured = $${params.push(parameters.updated[i].featured)}`);
}
if(isFinite(parameters.updated[i].min_cbd) && isFinite(parameters.updated[i].max_cbd)) {
what.push(`cbd = $${params.push(`[${parameters.updated[i].min_cbd},${parameters.updated[i].max_cbd}]`)}`);
} else if(isFinite(parameters.updated[i].min_cbd)) {
what.push(`cbd = numrange($${params.push(parameters.updated[i].min_cbd)}, upper(cbd), '[]')`);
} else if(isFinite(parameters.updated[i].max_cbd)) {
what.push(`cbd = numrange(lower(cbd), $${params.push(parameters.updated[i].max_cbd)}, '[]')`);
}
if(isFinite(parameters.updated[i].min_thc) && isFinite(parameters.updated[i].max_thc)) {
what.push(`thc = $${params.push(`[${parameters.updated[i].min_thc},${parameters.updated[i].max_thc}]`)}`);
} else if(isFinite(parameters.updated[i].min_thc)) {
what.push(`thc = numrange($${params.push(parameters.updated[i].min_thc)}, upper(thc), '[]')`);
} else if(isFinite(parameters.updated[i].max_thc)) {
what.push(`thc = numrange(lower(thc), $${params.push(parameters.updated[i].max_thc)}, '[]')`);
}
if(what.length > 0) {
await pool.query(`update menu_item set ${what.join(', ')} where store_id = $1 and menu_item_id = $2`, params);
if(Array.isArray(parameters.deleted) && parameters.deleted.length) {
//need to check if menu item actually belongs to the store
//and check if store belongs to the user
await pool.query('delete from menu_item where menu_item_id = any($1)', [parameters.deleted]);
}
ws.send(JSON.stringify({
response_ID: request_ID,
data: shit
}));
} else {
ws.send(JSON.stringify({
response_ID: request_ID,
data: 'forbidden'
}
if(Array.isArray(parameters.deleted) && parameters.deleted.length) {
await pool.query('delete from menu_item where menu_item_id = any($1) and store_id = $2', [parameters.deleted, parameters.store]);
}
ws.send(JSON.stringify({
response_ID: request_ID,
data: shit
}));
} else if(auth === undefined) {
if(ws.user_ID) {
authenticated.delete(ws.user_ID);
ws.publish('user/authenticated', JSON.stringify({
what: 'user/authenticated',//tbd
how: 'replace',
data: authenticated.size
}));//client side virtual event user god false
let r = (await pool.query({text: 'select store_id, owner from user_store where user_id = $1', values: [ws.user_ID], rowMode: 'array'})).rows;
ws.send(JSON.stringify({
what: 'user',
how: 'update',
data: r.length ? Object.fromEntries(r) : null
}));
}
//send notification to store
}// else if()//parameters.cart_id, paramaters.email, parameters.password
//if login success
//delete from cart where user_id = $1 and status = 'active' and store_id = $2
//update cart set user_id = $1 where cart_id = $2//this will fail if there's another placed || ready with same store, which is ok. let them know
//if login failure
//do nothing with carts, ask to retry
//else if () //parameters.cart_id, paramaters.email
/* but with empty password ""
update cart set status = 'placed', user_id = $2 where cart_id = $1
subquery doesn't work :(
try {
let newUser = await pool.query('insert into usr (email, passphrase_hash) values ($1, $2) returning user_id, email, type', [parameters.email, await argon2.hash(parameters.passphrase)]);//technically, we only need the id??
ws.send(JSON.stringify({
response_ID: request_ID
}));
ws.publish('user/s', JSON.stringify({
what: "users",
how: 'add',
data: newUser.rows[0]
//ws.publish('/users/' + id? dunno if needed, JSON.stringify(event.json id, email, add))
} catch(e) {
let error = 'user already exists'; do nothing with carts, ask to retry
if(e.constraint !== 'usr_email_key') {
console.error(e);
error = 'error';
}
ws.send(JSON.stringify({
response_ID: request_ID,
data: error
}));
case 'user_reservations':
if(isLoggedIn(ws)) {
if(is(ws, 'god') && parameters && Number.isInteger(Number(parameters.user_ID))) {
//trust parameters.user_id
} else {
ws.send(JSON.stringify({
response_ID: request_ID,
data: (await pool.query(`select * from user_reservations($1)`, [ws.user_ID])).rows
case 'user_reservations': {
const auth = await is(ws);
let n = Number.isInteger(Number(parameters?.user_ID));
if(auth === undefined) {
if(ws.user_ID) {
authenticated.delete(ws.user_ID);
ws.publish('user/authenticated', JSON.stringify({
what: 'user/authenticated',//tbd
how: 'replace',
data: authenticated.size
const id = auth && n ? parameters.user_ID : ws.user_ID;
ws.subscribe('user/reservations/' + id);
let data = (await pool.query(`select * from user_reservations($1)`, [id])).rows;
for(let i = 0; i < data.length; ++i) {
let out;
for(let j = 0; j < data[i].statuses.length; ++j) {
if(j === data[i].statuses.length - 1 && data[i].statuses[j].end !== Infinity && (data[i].statuses[j].status === 'placed' || data[i].statuses[j].status === 'ready')) {
out = {status: 'store_cancelled', as_of: data[i].statuses[j].end};
}
delete data[i].statuses[j].end;
}
if(out) {
data[i].statuses.push(out);
}
}
break;
case 'user_cancel_reservation':
if(isLoggedIn(ws)) {
if(is(ws, 'god') && parameters && Number.isInteger(Number(parameters.user_ID))) {
//trust parameters.user_id
} else {
ws.send(JSON.stringify({
response_ID: request_ID,
data: (await pool.query(`update cart set status = 'user_cancelled' where user_id = $1 and store_id = $2 and (status = 'placed' or status = 'ready')`, [ws.user_ID, parameters.store])).rowCount
break;}
case 'user_cancel_reservation': {
const auth = await is(ws);
let n = Number.isInteger(Number(parameters?.user_ID));
if(auth === undefined) {
if(ws.user_ID) {
authenticated.delete(ws.user_ID);
ws.publish('user/authenticated', JSON.stringify({
what: 'user/authenticated',//tbd
how: 'replace',
data: authenticated.size
break;
case 'store_reservations':
if(isLoggedIn(ws)) {
if(is(ws, 'god') && parameters && Number.isInteger(Number(parameters.store))) {
ws.subscribe('store/reservations/' + parameters.store)
break;}
case 'store_reservations': {
if(Number.isInteger(Number(parameters?.store))) {
const auth = await is(ws, parameters.store);
if(auth) {
ws.subscribe('store/reservations/' + parameters.store);
let data = (await pool.query(`select * from store_reservations($1)`, [parameters.store])).rows;
for(let i = 0; i < data.length; ++i) {
let out;
for(let j = 0; j < data[i].statuses.length; ++j) {
if(j === data[i].statuses.length - 1 && data[i].statuses[j].end !== Infinity && (data[i].statuses[j].status === 'placed' || data[i].statuses[j].status === 'ready')) {
out = {status: 'user_cancelled', as_of: data[i].statuses[j].end};
}
delete data[i].statuses[j].end;
}
if(out) {
data[i].statuses.push(out);
}
}
ws.send(JSON.stringify({
response_ID: request_ID,
data
}));
} else if(auth === undefined) {
if(ws.user_ID) {
authenticated.delete(ws.user_ID);
ws.publish('user/authenticated', JSON.stringify({
what: 'user/authenticated',//tbd
how: 'replace',
data: authenticated.size
}));
delete ws.user_ID;
}
ws.send(JSON.stringify({
response_ID: request_ID,
data: 'unauthenticated'
}));//client side virtual event user null
} else {
data: (await pool.query(`select * from store_reservations($1)`, [parameters.store])).rows
data: 'unauthorized'
}));//client side virtual event user god false
let r = (await pool.query({text: 'select store_id, owner from user_store where user_id = $1', values: [ws.user_ID], rowMode: 'array'})).rows;
ws.send(JSON.stringify({
what: 'user',
how: 'update',
data: r.length ? Object.fromEntries(r) : null
break;
case 'store_update_reservation':
if(isLoggedIn(ws)) {
if(is(ws, 'god') && parameters && Number.isInteger(Number(parameters.store)) && Number.isInteger(Number(parameters.user))) {
let data = 'invalid update';
break;}
case 'store_update_reservation': {
if(Number.isInteger(Number(parameters?.store)) && Number.isInteger(Number(parameters?.user))) {
const auth = await is(ws, parameters.store);
if(auth) {
let data;
`update cart set status = 'ready' where store_id = $1 and user_id = $2 and status = 'placed'`,
[parameters.store, parameters.user])).rowCount
`update cart set status = 'ready' where store_id = $1 and user_id = $2 and status = 'placed' returning system_time_start`,
[parameters.store, parameters.user]))
`update cart set status = 'paid' where store_id = $1 and user_id = $2 and status = 'ready'`,
[parameters.store, parameters.user])).rowCount
`update cart set status = 'paid' where store_id = $1 and user_id = $2 and status = 'ready' returning system_time_start`,
[parameters.store, parameters.user]))
`update cart set status = 'store_cancelled' where store_id = $1 and user_id = $2 and status = 'placed'`,
[parameters.store, parameters.user])).rowCount
`update cart set status = 'store_cancelled' where store_id = $1 and user_id = $2 and status = 'placed' returning system_time_start`,
[parameters.store, parameters.user]))
`update cart set status = 'no_show' where store_id = $1 and user_id = $2 and status = 'ready'`,
[parameters.store, parameters.user])).rowCount
`update cart set status = 'no_show' where store_id = $1 and user_id = $2 and status = 'ready' returning system_time_start`,
[parameters.store, parameters.user]))
}
if(data?.rowCount === 1) {
ws.send(JSON.stringify({
response_ID: request_ID
}));
if(parameters.update === 'cancel') {
parameters.update = 'store_cancelled';
} else if(parameters.update === 'noshow') {
parameters.update = 'no_show';
}
ws.publish('store/reservations/' + parameters.store, JSON.stringify({
what: 'store/reservations/' + parameters.store,
how: 'update',
data: {
user_id: id,
status: parameters.update,
as_of: data.rows[0].system_time_start
}
}));
} else {
ws.send(JSON.stringify({
response_ID: request_ID,
data: 'invalid update'
}));
}
} else if(auth === undefined) {
if(ws.user_ID) {
authenticated.delete(ws.user_ID);
ws.publish('user/authenticated', JSON.stringify({
what: 'user/authenticated',//tbd
how: 'replace',
data: authenticated.size
}));
delete ws.user_ID;
data
data: 'unauthenticated'
}));//client side virtual event user null
} else {
ws.send(JSON.stringify({
response_ID: request_ID,
data: 'unauthorized'
}));//client side virtual event user god false
let r = (await pool.query({text: 'select store_id, owner from user_store where user_id = $1', values: [ws.user_ID], rowMode: 'array'})).rows;
ws.send(JSON.stringify({
what: 'user',
how: 'update',
data: r.length ? Object.fromEntries(r) : null