Compare commits

...

15 Commits
prod ... master

Author SHA1 Message Date
964ab26007 Set title on unacceptable merch type error 2021-02-28 18:48:14 -05:00
6b07ec7d07 Check keywords on transaction, update shop gold
Shops now have a "type" and keywords of items they only deal with.

Also add vendors table in preparation for implementing the endpoint.
2021-02-12 00:35:19 -05:00
a64caa4081 Add keywords to transactions table
Keywords are now saved to the merchandise_list form_list when selling to the
shop.
2021-02-09 23:46:42 -05:00
80345c3a6f Add keywords to Merchandise 2021-02-09 01:00:28 -05:00
55c68fee2c Fix incorrect constraint in error handler 2021-01-15 17:22:50 -05:00
2f95fab825 Update sqlx-data.json 2020-11-29 02:26:50 -05:00
81840b3d34 Add shelves list to interior_ref_lists
So that shops can have multiple shelves that save their page, filter, sort, etc.
state to the server. Buttons for the shelves are reconstructed in the plugin
during the load shop procedure.
2020-11-21 01:29:25 -05:00
0adbf7c5c0 Use non-blocking logging io and LTO in release
Also tweak README explanation of sqlx prepare.
2020-11-18 23:28:03 -05:00
b214786415 Nicer 404 error with problem JSON 2020-11-15 00:42:53 -05:00
d9a891c6b5 Update sqlx-data.json 2020-11-14 22:46:18 -05:00
50184da1f6 Fixed bincode responses, refactored content_type handling, better error reporting
Fixed issues with Bincode responses not actually being readable, oops. Also fix handling Bincode requests.

Added `TypedCache` for DRYing up GET request content-type handling.

Added `DeserializedBody` for DRYing up POST/PATCH request conent-type handling.

Removed "Unsaved" structs since I could just mutate Posted structs instead.

Added improved error reporting and stopped sending unfiltered interal error data.

Upgraded sqlx to proper 0.4.1 release.
2020-11-14 02:19:33 -05:00
0bc94e4b7d Add custom server header to responses 2020-11-12 21:05:04 -05:00
a1107b7100 Better readme instructions for docker and tls 2020-11-12 21:01:29 -05:00
9949c537a0 Add more docker setup instructions 2020-11-12 01:41:06 -05:00
d277b5c5cd Allow configuring port, serve over https 2020-11-11 18:41:38 -05:00
23 changed files with 1576 additions and 1078 deletions

333
Cargo.lock generated
View File

@ -146,6 +146,7 @@ dependencies = [
"sqlx", "sqlx",
"tokio", "tokio",
"tracing", "tracing",
"tracing-appender",
"tracing-futures", "tracing-futures",
"tracing-subscriber", "tracing-subscriber",
"url", "url",
@ -236,6 +237,12 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39092a32794787acd8525ee150305ff051b0aa6cc2abaf193924f5ab05425f39" checksum = "39092a32794787acd8525ee150305ff051b0aa6cc2abaf193924f5ab05425f39"
[[package]]
name = "bumpalo"
version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e8c087f005730276d1096a652e92a8bacee2e2472bcc9715a74d2bec38b5820"
[[package]] [[package]]
name = "byte-tools" name = "byte-tools"
version = "0.3.1" version = "0.3.1"
@ -322,22 +329,6 @@ dependencies = [
"proc-macro-hack", "proc-macro-hack",
] ]
[[package]]
name = "core-foundation"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57d24c7a13c43e870e37c1556b74555437870a04514f7685f5b354e090567171"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "core-foundation-sys"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3a71ab494c0b5b860bdc8407ae08978052417070c2ced38573a9157ad75b8ac"
[[package]] [[package]]
name = "cpuid-bool" name = "cpuid-bool"
version = "0.1.2" version = "0.1.2"
@ -467,21 +458,6 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "foreign-types"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
dependencies = [
"foreign-types-shared",
]
[[package]]
name = "foreign-types-shared"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]] [[package]]
name = "fuchsia-cprng" name = "fuchsia-cprng"
version = "0.1.1" version = "0.1.1"
@ -863,6 +839,15 @@ version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6" checksum = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6"
[[package]]
name = "js-sys"
version = "0.3.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca059e81d9486668f12d455a4ea6daa600bd408134cd17e3d3fb5a32d1f016f8"
dependencies = [
"wasm-bindgen",
]
[[package]] [[package]]
name = "kernel32-sys" name = "kernel32-sys"
version = "0.2.2" version = "0.2.2"
@ -1067,24 +1052,6 @@ dependencies = [
"twoway", "twoway",
] ]
[[package]]
name = "native-tls"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b0d88c06fe90d5ee94048ba40409ef1d9315d86f6f38c2efdaad4fb50c58b2d"
dependencies = [
"lazy_static",
"libc",
"log",
"openssl",
"openssl-probe",
"openssl-sys",
"schannel",
"security-framework",
"security-framework-sys",
"tempfile",
]
[[package]] [[package]]
name = "net2" name = "net2"
version = "0.2.34" version = "0.2.34"
@ -1143,39 +1110,12 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
[[package]]
name = "openssl"
version = "0.10.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d575eff3665419f9b83678ff2815858ad9d11567e082f5ac1814baba4e2bcb4"
dependencies = [
"bitflags",
"cfg-if 0.1.10",
"foreign-types",
"lazy_static",
"libc",
"openssl-sys",
]
[[package]] [[package]]
name = "openssl-probe" name = "openssl-probe"
version = "0.1.2" version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de" checksum = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de"
[[package]]
name = "openssl-sys"
version = "0.9.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a842db4709b604f0fe5d1170ae3565899be2ad3d9cbc72dedc789ac0511f78de"
dependencies = [
"autocfg 1.0.0",
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]] [[package]]
name = "parking_lot" name = "parking_lot"
version = "0.11.0" version = "0.11.0"
@ -1240,12 +1180,6 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "pkg-config"
version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d36492546b6af1463394d46f0c834346f31548646f6ba10849802c9c9a27ac33"
[[package]] [[package]]
name = "ppv-lite86" name = "ppv-lite86"
version = "0.2.8" version = "0.2.8"
@ -1487,6 +1421,34 @@ dependencies = [
"winapi 0.3.9", "winapi 0.3.9",
] ]
[[package]]
name = "ring"
version = "0.16.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "952cd6b98c85bbc30efa1ba5783b8abf12fec8b3287ffa52605b9432313e34e4"
dependencies = [
"cc",
"libc",
"once_cell",
"spin",
"untrusted",
"web-sys",
"winapi 0.3.9",
]
[[package]]
name = "rustls"
version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d1126dcf58e93cee7d098dbda643b5f92ed724f1f6a63007c1116eed6700c81"
dependencies = [
"base64 0.12.3",
"log",
"ring",
"sct",
"webpki",
]
[[package]] [[package]]
name = "ryu" name = "ryu"
version = "1.0.5" version = "1.0.5"
@ -1499,16 +1461,6 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072"
[[package]]
name = "schannel"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75"
dependencies = [
"lazy_static",
"winapi 0.3.9",
]
[[package]] [[package]]
name = "scoped-tls" name = "scoped-tls"
version = "1.0.0" version = "1.0.0"
@ -1521,35 +1473,22 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "sct"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3042af939fca8c3453b7af0f1c66e533a15a86169e39de2657310ade8f98d3c"
dependencies = [
"ring",
"untrusted",
]
[[package]] [[package]]
name = "seahash" name = "seahash"
version = "4.0.1" version = "4.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39ee459cae272d224928ca09a1df5406da984f263dc544f9f8bde92a8c3dc916" checksum = "39ee459cae272d224928ca09a1df5406da984f263dc544f9f8bde92a8c3dc916"
[[package]]
name = "security-framework"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64808902d7d99f78eaddd2b4e2509713babc3dc3c85ad6f4c447680f3c01e535"
dependencies = [
"bitflags",
"core-foundation",
"core-foundation-sys",
"libc",
"security-framework-sys",
]
[[package]]
name = "security-framework-sys"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17bf11d99252f512695eb468de5516e5cf75455521e69dfe343f3b74e4748405"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.114" version = "1.0.114"
@ -1665,6 +1604,12 @@ dependencies = [
"winapi 0.3.9", "winapi 0.3.9",
] ]
[[package]]
name = "spin"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
[[package]] [[package]]
name = "sqlformat" name = "sqlformat"
version = "0.1.0" version = "0.1.0"
@ -1678,8 +1623,9 @@ dependencies = [
[[package]] [[package]]
name = "sqlx" name = "sqlx"
version = "0.4.0-beta.1" version = "0.4.1"
source = "git+https://github.com/launchbadge/sqlx?branch=master#12b4250454b13fa2699dee9a4c761154ae60ddb6" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1f8eb788e1733bdbf69a8f97087213ebdebd253d4782c686d3cfd586b0a9453"
dependencies = [ dependencies = [
"sqlx-core", "sqlx-core",
"sqlx-macros", "sqlx-macros",
@ -1687,8 +1633,9 @@ dependencies = [
[[package]] [[package]]
name = "sqlx-core" name = "sqlx-core"
version = "0.4.0-beta.1" version = "0.4.0"
source = "git+https://github.com/launchbadge/sqlx?branch=master#12b4250454b13fa2699dee9a4c761154ae60ddb6" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e647268dc1239dd9db2d3103fefd61151971a2214882cff9efea6f60cf50840"
dependencies = [ dependencies = [
"ahash 0.5.8", "ahash 0.5.8",
"atoi", "atoi",
@ -1718,6 +1665,7 @@ dependencies = [
"parking_lot", "parking_lot",
"percent-encoding", "percent-encoding",
"rand 0.7.3", "rand 0.7.3",
"rustls",
"serde", "serde",
"serde_json", "serde_json",
"sha-1 0.9.1", "sha-1 0.9.1",
@ -1729,13 +1677,16 @@ dependencies = [
"thiserror", "thiserror",
"url", "url",
"uuid 0.8.1", "uuid 0.8.1",
"webpki",
"webpki-roots",
"whoami", "whoami",
] ]
[[package]] [[package]]
name = "sqlx-macros" name = "sqlx-macros"
version = "0.4.0-beta.1" version = "0.4.0"
source = "git+https://github.com/launchbadge/sqlx?branch=master#12b4250454b13fa2699dee9a4c761154ae60ddb6" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7acd32cba35531345f8a94a038874baf00efd0b701c913f5b00d2870b474b64"
dependencies = [ dependencies = [
"dotenv", "dotenv",
"either", "either",
@ -1755,13 +1706,13 @@ dependencies = [
[[package]] [[package]]
name = "sqlx-rt" name = "sqlx-rt"
version = "0.1.1" version = "0.2.0"
source = "git+https://github.com/launchbadge/sqlx?branch=master#12b4250454b13fa2699dee9a4c761154ae60ddb6" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63fc5454c9dd7aaea3a0eeeb65ca40d06d0d8e7413a8184f7c3a3ffa5056190b"
dependencies = [ dependencies = [
"native-tls",
"once_cell", "once_cell",
"tokio", "tokio",
"tokio-native-tls", "tokio-rustls",
] ]
[[package]] [[package]]
@ -1883,13 +1834,15 @@ dependencies = [
] ]
[[package]] [[package]]
name = "tokio-native-tls" name = "tokio-rustls"
version = "0.1.0" version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd608593a919a8e05a7d1fc6df885e40f6a88d3a70a3a7eff23ff27964eda069" checksum = "e12831b255bcfa39dc0436b01e19fea231a37db570686c06ee72c423479f889a"
dependencies = [ dependencies = [
"native-tls", "futures-core",
"rustls",
"tokio", "tokio",
"webpki",
] ]
[[package]] [[package]]
@ -1938,10 +1891,21 @@ dependencies = [
] ]
[[package]] [[package]]
name = "tracing-attributes" name = "tracing-appender"
version = "0.1.9" version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0693bf8d6f2bf22c690fc61a9d21ac69efdbb894a17ed596b9af0f01e64b84b" checksum = "7aa52d56cc0d79ab604e8a022a1cebc4de33cf09dc9933c94353bea2e00d6e88"
dependencies = [
"chrono",
"crossbeam-channel",
"tracing-subscriber",
]
[[package]]
name = "tracing-attributes"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80e0ccfc3378da0cce270c946b676a376943f5cd16aeba64568e7939806f4ada"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -1980,9 +1944,9 @@ dependencies = [
[[package]] [[package]]
name = "tracing-serde" name = "tracing-serde"
version = "0.1.1" version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6ccba2f8f16e0ed268fc765d9b7ff22e965e7185d32f8f1ec8294fe17d86e79" checksum = "fb65ea441fbb84f9f6748fd496cf7f63ec9af5bca94dd86456978d055e8eb28b"
dependencies = [ dependencies = [
"serde", "serde",
"tracing-core", "tracing-core",
@ -1990,9 +1954,9 @@ dependencies = [
[[package]] [[package]]
name = "tracing-subscriber" name = "tracing-subscriber"
version = "0.2.10" version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7b33f8b2ef2ab0c3778c12646d9c42a24f7772bee4cdafc72199644a9f58fdc" checksum = "abd165311cc4d7a555ad11cc77a37756df836182db0d81aac908c8184c584f40"
dependencies = [ dependencies = [
"ansi_term", "ansi_term",
"chrono", "chrono",
@ -2003,6 +1967,7 @@ dependencies = [
"serde_json", "serde_json",
"sharded-slab", "sharded-slab",
"smallvec", "smallvec",
"thread_local",
"tracing-core", "tracing-core",
"tracing-log", "tracing-log",
"tracing-serde", "tracing-serde",
@ -2087,6 +2052,12 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564"
[[package]]
name = "untrusted"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
[[package]] [[package]]
name = "url" name = "url"
version = "2.1.1" version = "2.1.1"
@ -2129,12 +2100,6 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "vcpkg"
version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6454029bf181f092ad1b853286f23e2c507d8e8194d01d92da4a55c274a5508c"
[[package]] [[package]]
name = "version_check" name = "version_check"
version = "0.9.2" version = "0.9.2"
@ -2173,6 +2138,7 @@ dependencies = [
"serde_json", "serde_json",
"serde_urlencoded", "serde_urlencoded",
"tokio", "tokio",
"tokio-rustls",
"tokio-tungstenite", "tokio-tungstenite",
"tower-service", "tower-service",
"tracing", "tracing",
@ -2186,6 +2152,89 @@ version = "0.9.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
[[package]]
name = "wasm-bindgen"
version = "0.2.68"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ac64ead5ea5f05873d7c12b545865ca2b8d28adfc50a49b84770a3a97265d42"
dependencies = [
"cfg-if 0.1.10",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.68"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f22b422e2a757c35a73774860af8e112bff612ce6cb604224e8e47641a9e4f68"
dependencies = [
"bumpalo",
"lazy_static",
"log",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.68"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b13312a745c08c469f0b292dd2fcd6411dba5f7160f593da6ef69b64e407038"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.68"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f249f06ef7ee334cc3b8ff031bfc11ec99d00f34d86da7498396dc1e3b1498fe"
dependencies = [
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.68"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d649a3145108d7d3fbcde896a468d1bd636791823c9921135218ad89be08307"
[[package]]
name = "web-sys"
version = "0.3.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4bf6ef87ad7ae8008e15a355ce696bed26012b7caa21605188cfd8214ab51e2d"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "webpki"
version = "0.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab146130f5f790d45f82aeeb09e55a256573373ec64409fc19a6fb82fb1032ae"
dependencies = [
"ring",
"untrusted",
]
[[package]]
name = "webpki-roots"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f20dea7535251981a9670857150d571846545088359b28e4951d350bdaf179f"
dependencies = [
"webpki",
]
[[package]] [[package]]
name = "whoami" name = "whoami"
version = "0.9.0" version = "0.9.0"

View File

@ -18,8 +18,8 @@ listenfd = "0.3"
mime = "0.3" mime = "0.3"
openssl-probe = "0.1" openssl-probe = "0.1"
tokio = { version = "0.2", features = ["macros", "rt-threaded", "sync"] } tokio = { version = "0.2", features = ["macros", "rt-threaded", "sync"] }
sqlx = { git = "https://github.com/launchbadge/sqlx", branch = "master", default-features = false, features = [ "runtime-tokio", "macros", "postgres", "chrono", "uuid", "ipnetwork", "json", "migrate", "offline" ] } sqlx = { version = "0.4.1", default-features = false, features = [ "runtime-tokio-rustls", "macros", "postgres", "chrono", "uuid", "ipnetwork", "json", "migrate", "offline" ] }
warp = { version = "0.2", features = ["compression"] } warp = { version = "0.2", features = ["compression", "tls"] }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
uuid = { version = "0.8", features = ["serde", "v4"] } uuid = { version = "0.8", features = ["serde", "v4"] }
@ -28,7 +28,11 @@ url = "2.1"
async-trait = "0.1" async-trait = "0.1"
seahash = "4.0" seahash = "4.0"
tracing = "0.1" tracing = "0.1"
tracing-appender = "0.1"
tracing-subscriber = "0.2" tracing-subscriber = "0.2"
tracing-futures = "0.2" tracing-futures = "0.2"
lru = "0.5" lru = "0.5"
http = "0.2" http = "0.2"
[profile.release]
lto = true

101
README.md
View File

@ -16,16 +16,31 @@ are (all prefixed under `/v1`, the API version):
- `/merchandise_lists`: Lists of in-game Forms that are in the merchant chest - `/merchandise_lists`: Lists of in-game Forms that are in the merchant chest
of individual shops. When a user visits a shop, these forms are loaded of individual shops. When a user visits a shop, these forms are loaded
onto the shop's shelves and are purchasable. onto the shop's shelves and are purchasable.
- `/transactions`: Allows posting a new buy or sell between an owner and a
shop's merchandise.
Bazaar Realm was designed to allow users to change the API they are using the Bazaar Realm was designed to allow users to change the API they are using the
mod under, if they wish. The API can run on a small server with minimal mod under, if they wish. The API can run on a small server with minimal
resources, which should be suitable for a small group of friends to share resources, which should be suitable for a group of friends to share shops
shops with each other. with each other.
It uses the [`warp`](https://crates.io/crates/warp) web server framework and It uses the [`warp`](https://crates.io/crates/warp) web server framework and
[`sqlx`](https://crates.io/crates/sqlx) for database queries to a [PostgreSQL [`sqlx`](https://crates.io/crates/sqlx) for database queries to a [PostgreSQL
database](https://www.postgresql.org). database](https://www.postgresql.org).
The API was designed with performance as a high priority. When it serves a
response, it also caches that response so future queries for the same data
can be returned in less than 1ms. To reduce data sent over the network,
clients can use the
[ETag](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag)
headers to indicate to the server what version of the data they have cached
so the server can send a 304 response with no data if the resource hasn't
changed since the client last requested. Using the
[Accept](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept)
header, clients can also opt for the more space-efficient and faster to
deserialize [bincode](https://github.com/servo/bincode) format instead of the
JSON default.
Related projects: Related projects:
- [`BazaarRealmClient`](https://github.com/thallada/BazaarRealmClient): DLL that - [`BazaarRealmClient`](https://github.com/thallada/BazaarRealmClient): DLL that
@ -40,6 +55,29 @@ Related projects:
The easiest way to get the server up and running is using Docker. The easiest way to get the server up and running is using Docker.
1. Download and install [Docker Desktop](https://www.docker.com/get-started)
2. In PowerShell, cmd.exe, or a terminal run `docker pull postgres:alpine` then `docker pull thallada/bazaarrealm:latest`
3. Run (replacing `<password>` with a secure generated password):
```
docker run -d --name postgres --network=bazaarrealm --network-alias=db --env POSTGRES_DB=bazaarrealm --env POSTGRES_USER=bazaarrealm --env POSTGRES_PASSWORD=<password> postgres:alpine
```
4. Run (replacing `<password>` with what you generated in previous step):
```
docker run -d --name bazaarrealm -p 3030:3030 --network=bazaarrealm --network-alias=api --env DATABASE_URL=postgresql://bazaarrealm:<password>@db/bazaarrealm --env HOST=http://localhost:3030 thallada/bazaarrealm:latest
```
5. The server should now be available at `http://localhost:3030`.
## Docker-Compose Setup
An alternative way to set up the API, is to use `docker-compose` which can
orchestrate setting up the database and web server containers for you. This
method is more useful if you would like to make changes to the API code and
test them out.
1. Download and install [Docker Desktop](https://www.docker.com/get-started) 1. Download and install [Docker Desktop](https://www.docker.com/get-started)
2. Git clone this repo into a folder of your choosing: `git clone https://github.com/thallada/BazaarRealmAPI.git` 2. Git clone this repo into a folder of your choosing: `git clone https://github.com/thallada/BazaarRealmAPI.git`
3. Create a new file `.env.docker` in the checked out `bazaar_realm_api` 3. Create a new file `.env.docker` in the checked out `bazaar_realm_api`
@ -50,6 +88,7 @@ The easiest way to get the server up and running is using Docker.
DATABASE_URL="postgresql://bazaarrealm:<password>@db/bazaarrealm" DATABASE_URL="postgresql://bazaarrealm:<password>@db/bazaarrealm"
RUST_LOG="bazaar_realm_api=debug,warp=info" RUST_LOG="bazaar_realm_api=debug,warp=info"
HOST="http://localhost:3030" HOST="http://localhost:3030"
PORT=3030
POSTGRES_DB=bazaarrealm POSTGRES_DB=bazaarrealm
POSTGRES_USER=bazaarrealm POSTGRES_USER=bazaarrealm
POSTGRES_PASSWORD=<password> POSTGRES_PASSWORD=<password>
@ -92,16 +131,36 @@ postgres=# CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
DATABASE_URL=postgresql://bazaarrealm:<password>@localhost/bazaarrealm DATABASE_URL=postgresql://bazaarrealm:<password>@localhost/bazaarrealm
RUST_LOG="bazaar_realm_api=debug" RUST_LOG="bazaar_realm_api=debug"
HOST="http://localhost:3030" HOST="http://localhost:3030"
PORT=3030
``` ```
4. Install 4. Install
[`sqlx_cli`](https://github.com/launchbadge/sqlx/tree/master/sqlx-cli) with [`sqlx_cli`](https://github.com/launchbadge/sqlx/tree/master/sqlx-cli) with
`cargo install --version=0.1.0-beta.1 sqlx-cli --no-default-features --features postgres` `cargo install --version=0.1.0-beta.1 sqlx-cli --no-default-features --features postgres`
5. `cd db` to enter the `db` sub-directory of this repo. 5. Run `sqlx migrate --source db/migrations run` which will run all the database
6. Run `sqlx migrate run` which will run all the database migrations. migrations.
7. `cd ..` to return to the top-level directory of this repo. 6. Run `./devserver.sh` to run the dev server (by default it listens at
8. Run `./devserver.sh` to run the dev server (by default it listens at `127.0.0.1:3030`). Note that this runs the server in debug mode and shouldn't
`127.0.0.1:3030`). be used to serve requests from the mod. You can build the release version of
the server with `cargo build --release`.
## TLS setup
If you would like to access the server over HTTPS, you can use [Let's
Encrypt](https://letsencrypt.org/) to generate a SSL certificate and key and
provide it to the API. Once you use [certbot](https://certbot.eff.org/) to
generate the certificate and key for your domain in
`/etc/letsencrypt/live/<domain>/`, run the api server with:
```
docker run -d --name bazaarrealm --network=host --env DATABASE_URL=postgresql://bazaarrealm:<password>@localhost/bazaarrealm --env PORT=443 --HOST=https://<domain> --env TLS_CERT=/etc/letsencrypt/live/<domain>/fullchain.pem --env TLS_KEY=/etc/letsencrypt/live/<domain>/privkey.pem -v /etc/letsencrypt/:/etc/letsencrypt/ thallada/bazaarrealm:latest
```
This command assumes that you are on Linux and you have a running postgres
database already set up outside of docker. See Manual Development Setup for
database setup instructions.
The server should be accessible at your domain: `https://<domain>`.
## Testing Data ## Testing Data
@ -124,6 +183,20 @@ http GET "http://localhost:3030/v1/interior_ref_lists"
http GET "http://localhost:3030/v1/merchandise_lists" http GET "http://localhost:3030/v1/merchandise_lists"
``` ```
## Database Migrations
Migrations are handled by `sqlx`. When the server initially starts, it will
connect to the database and check if there are any migrations in
`db/migrations` that have not yet been applied. It will apply any at that
time and then continue starting the server.
A new migration can be created by running: `sqlx migrate add <name>`.
To allow the docker container for the API to get built in CI without a
database, the `sqlx-data.json` file needs to be re-generated every time the
database schema changes or any query is updated. It can be generated with `cargo
sqlx prepare`.
## Authentication ## Authentication
I don't want to require users of Bazaar Realm to have to remember a password, I don't want to require users of Bazaar Realm to have to remember a password,
@ -133,14 +206,6 @@ unique UUID identifier instead. This is the api key that the
The api key is stored in the save game files for the player character and is The api key is stored in the save game files for the player character and is
required to be sent with any API request that modifies data. required to be sent with any API request that modifies data.
Yes, it's not most secure solution, but I'm not convinced security is a huge Yes, it's not the most secure solution, but I'm not convinced security is a
concern here. As long as users don't share their API key or the save game huge concern here. As long as users don't share their API key or the save
files that contain it, their data should be secure. game files that contain it, their data should be secure.
## Todo
- Add update endpoints.
- Add endpoints for the other models.
- Make self-contained docker container that can run the app without any setup.
- Add rate-limiting per IP address. The `tower` crate has a service that might
be useful for this.

View File

@ -13,6 +13,11 @@ CREATE TABLE "shops" (
"name" VARCHAR(255) NOT NULL, "name" VARCHAR(255) NOT NULL,
"owner_id" INTEGER REFERENCES "owners"(id) NOT NULL, "owner_id" INTEGER REFERENCES "owners"(id) NOT NULL,
"description" TEXT, "description" TEXT,
"gold" INTEGER NOT NULL DEFAULT 0
CONSTRAINT "shop_gold_gt_zero" CHECK (gold >= 0),
"shop_type" VARCHAR(255) NOT NULL DEFAULT 'general_store',
"vendor_keywords" TEXT[] NOT NULL DEFAULT '{"VendorItemKey", "VendorNoSale"}',
"vendor_keywords_exclude" BOOLEAN NOT NULL DEFAULT true,
"created_at" timestamp(3) NOT NULL, "created_at" timestamp(3) NOT NULL,
"updated_at" timestamp(3) NOT NULL "updated_at" timestamp(3) NOT NULL
); );
@ -22,6 +27,7 @@ CREATE TABLE "interior_ref_lists" (
"shop_id" INTEGER REFERENCES "shops"(id) NOT NULL UNIQUE, "shop_id" INTEGER REFERENCES "shops"(id) NOT NULL UNIQUE,
"owner_id" INTEGER REFERENCES "owners"(id) NOT NULL, "owner_id" INTEGER REFERENCES "owners"(id) NOT NULL,
"ref_list" jsonb NOT NULL, "ref_list" jsonb NOT NULL,
"shelves" jsonb NOT NULL,
"created_at" timestamp(3) NOT NULL, "created_at" timestamp(3) NOT NULL,
"updated_at" timestamp(3) NOT NULL "updated_at" timestamp(3) NOT NULL
); );
@ -35,6 +41,14 @@ CREATE TABLE "merchandise_lists" (
"updated_at" timestamp(3) NOT NULL "updated_at" timestamp(3) NOT NULL
); );
CREATE INDEX "merchandise_lists_mod_name_and_local_form_id" ON "merchandise_lists" USING GIN (form_list jsonb_path_ops); CREATE INDEX "merchandise_lists_mod_name_and_local_form_id" ON "merchandise_lists" USING GIN (form_list jsonb_path_ops);
CREATE TABLE "vendors" (
"id" SERIAL PRIMARY KEY NOT NULL,
"shop_id" INTEGER REFERENCES "shops"(id) NOT NULL UNIQUE,
"owner_id" INTEGER REFERENCES "owners"(id) NOT NULL,
"name" VARCHAR(255) NOT NULL,
"body_preset" INTEGER NOT NULL
);
CREATE UNIQUE INDEX "vendors_unique_name_and_owner_id" ON "vendors" ("name", "owner_id", "shop_id");
CREATE TABLE "transactions" ( CREATE TABLE "transactions" (
"id" SERIAL PRIMARY KEY NOT NULL, "id" SERIAL PRIMARY KEY NOT NULL,
"shop_id" INTEGER REFERENCES "shops"(id) NOT NULL, "shop_id" INTEGER REFERENCES "shops"(id) NOT NULL,
@ -48,6 +62,7 @@ CREATE TABLE "transactions" (
"is_sell" BOOLEAN NOT NULL, "is_sell" BOOLEAN NOT NULL,
"quantity" INTEGER NOT NULL, "quantity" INTEGER NOT NULL,
"amount" INTEGER NOT NULL, "amount" INTEGER NOT NULL,
"keywords" TEXT[] NOT NULL DEFAULT '{}',
"created_at" timestamp(3) NOT NULL, "created_at" timestamp(3) NOT NULL,
"updated_at" timestamp(3) NOT NULL "updated_at" timestamp(3) NOT NULL
); );

View File

@ -3,4 +3,5 @@ DROP TABLE shops CASCADE;
DROP TABLE interior_ref_lists CASCADE; DROP TABLE interior_ref_lists CASCADE;
DROP TABLE merchandise_lists CASCADE; DROP TABLE merchandise_lists CASCADE;
DROP TABLE transactions CASCADE; DROP TABLE transactions CASCADE;
DROP TABLE refinery_schema_history CASCADE; DROP TABLE vendors CASCADE;
DROP TABLE _sqlx_migrations CASCADE;

File diff suppressed because it is too large Load Diff

View File

@ -1,17 +1,19 @@
use anyhow::Result; use anyhow::Result;
use http::StatusCode; use http::StatusCode;
use hyper::body::Bytes;
use mime::Mime; use mime::Mime;
use uuid::Uuid; use uuid::Uuid;
use warp::reply::{with_header, with_status}; use warp::reply::{with_header, with_status};
use warp::{Rejection, Reply}; use warp::{Rejection, Reply};
use crate::caches::CACHES; use crate::caches::{CachedResponse, CACHES};
use crate::models::{InteriorRefList, ListParams, PostedInteriorRefList, UnsavedInteriorRefList}; use crate::models::{InteriorRefList, ListParams, PostedInteriorRefList};
use crate::problem::reject_anyhow; use crate::problem::reject_anyhow;
use crate::Environment; use crate::Environment;
use super::{ use super::{
authenticate, check_etag, AcceptHeader, Bincode, ContentType, DataReply, ETagReply, Json, authenticate, check_etag, AcceptHeader, Bincode, ContentType, DataReply, DeserializedBody,
ETagReply, Json, TypedCache,
}; };
pub async fn get( pub async fn get(
@ -20,12 +22,14 @@ pub async fn get(
accept: Option<AcceptHeader>, accept: Option<AcceptHeader>,
env: Environment, env: Environment,
) -> Result<impl Reply, Rejection> { ) -> Result<impl Reply, Rejection> {
let (content_type, cache) = match accept { let TypedCache {
Some(accept) if accept.accepts_bincode() => { content_type,
(ContentType::Bincode, &CACHES.interior_ref_list_bin) cache,
} } = TypedCache::<i32, CachedResponse>::pick_cache(
_ => (ContentType::Json, &CACHES.interior_ref_list), accept,
}; &CACHES.interior_ref_list_bin,
&CACHES.interior_ref_list,
);
let response = cache let response = cache
.get_response(id, || async { .get_response(id, || async {
let interior_ref_list = InteriorRefList::get(&env.db, id).await?; let interior_ref_list = InteriorRefList::get(&env.db, id).await?;
@ -50,13 +54,14 @@ pub async fn get_by_shop_id(
accept: Option<AcceptHeader>, accept: Option<AcceptHeader>,
env: Environment, env: Environment,
) -> Result<impl Reply, Rejection> { ) -> Result<impl Reply, Rejection> {
let (content_type, cache) = match accept { let TypedCache {
Some(accept) if accept.accepts_bincode() => ( content_type,
ContentType::Bincode, cache,
&CACHES.interior_ref_list_by_shop_id_bin, } = TypedCache::<i32, CachedResponse>::pick_cache(
), accept,
_ => (ContentType::Json, &CACHES.interior_ref_list_by_shop_id), &CACHES.interior_ref_list_by_shop_id_bin,
}; &CACHES.interior_ref_list_by_shop_id,
);
let response = cache let response = cache
.get_response(shop_id, || async { .get_response(shop_id, || async {
let interior_ref_list = InteriorRefList::get_by_shop_id(&env.db, shop_id).await?; let interior_ref_list = InteriorRefList::get_by_shop_id(&env.db, shop_id).await?;
@ -81,12 +86,14 @@ pub async fn list(
accept: Option<AcceptHeader>, accept: Option<AcceptHeader>,
env: Environment, env: Environment,
) -> Result<impl Reply, Rejection> { ) -> Result<impl Reply, Rejection> {
let (content_type, cache) = match accept { let TypedCache {
Some(accept) if accept.accepts_bincode() => { content_type,
(ContentType::Bincode, &CACHES.list_interior_ref_lists_bin) cache,
} } = TypedCache::<ListParams, CachedResponse>::pick_cache(
_ => (ContentType::Json, &CACHES.list_interior_ref_lists), accept,
}; &CACHES.list_interior_ref_lists_bin,
&CACHES.list_interior_ref_lists,
);
let response = cache let response = cache
.get_response(list_params.clone(), || async { .get_response(list_params.clone(), || async {
let interior_ref_lists = InteriorRefList::list(&env.db, &list_params).await?; let interior_ref_lists = InteriorRefList::list(&env.db, &list_params).await?;
@ -107,24 +114,19 @@ pub async fn list(
} }
pub async fn create( pub async fn create(
interior_ref_list: PostedInteriorRefList, bytes: Bytes,
api_key: Option<Uuid>, api_key: Option<Uuid>,
content_type: Option<Mime>, content_type: Option<Mime>,
env: Environment, env: Environment,
) -> Result<impl Reply, Rejection> { ) -> Result<impl Reply, Rejection> {
let content_type = match content_type { let DeserializedBody {
Some(content_type) if content_type == mime::APPLICATION_OCTET_STREAM => { body: mut interior_ref_list,
ContentType::Bincode content_type,
} } = DeserializedBody::<PostedInteriorRefList>::from_bytes(bytes, content_type)
_ => ContentType::Json, .map_err(reject_anyhow)?;
};
let owner_id = authenticate(&env, api_key).await.map_err(reject_anyhow)?; let owner_id = authenticate(&env, api_key).await.map_err(reject_anyhow)?;
let unsaved_interior_ref_list = UnsavedInteriorRefList { interior_ref_list.owner_id = Some(owner_id);
owner_id, let saved_interior_ref_list = InteriorRefList::create(interior_ref_list, &env.db)
shop_id: interior_ref_list.shop_id,
ref_list: interior_ref_list.ref_list,
};
let saved_interior_ref_list = InteriorRefList::create(unsaved_interior_ref_list, &env.db)
.await .await
.map_err(reject_anyhow)?; .map_err(reject_anyhow)?;
let url = saved_interior_ref_list let url = saved_interior_ref_list
@ -159,17 +161,16 @@ pub async fn create(
pub async fn update( pub async fn update(
id: i32, id: i32,
interior_ref_list: PostedInteriorRefList, bytes: Bytes,
api_key: Option<Uuid>, api_key: Option<Uuid>,
content_type: Option<Mime>, content_type: Option<Mime>,
env: Environment, env: Environment,
) -> Result<impl Reply, Rejection> { ) -> Result<impl Reply, Rejection> {
let content_type = match content_type { let DeserializedBody {
Some(content_type) if content_type == mime::APPLICATION_OCTET_STREAM => { body: interior_ref_list,
ContentType::Bincode content_type,
} } = DeserializedBody::<PostedInteriorRefList>::from_bytes(bytes, content_type)
_ => ContentType::Json, .map_err(reject_anyhow)?;
};
let owner_id = authenticate(&env, api_key).await.map_err(reject_anyhow)?; let owner_id = authenticate(&env, api_key).await.map_err(reject_anyhow)?;
let updated_interior_ref_list = let updated_interior_ref_list =
InteriorRefList::update(interior_ref_list, &env.db, owner_id, id) InteriorRefList::update(interior_ref_list, &env.db, owner_id, id)
@ -209,17 +210,16 @@ pub async fn update(
pub async fn update_by_shop_id( pub async fn update_by_shop_id(
shop_id: i32, shop_id: i32,
interior_ref_list: PostedInteriorRefList, bytes: Bytes,
api_key: Option<Uuid>, api_key: Option<Uuid>,
content_type: Option<Mime>, content_type: Option<Mime>,
env: Environment, env: Environment,
) -> Result<impl Reply, Rejection> { ) -> Result<impl Reply, Rejection> {
let content_type = match content_type { let DeserializedBody {
Some(content_type) if content_type == mime::APPLICATION_OCTET_STREAM => { body: interior_ref_list,
ContentType::Bincode content_type,
} } = DeserializedBody::<PostedInteriorRefList>::from_bytes(bytes, content_type)
_ => ContentType::Json, .map_err(reject_anyhow)?;
};
let owner_id = authenticate(&env, api_key).await.map_err(reject_anyhow)?; let owner_id = authenticate(&env, api_key).await.map_err(reject_anyhow)?;
let updated_interior_ref_list = let updated_interior_ref_list =
InteriorRefList::update_by_shop_id(interior_ref_list, &env.db, owner_id, shop_id) InteriorRefList::update_by_shop_id(interior_ref_list, &env.db, owner_id, shop_id)

View File

@ -1,17 +1,19 @@
use anyhow::Result; use anyhow::Result;
use http::StatusCode; use http::StatusCode;
use hyper::body::Bytes;
use mime::Mime; use mime::Mime;
use uuid::Uuid; use uuid::Uuid;
use warp::reply::{with_header, with_status}; use warp::reply::{with_header, with_status};
use warp::{Rejection, Reply}; use warp::{Rejection, Reply};
use crate::caches::CACHES; use crate::caches::{CachedResponse, CACHES};
use crate::models::{ListParams, MerchandiseList, PostedMerchandiseList, UnsavedMerchandiseList}; use crate::models::{ListParams, MerchandiseList, PostedMerchandiseList};
use crate::problem::reject_anyhow; use crate::problem::reject_anyhow;
use crate::Environment; use crate::Environment;
use super::{ use super::{
authenticate, check_etag, AcceptHeader, Bincode, ContentType, DataReply, ETagReply, Json, authenticate, check_etag, AcceptHeader, Bincode, ContentType, DataReply, DeserializedBody,
ETagReply, Json, TypedCache,
}; };
pub async fn get( pub async fn get(
@ -20,12 +22,14 @@ pub async fn get(
accept: Option<AcceptHeader>, accept: Option<AcceptHeader>,
env: Environment, env: Environment,
) -> Result<impl Reply, Rejection> { ) -> Result<impl Reply, Rejection> {
let (content_type, cache) = match accept { let TypedCache {
Some(accept) if accept.accepts_bincode() => { content_type,
(ContentType::Bincode, &CACHES.merchandise_list_bin) cache,
} } = TypedCache::<i32, CachedResponse>::pick_cache(
_ => (ContentType::Json, &CACHES.merchandise_list), accept,
}; &CACHES.merchandise_list_bin,
&CACHES.merchandise_list,
);
let response = cache let response = cache
.get_response(id, || async { .get_response(id, || async {
let merchandise_list = MerchandiseList::get(&env.db, id).await?; let merchandise_list = MerchandiseList::get(&env.db, id).await?;
@ -50,12 +54,14 @@ pub async fn get_by_shop_id(
accept: Option<AcceptHeader>, accept: Option<AcceptHeader>,
env: Environment, env: Environment,
) -> Result<impl Reply, Rejection> { ) -> Result<impl Reply, Rejection> {
let (content_type, cache) = match accept { let TypedCache {
Some(accept) if accept.accepts_bincode() => { content_type,
(ContentType::Bincode, &CACHES.merchandise_list_bin) cache,
} } = TypedCache::<i32, CachedResponse>::pick_cache(
_ => (ContentType::Json, &CACHES.merchandise_list), accept,
}; &CACHES.merchandise_list_by_shop_id_bin,
&CACHES.merchandise_list_by_shop_id,
);
let response = cache let response = cache
.get_response(shop_id, || async { .get_response(shop_id, || async {
let merchandise_list = MerchandiseList::get_by_shop_id(&env.db, shop_id).await?; let merchandise_list = MerchandiseList::get_by_shop_id(&env.db, shop_id).await?;
@ -80,12 +86,14 @@ pub async fn list(
accept: Option<AcceptHeader>, accept: Option<AcceptHeader>,
env: Environment, env: Environment,
) -> Result<impl Reply, Rejection> { ) -> Result<impl Reply, Rejection> {
let (content_type, cache) = match accept { let TypedCache {
Some(accept) if accept.accepts_bincode() => { content_type,
(ContentType::Bincode, &CACHES.list_merchandise_lists_bin) cache,
} } = TypedCache::<ListParams, CachedResponse>::pick_cache(
_ => (ContentType::Json, &CACHES.list_merchandise_lists), accept,
}; &CACHES.list_merchandise_lists_bin,
&CACHES.list_merchandise_lists,
);
let response = cache let response = cache
.get_response(list_params.clone(), || async { .get_response(list_params.clone(), || async {
let merchandise_lists = MerchandiseList::list(&env.db, &list_params).await?; let merchandise_lists = MerchandiseList::list(&env.db, &list_params).await?;
@ -105,24 +113,19 @@ pub async fn list(
} }
pub async fn create( pub async fn create(
merchandise_list: PostedMerchandiseList, bytes: Bytes,
api_key: Option<Uuid>, api_key: Option<Uuid>,
content_type: Option<Mime>, content_type: Option<Mime>,
env: Environment, env: Environment,
) -> Result<impl Reply, Rejection> { ) -> Result<impl Reply, Rejection> {
let content_type = match content_type { let DeserializedBody {
Some(content_type) if content_type == mime::APPLICATION_OCTET_STREAM => { body: mut merchandise_list,
ContentType::Bincode content_type,
} } = DeserializedBody::<PostedMerchandiseList>::from_bytes(bytes, content_type)
_ => ContentType::Json, .map_err(reject_anyhow)?;
};
let owner_id = authenticate(&env, api_key).await.map_err(reject_anyhow)?; let owner_id = authenticate(&env, api_key).await.map_err(reject_anyhow)?;
let unsaved_merchandise_list = UnsavedMerchandiseList { merchandise_list.owner_id = Some(owner_id);
owner_id, let saved_merchandise_list = MerchandiseList::create(merchandise_list, &env.db)
shop_id: merchandise_list.shop_id,
form_list: merchandise_list.form_list,
};
let saved_merchandise_list = MerchandiseList::create(unsaved_merchandise_list, &env.db)
.await .await
.map_err(reject_anyhow)?; .map_err(reject_anyhow)?;
let url = saved_merchandise_list let url = saved_merchandise_list
@ -156,17 +159,16 @@ pub async fn create(
pub async fn update( pub async fn update(
id: i32, id: i32,
merchandise_list: PostedMerchandiseList, bytes: Bytes,
api_key: Option<Uuid>, api_key: Option<Uuid>,
content_type: Option<Mime>, content_type: Option<Mime>,
env: Environment, env: Environment,
) -> Result<impl Reply, Rejection> { ) -> Result<impl Reply, Rejection> {
let content_type = match content_type { let DeserializedBody {
Some(content_type) if content_type == mime::APPLICATION_OCTET_STREAM => { body: merchandise_list,
ContentType::Bincode content_type,
} } = DeserializedBody::<PostedMerchandiseList>::from_bytes(bytes, content_type)
_ => ContentType::Json, .map_err(reject_anyhow)?;
};
let owner_id = authenticate(&env, api_key).await.map_err(reject_anyhow)?; let owner_id = authenticate(&env, api_key).await.map_err(reject_anyhow)?;
let updated_merchandise_list = MerchandiseList::update(merchandise_list, &env.db, owner_id, id) let updated_merchandise_list = MerchandiseList::update(merchandise_list, &env.db, owner_id, id)
.await .await
@ -205,17 +207,16 @@ pub async fn update(
pub async fn update_by_shop_id( pub async fn update_by_shop_id(
shop_id: i32, shop_id: i32,
merchandise_list: PostedMerchandiseList, bytes: Bytes,
api_key: Option<Uuid>, api_key: Option<Uuid>,
content_type: Option<Mime>, content_type: Option<Mime>,
env: Environment, env: Environment,
) -> Result<impl Reply, Rejection> { ) -> Result<impl Reply, Rejection> {
let content_type = match content_type { let DeserializedBody {
Some(content_type) if content_type == mime::APPLICATION_OCTET_STREAM => { body: merchandise_list,
ContentType::Bincode content_type,
} } = DeserializedBody::<PostedMerchandiseList>::from_bytes(bytes, content_type)
_ => ContentType::Json, .map_err(reject_anyhow)?;
};
let owner_id = authenticate(&env, api_key).await.map_err(reject_anyhow)?; let owner_id = authenticate(&env, api_key).await.map_err(reject_anyhow)?;
let updated_merchandise_list = let updated_merchandise_list =
MerchandiseList::update_by_shop_id(merchandise_list, &env.db, owner_id, shop_id) MerchandiseList::update_by_shop_id(merchandise_list, &env.db, owner_id, shop_id)

View File

@ -1,14 +1,17 @@
use std::fmt::Debug;
use std::hash::Hash;
use std::marker::PhantomData; use std::marker::PhantomData;
use std::str::FromStr; use std::str::FromStr;
use anyhow::{anyhow, Error, Result}; use anyhow::{anyhow, Error, Result};
use http::header::{HeaderValue, CONTENT_TYPE, ETAG}; use http::header::{HeaderValue, CONTENT_TYPE, ETAG, SERVER};
use http::StatusCode; use http::StatusCode;
use http_api_problem::HttpApiProblem; use http_api_problem::HttpApiProblem;
use hyper::body::Bytes;
use mime::{FromStrError, Mime}; use mime::{FromStrError, Mime};
use seahash::hash; use seahash::hash;
use serde::Serialize; use serde::{de::DeserializeOwned, Serialize};
use tracing::{error, instrument, warn}; use tracing::{debug, error, instrument, warn};
use uuid::Uuid; use uuid::Uuid;
use warp::reply::Response; use warp::reply::Response;
use warp::Reply; use warp::Reply;
@ -19,10 +22,12 @@ pub mod owner;
pub mod shop; pub mod shop;
pub mod transaction; pub mod transaction;
use super::caches::{CachedResponse, CACHES}; use super::caches::{Cache, CachedResponse, CACHES};
use super::problem::{unauthorized_no_api_key, unauthorized_no_owner}; use super::problem::{unauthorized_no_api_key, unauthorized_no_owner};
use super::Environment; use super::Environment;
pub static SERVER_STRING: &str = "BazaarRealmAPI/0.1.0";
#[instrument(level = "debug", skip(env, api_key))] #[instrument(level = "debug", skip(env, api_key))]
pub async fn authenticate(env: &Environment, api_key: Option<Uuid>) -> Result<i32> { pub async fn authenticate(env: &Environment, api_key: Option<Uuid>) -> Result<i32> {
if let Some(api_key) = api_key { if let Some(api_key) = api_key {
@ -75,6 +80,8 @@ impl Reply for ETagReply<Json> {
let mut res = Response::new(self.body.into()); let mut res = Response::new(self.body.into());
res.headers_mut() res.headers_mut()
.insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); .insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
res.headers_mut()
.insert(SERVER, HeaderValue::from_static(SERVER_STRING));
if let Ok(val) = HeaderValue::from_str(&self.etag) { if let Ok(val) = HeaderValue::from_str(&self.etag) {
res.headers_mut().insert(ETAG, val); res.headers_mut().insert(ETAG, val);
} else { } else {
@ -113,6 +120,8 @@ impl Reply for ETagReply<Bincode> {
CONTENT_TYPE, CONTENT_TYPE,
HeaderValue::from_static("application/octet-stream"), HeaderValue::from_static("application/octet-stream"),
); );
res.headers_mut()
.insert(SERVER, HeaderValue::from_static(SERVER_STRING));
if let Ok(val) = HeaderValue::from_str(&self.etag) { if let Ok(val) = HeaderValue::from_str(&self.etag) {
res.headers_mut().insert(ETAG, val); res.headers_mut().insert(ETAG, val);
} else { } else {
@ -178,3 +187,76 @@ impl AcceptHeader {
self.mimes.contains(&mime::APPLICATION_OCTET_STREAM) self.mimes.contains(&mime::APPLICATION_OCTET_STREAM)
} }
} }
pub struct DeserializedBody<T> {
body: T,
content_type: ContentType,
}
impl<T: DeserializeOwned> DeserializedBody<T> {
pub fn from_bytes(bytes: Bytes, content_type: Option<Mime>) -> Result<Self> {
match content_type {
Some(content_type) if content_type == mime::APPLICATION_OCTET_STREAM => {
debug!(
content_type = ?ContentType::Bincode,
"deserializing body as bincode"
);
Ok(Self {
content_type: ContentType::Bincode,
body: bincode::deserialize(&bytes)?,
})
}
_ => {
debug!(
content_type = ?ContentType::Json,
"deserializing body as json"
);
Ok(Self {
content_type: ContentType::Json,
body: serde_json::from_slice(&bytes)?,
})
}
}
}
}
pub struct TypedCache<'a, K, V>
where
K: Eq + Hash + Debug,
V: Clone,
{
cache: &'a Cache<K, V>,
content_type: ContentType,
}
impl<'a, K, V> TypedCache<'a, K, V>
where
K: Eq + Hash + Debug,
V: Clone,
{
pub fn pick_cache(
accept: Option<AcceptHeader>,
bincode_cache: &'a Cache<K, V>,
json_cache: &'a Cache<K, V>,
) -> Self {
match accept {
Some(accept) if accept.accepts_bincode() => {
debug!(
content_type = ?ContentType::Bincode,
"serializing body as bincode"
);
Self {
content_type: ContentType::Bincode,
cache: bincode_cache,
}
}
_ => {
debug!(content_type = ?ContentType::Json, "serializing body as json");
Self {
content_type: ContentType::Json,
cache: json_cache,
}
}
}
}
}

View File

@ -1,5 +1,6 @@
use anyhow::Result; use anyhow::Result;
use http::StatusCode; use http::StatusCode;
use hyper::body::Bytes;
use ipnetwork::IpNetwork; use ipnetwork::IpNetwork;
use mime::Mime; use mime::Mime;
use std::net::SocketAddr; use std::net::SocketAddr;
@ -7,13 +8,14 @@ use uuid::Uuid;
use warp::reply::{with_header, with_status}; use warp::reply::{with_header, with_status};
use warp::{Rejection, Reply}; use warp::{Rejection, Reply};
use crate::caches::CACHES; use crate::caches::{CachedResponse, CACHES};
use crate::models::{ListParams, Owner, PostedOwner, UnsavedOwner}; use crate::models::{FullPostedOwner, ListParams, Owner, PostedOwner};
use crate::problem::{reject_anyhow, unauthorized_no_api_key}; use crate::problem::{reject_anyhow, unauthorized_no_api_key};
use crate::Environment; use crate::Environment;
use super::{ use super::{
authenticate, check_etag, AcceptHeader, Bincode, ContentType, DataReply, ETagReply, Json, authenticate, check_etag, AcceptHeader, Bincode, ContentType, DataReply, DeserializedBody,
ETagReply, Json, TypedCache,
}; };
pub async fn get( pub async fn get(
@ -22,10 +24,10 @@ pub async fn get(
accept: Option<AcceptHeader>, accept: Option<AcceptHeader>,
env: Environment, env: Environment,
) -> Result<impl Reply, Rejection> { ) -> Result<impl Reply, Rejection> {
let (content_type, cache) = match accept { let TypedCache {
Some(accept) if accept.accepts_bincode() => (ContentType::Bincode, &CACHES.owner_bin), content_type,
_ => (ContentType::Json, &CACHES.owner), cache,
}; } = TypedCache::<i32, CachedResponse>::pick_cache(accept, &CACHES.owner_bin, &CACHES.owner);
let response = cache let response = cache
.get_response(id, || async { .get_response(id, || async {
let owner = Owner::get(&env.db, id).await?; let owner = Owner::get(&env.db, id).await?;
@ -46,10 +48,14 @@ pub async fn list(
accept: Option<AcceptHeader>, accept: Option<AcceptHeader>,
env: Environment, env: Environment,
) -> Result<impl Reply, Rejection> { ) -> Result<impl Reply, Rejection> {
let (content_type, cache) = match accept { let TypedCache {
Some(accept) if accept.accepts_bincode() => (ContentType::Bincode, &CACHES.list_owners_bin), content_type,
_ => (ContentType::Json, &CACHES.list_owners), cache,
}; } = TypedCache::<ListParams, CachedResponse>::pick_cache(
accept,
&CACHES.list_owners_bin,
&CACHES.list_owners,
);
let response = cache let response = cache
.get_response(list_params.clone(), || async { .get_response(list_params.clone(), || async {
let owners = Owner::list(&env.db, &list_params).await?; let owners = Owner::list(&env.db, &list_params).await?;
@ -65,7 +71,7 @@ pub async fn list(
} }
pub async fn create( pub async fn create(
owner: PostedOwner, bytes: Bytes,
remote_addr: Option<SocketAddr>, remote_addr: Option<SocketAddr>,
api_key: Option<Uuid>, api_key: Option<Uuid>,
real_ip: Option<IpNetwork>, real_ip: Option<IpNetwork>,
@ -73,24 +79,21 @@ pub async fn create(
env: Environment, env: Environment,
) -> Result<impl Reply, Rejection> { ) -> Result<impl Reply, Rejection> {
if let Some(api_key) = api_key { if let Some(api_key) = api_key {
let content_type = match content_type { let DeserializedBody {
Some(content_type) if content_type == mime::APPLICATION_OCTET_STREAM => { body: owner,
ContentType::Bincode content_type,
} } = DeserializedBody::<PostedOwner>::from_bytes(bytes, content_type)
_ => ContentType::Json, .map_err(reject_anyhow)?;
}; let owner = FullPostedOwner {
let unsaved_owner = UnsavedOwner { name: owner.name,
mod_version: owner.mod_version,
api_key, api_key,
ip_address: match remote_addr { ip_address: match remote_addr {
Some(addr) => Some(IpNetwork::from(addr.ip())), Some(addr) => Some(IpNetwork::from(addr.ip())),
None => real_ip, None => real_ip,
}, },
name: owner.name,
mod_version: owner.mod_version,
}; };
let saved_owner = Owner::create(unsaved_owner, &env.db) let saved_owner = Owner::create(owner, &env.db).await.map_err(reject_anyhow)?;
.await
.map_err(reject_anyhow)?;
let url = saved_owner.url(&env.api_url).map_err(reject_anyhow)?; let url = saved_owner.url(&env.api_url).map_err(reject_anyhow)?;
let reply: Box<dyn Reply> = match content_type { let reply: Box<dyn Reply> = match content_type {
ContentType::Bincode => Box::new( ContentType::Bincode => Box::new(
@ -114,17 +117,15 @@ pub async fn create(
pub async fn update( pub async fn update(
id: i32, id: i32,
owner: PostedOwner, bytes: Bytes,
api_key: Option<Uuid>, api_key: Option<Uuid>,
content_type: Option<Mime>, content_type: Option<Mime>,
env: Environment, env: Environment,
) -> Result<impl Reply, Rejection> { ) -> Result<impl Reply, Rejection> {
let content_type = match content_type { let DeserializedBody {
Some(content_type) if content_type == mime::APPLICATION_OCTET_STREAM => { body: owner,
ContentType::Bincode content_type,
} } = DeserializedBody::<PostedOwner>::from_bytes(bytes, content_type).map_err(reject_anyhow)?;
_ => ContentType::Json,
};
let owner_id = authenticate(&env, api_key).await.map_err(reject_anyhow)?; let owner_id = authenticate(&env, api_key).await.map_err(reject_anyhow)?;
let updated_owner = Owner::update(owner, &env.db, owner_id, id) let updated_owner = Owner::update(owner, &env.db, owner_id, id)
.await .await

View File

@ -1,20 +1,22 @@
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use http::StatusCode; use http::StatusCode;
use hyper::body::Bytes;
use mime::Mime; use mime::Mime;
use uuid::Uuid; use uuid::Uuid;
use warp::reply::{with_header, with_status}; use warp::reply::{with_header, with_status};
use warp::{Rejection, Reply}; use warp::{Rejection, Reply};
use crate::caches::CACHES; use crate::caches::{CachedResponse, CACHES};
use crate::models::{ use crate::models::{
InteriorRefList, ListParams, MerchandiseList, PostedShop, Shop, UnsavedInteriorRefList, InteriorRefList, ListParams, MerchandiseList, PostedInteriorRefList, PostedMerchandiseList,
UnsavedMerchandiseList, UnsavedShop, PostedShop, Shop,
}; };
use crate::problem::reject_anyhow; use crate::problem::reject_anyhow;
use crate::Environment; use crate::Environment;
use super::{ use super::{
authenticate, check_etag, AcceptHeader, Bincode, ContentType, DataReply, ETagReply, Json, authenticate, check_etag, AcceptHeader, Bincode, ContentType, DataReply, DeserializedBody,
ETagReply, Json, TypedCache,
}; };
pub async fn get( pub async fn get(
@ -23,10 +25,10 @@ pub async fn get(
accept: Option<AcceptHeader>, accept: Option<AcceptHeader>,
env: Environment, env: Environment,
) -> Result<impl Reply, Rejection> { ) -> Result<impl Reply, Rejection> {
let (content_type, cache) = match accept { let TypedCache {
Some(accept) if accept.accepts_bincode() => (ContentType::Bincode, &CACHES.shop_bin), content_type,
_ => (ContentType::Json, &CACHES.shop), cache,
}; } = TypedCache::<i32, CachedResponse>::pick_cache(accept, &CACHES.shop_bin, &CACHES.shop);
let response = cache let response = cache
.get_response(id, || async { .get_response(id, || async {
let shop = Shop::get(&env.db, id).await?; let shop = Shop::get(&env.db, id).await?;
@ -47,10 +49,14 @@ pub async fn list(
accept: Option<AcceptHeader>, accept: Option<AcceptHeader>,
env: Environment, env: Environment,
) -> Result<impl Reply, Rejection> { ) -> Result<impl Reply, Rejection> {
let (content_type, cache) = match accept { let TypedCache {
Some(accept) if accept.accepts_bincode() => (ContentType::Bincode, &CACHES.list_shops_bin), content_type,
_ => (ContentType::Json, &CACHES.list_shops), cache,
}; } = TypedCache::<ListParams, CachedResponse>::pick_cache(
accept,
&CACHES.list_shops_bin,
&CACHES.list_shops,
);
let response = cache let response = cache
.get_response(list_params.clone(), || async { .get_response(list_params.clone(), || async {
let shops = Shop::list(&env.db, &list_params).await?; let shops = Shop::list(&env.db, &list_params).await?;
@ -66,44 +72,37 @@ pub async fn list(
} }
pub async fn create( pub async fn create(
shop: PostedShop, bytes: Bytes,
api_key: Option<Uuid>, api_key: Option<Uuid>,
content_type: Option<Mime>, content_type: Option<Mime>,
env: Environment, env: Environment,
) -> Result<impl Reply, Rejection> { ) -> Result<impl Reply, Rejection> {
let content_type = match content_type { let DeserializedBody {
Some(content_type) if content_type == mime::APPLICATION_OCTET_STREAM => { body: mut shop,
ContentType::Bincode content_type,
} } = DeserializedBody::<PostedShop>::from_bytes(bytes, content_type).map_err(reject_anyhow)?;
_ => ContentType::Json,
};
let owner_id = authenticate(&env, api_key).await.map_err(reject_anyhow)?; let owner_id = authenticate(&env, api_key).await.map_err(reject_anyhow)?;
let unsaved_shop = UnsavedShop { shop.owner_id = Some(owner_id);
name: shop.name,
description: shop.description,
owner_id,
};
let mut tx = env let mut tx = env
.db .db
.begin() .begin()
.await .await
.map_err(|error| reject_anyhow(anyhow!(error)))?; .map_err(|error| reject_anyhow(anyhow!(error)))?;
let saved_shop = Shop::create(unsaved_shop, &mut tx) let saved_shop = Shop::create(shop, &mut tx).await.map_err(reject_anyhow)?;
.await
.map_err(reject_anyhow)?;
// also save empty interior_ref_list and merchandise_list rows // also save empty interior_ref_list and merchandise_list rows
let interior_ref_list = UnsavedInteriorRefList { let interior_ref_list = PostedInteriorRefList {
shop_id: saved_shop.id, shop_id: saved_shop.id,
owner_id, owner_id: Some(owner_id),
ref_list: sqlx::types::Json::default(), ref_list: sqlx::types::Json::default(),
shelves: sqlx::types::Json::default(),
}; };
InteriorRefList::create(interior_ref_list, &mut tx) InteriorRefList::create(interior_ref_list, &mut tx)
.await .await
.map_err(reject_anyhow)?; .map_err(reject_anyhow)?;
let merchandise_list = UnsavedMerchandiseList { let merchandise_list = PostedMerchandiseList {
shop_id: saved_shop.id, shop_id: saved_shop.id,
owner_id, owner_id: Some(owner_id),
form_list: sqlx::types::Json::default(), form_list: sqlx::types::Json::default(),
}; };
MerchandiseList::create(merchandise_list, &mut tx) MerchandiseList::create(merchandise_list, &mut tx)
@ -133,27 +132,22 @@ pub async fn create(
pub async fn update( pub async fn update(
id: i32, id: i32,
shop: PostedShop, bytes: Bytes,
api_key: Option<Uuid>, api_key: Option<Uuid>,
content_type: Option<Mime>, content_type: Option<Mime>,
env: Environment, env: Environment,
) -> Result<impl Reply, Rejection> { ) -> Result<impl Reply, Rejection> {
let content_type = match content_type { let DeserializedBody {
Some(content_type) if content_type == mime::APPLICATION_OCTET_STREAM => { body: mut shop,
ContentType::Bincode content_type,
} } = DeserializedBody::<PostedShop>::from_bytes(bytes, content_type).map_err(reject_anyhow)?;
_ => ContentType::Json,
};
let owner_id = authenticate(&env, api_key).await.map_err(reject_anyhow)?; let owner_id = authenticate(&env, api_key).await.map_err(reject_anyhow)?;
let posted_shop = PostedShop { shop.owner_id = match shop.owner_id {
owner_id: match shop.owner_id { // allows an owner to transfer ownership of shop to another owner
// allows an owner to transfer ownership of shop to another owner Some(posted_owner_id) => Some(posted_owner_id),
Some(posted_owner_id) => Some(posted_owner_id), None => Some(owner_id),
None => Some(owner_id),
},
..shop
}; };
let updated_shop = Shop::update(posted_shop, &env.db, owner_id, id) let updated_shop = Shop::update(shop, &env.db, owner_id, id)
.await .await
.map_err(reject_anyhow)?; .map_err(reject_anyhow)?;
let url = updated_shop.url(&env.api_url).map_err(reject_anyhow)?; let url = updated_shop.url(&env.api_url).map_err(reject_anyhow)?;

View File

@ -1,19 +1,20 @@
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use http::StatusCode; use http::StatusCode;
use http_api_problem::HttpApiProblem;
use hyper::body::Bytes;
use mime::Mime; use mime::Mime;
use uuid::Uuid; use uuid::Uuid;
use warp::reply::{with_header, with_status}; use warp::reply::{with_header, with_status};
use warp::{Rejection, Reply}; use warp::{reject, Rejection, Reply};
use crate::caches::CACHES; use crate::caches::{CachedResponse, CACHES};
use crate::models::{ use crate::models::{ListParams, MerchandiseList, PostedTransaction, Shop, Transaction};
ListParams, MerchandiseList, PostedTransaction, Transaction, UnsavedTransaction,
};
use crate::problem::reject_anyhow; use crate::problem::reject_anyhow;
use crate::Environment; use crate::Environment;
use super::{ use super::{
authenticate, check_etag, AcceptHeader, Bincode, ContentType, DataReply, ETagReply, Json, authenticate, check_etag, AcceptHeader, Bincode, ContentType, DataReply, DeserializedBody,
ETagReply, Json, TypedCache,
}; };
pub async fn get( pub async fn get(
@ -22,10 +23,14 @@ pub async fn get(
accept: Option<AcceptHeader>, accept: Option<AcceptHeader>,
env: Environment, env: Environment,
) -> Result<impl Reply, Rejection> { ) -> Result<impl Reply, Rejection> {
let (content_type, cache) = match accept { let TypedCache {
Some(accept) if accept.accepts_bincode() => (ContentType::Bincode, &CACHES.transaction_bin), content_type,
_ => (ContentType::Json, &CACHES.transaction), cache,
}; } = TypedCache::<i32, CachedResponse>::pick_cache(
accept,
&CACHES.transaction_bin,
&CACHES.transaction,
);
let response = cache let response = cache
.get_response(id, || async { .get_response(id, || async {
let transaction = Transaction::get(&env.db, id).await?; let transaction = Transaction::get(&env.db, id).await?;
@ -48,12 +53,14 @@ pub async fn list(
accept: Option<AcceptHeader>, accept: Option<AcceptHeader>,
env: Environment, env: Environment,
) -> Result<impl Reply, Rejection> { ) -> Result<impl Reply, Rejection> {
let (content_type, cache) = match accept { let TypedCache {
Some(accept) if accept.accepts_bincode() => { content_type,
(ContentType::Bincode, &CACHES.list_transactions_bin) cache,
} } = TypedCache::<ListParams, CachedResponse>::pick_cache(
_ => (ContentType::Json, &CACHES.list_transactions), accept,
}; &CACHES.list_transactions_bin,
&CACHES.list_transactions,
);
let response = cache let response = cache
.get_response(list_params.clone(), || async { .get_response(list_params.clone(), || async {
let transactions = Transaction::list(&env.db, &list_params).await?; let transactions = Transaction::list(&env.db, &list_params).await?;
@ -77,13 +84,14 @@ pub async fn list_by_shop_id(
accept: Option<AcceptHeader>, accept: Option<AcceptHeader>,
env: Environment, env: Environment,
) -> Result<impl Reply, Rejection> { ) -> Result<impl Reply, Rejection> {
let (content_type, cache) = match accept { let TypedCache {
Some(accept) if accept.accepts_bincode() => ( content_type,
ContentType::Bincode, cache,
&CACHES.list_transactions_by_shop_id_bin, } = TypedCache::<(i32, ListParams), CachedResponse>::pick_cache(
), accept,
_ => (ContentType::Json, &CACHES.list_transactions_by_shop_id), &CACHES.list_transactions_by_shop_id_bin,
}; &CACHES.list_transactions_by_shop_id,
);
let response = cache let response = cache
.get_response((shop_id, list_params.clone()), || async { .get_response((shop_id, list_params.clone()), || async {
let transactions = Transaction::list_by_shop_id(&env.db, shop_id, &list_params).await?; let transactions = Transaction::list_by_shop_id(&env.db, shop_id, &list_params).await?;
@ -101,42 +109,43 @@ pub async fn list_by_shop_id(
} }
pub async fn create( pub async fn create(
transaction: PostedTransaction, bytes: Bytes,
api_key: Option<Uuid>, api_key: Option<Uuid>,
content_type: Option<Mime>, content_type: Option<Mime>,
env: Environment, env: Environment,
) -> Result<impl Reply, Rejection> { ) -> Result<impl Reply, Rejection> {
let content_type = match content_type { let DeserializedBody {
Some(content_type) if content_type == mime::APPLICATION_OCTET_STREAM => { body: mut transaction,
ContentType::Bincode content_type,
} } = DeserializedBody::<PostedTransaction>::from_bytes(bytes, content_type)
_ => ContentType::Json, .map_err(reject_anyhow)?;
};
let owner_id = authenticate(&env, api_key).await.map_err(reject_anyhow)?; let owner_id = authenticate(&env, api_key).await.map_err(reject_anyhow)?;
let unsaved_transaction = UnsavedTransaction { transaction.owner_id = Some(owner_id);
shop_id: transaction.shop_id,
owner_id,
mod_name: transaction.mod_name,
local_form_id: transaction.local_form_id,
name: transaction.name,
form_type: transaction.form_type,
is_food: transaction.is_food,
price: transaction.price,
is_sell: transaction.is_sell,
quantity: transaction.quantity,
amount: transaction.amount,
};
let mut tx = env let mut tx = env
.db .db
.begin() .begin()
.await .await
.map_err(|error| reject_anyhow(anyhow!(error)))?; .map_err(|error| reject_anyhow(anyhow!(error)))?;
let saved_transaction = Transaction::create(unsaved_transaction, &mut tx) let saved_transaction = Transaction::create(transaction, &mut tx)
.await .await
.map_err(reject_anyhow)?; .map_err(reject_anyhow)?;
let quantity_delta = match transaction.is_sell { if !Shop::accepts_keywords(
true => transaction.quantity, &mut tx,
false => transaction.quantity * -1, saved_transaction.shop_id,
&saved_transaction.keywords,
)
.await
.map_err(reject_anyhow)?
{
return Err(reject::custom(
HttpApiProblem::with_title_and_type_from_status(StatusCode::BAD_REQUEST)
.set_title("Unacceptable Merchandise Type")
.set_detail("Shop does not accept that kind of merchandise"),
));
}
let (quantity_delta, shop_gold_delta) = match saved_transaction.is_sell {
true => (saved_transaction.quantity, saved_transaction.price * -1),
false => (saved_transaction.quantity * -1, saved_transaction.price),
}; };
let updated_merchandise_list = MerchandiseList::update_merchandise_quantity( let updated_merchandise_list = MerchandiseList::update_merchandise_quantity(
&mut tx, &mut tx,
@ -148,9 +157,13 @@ pub async fn create(
saved_transaction.is_food, saved_transaction.is_food,
saved_transaction.price, saved_transaction.price,
quantity_delta, quantity_delta,
&saved_transaction.keywords,
) )
.await .await
.map_err(reject_anyhow)?; .map_err(reject_anyhow)?;
Shop::update_gold(&mut tx, saved_transaction.shop_id, shop_gold_delta)
.await
.map_err(reject_anyhow)?;
tx.commit() tx.commit()
.await .await
.map_err(|error| reject_anyhow(anyhow!(error)))?; .map_err(|error| reject_anyhow(anyhow!(error)))?;
@ -189,10 +202,21 @@ pub async fn create(
CACHES.list_transactions_by_shop_id_bin.clear().await; CACHES.list_transactions_by_shop_id_bin.clear().await;
CACHES.list_merchandise_lists.clear().await; CACHES.list_merchandise_lists.clear().await;
CACHES.list_merchandise_lists_bin.clear().await; CACHES.list_merchandise_lists_bin.clear().await;
CACHES
.shop
.delete_response(updated_merchandise_list.shop_id)
.await;
CACHES
.shop_bin
.delete_response(updated_merchandise_list.shop_id)
.await;
CACHES.list_shops.clear().await;
CACHES.list_shops_bin.clear().await;
}); });
Ok(reply) Ok(reply)
} }
// Does NOT reverse the transaction side-effects!
pub async fn delete( pub async fn delete(
id: i32, id: i32,
api_key: Option<Uuid>, api_key: Option<Uuid>,

View File

@ -3,16 +3,16 @@ extern crate lazy_static;
use anyhow::Result; use anyhow::Result;
use dotenv::dotenv; use dotenv::dotenv;
use http::StatusCode; use http::header::SERVER;
use hyper::server::Server; use hyper::{body::Bytes, server::Server};
use listenfd::ListenFd; use listenfd::ListenFd;
use serde::{de::DeserializeOwned, Serialize};
use sqlx::postgres::PgPoolOptions; use sqlx::postgres::PgPoolOptions;
use sqlx::{migrate, Pool, Postgres}; use sqlx::{migrate, Pool, Postgres};
use std::convert::Infallible; use std::convert::Infallible;
use std::env; use std::env;
use tracing_subscriber::fmt::format::FmtSpan; use tracing_subscriber::fmt::format::FmtSpan;
use url::Url; use url::Url;
use warp::http::Response;
use warp::Filter; use warp::Filter;
mod caches; mod caches;
@ -22,10 +22,8 @@ mod macros;
mod models; mod models;
mod problem; mod problem;
use models::{ use handlers::SERVER_STRING;
ListParams, PostedInteriorRefList, PostedMerchandiseList, PostedOwner, PostedShop, use models::ListParams;
PostedTransaction,
};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Environment { pub struct Environment {
@ -45,21 +43,12 @@ impl Environment {
} }
} }
#[derive(Serialize)]
struct ErrorMessage {
code: u16,
message: String,
}
fn with_env(env: Environment) -> impl Filter<Extract = (Environment,), Error = Infallible> + Clone { fn with_env(env: Environment) -> impl Filter<Extract = (Environment,), Error = Infallible> + Clone {
warp::any().map(move || env.clone()) warp::any().map(move || env.clone())
} }
fn json_body<T>() -> impl Filter<Extract = (T,), Error = warp::Rejection> + Clone fn extract_body_bytes() -> impl Filter<Extract = (Bytes,), Error = warp::Rejection> + Clone {
where warp::body::content_length_limit(1024 * 1024).and(warp::body::bytes())
T: Send + DeserializeOwned,
{
warp::body::content_length_limit(1024 * 1024).and(warp::body::json())
} }
#[tokio::main] #[tokio::main]
@ -69,10 +58,11 @@ async fn main() -> Result<()> {
let env_log_filter = let env_log_filter =
env::var("RUST_LOG").unwrap_or_else(|_| "warp=info,bazaar_realm_api=info".to_owned()); env::var("RUST_LOG").unwrap_or_else(|_| "warp=info,bazaar_realm_api=info".to_owned());
let (non_blocking_writer, _guard) = tracing_appender::non_blocking(std::io::stdout());
tracing_subscriber::fmt() tracing_subscriber::fmt()
.with_env_filter(env_log_filter) .with_env_filter(env_log_filter)
.with_span_events(FmtSpan::CLOSE) .with_span_events(FmtSpan::CLOSE)
.with_writer(std::io::stdout) .with_writer(non_blocking_writer)
.init(); .init();
let host = env::var("HOST").expect("`HOST` environment variable not defined"); let host = env::var("HOST").expect("`HOST` environment variable not defined");
@ -85,7 +75,7 @@ async fn main() -> Result<()> {
let status_handler = warp::path::path("status") let status_handler = warp::path::path("status")
.and(warp::path::end()) .and(warp::path::end())
.and(warp::get()) .and(warp::get())
.map(|| StatusCode::OK); // TODO: return what api versions this server supports instead .map(|| Response::builder().header(SERVER, SERVER_STRING).body("Ok"));
let get_owner_handler = warp::path("owners").and( let get_owner_handler = warp::path("owners").and(
warp::path::param() warp::path::param()
.and(warp::path::end()) .and(warp::path::end())
@ -98,7 +88,7 @@ async fn main() -> Result<()> {
let create_owner_handler = warp::path("owners").and( let create_owner_handler = warp::path("owners").and(
warp::path::end() warp::path::end()
.and(warp::post()) .and(warp::post())
.and(json_body::<PostedOwner>()) .and(extract_body_bytes())
.and(warp::addr::remote()) .and(warp::addr::remote())
.and(warp::header::optional("api-key")) .and(warp::header::optional("api-key"))
.and(warp::header::optional("x-real-ip")) .and(warp::header::optional("x-real-ip"))
@ -118,7 +108,7 @@ async fn main() -> Result<()> {
warp::path::param() warp::path::param()
.and(warp::path::end()) .and(warp::path::end())
.and(warp::patch()) .and(warp::patch())
.and(json_body::<PostedOwner>()) .and(extract_body_bytes())
.and(warp::header::optional("api-key")) .and(warp::header::optional("api-key"))
.and(warp::header::optional("content-type")) .and(warp::header::optional("content-type"))
.and(with_env(env.clone())) .and(with_env(env.clone()))
@ -145,7 +135,7 @@ async fn main() -> Result<()> {
let create_shop_handler = warp::path("shops").and( let create_shop_handler = warp::path("shops").and(
warp::path::end() warp::path::end()
.and(warp::post()) .and(warp::post())
.and(json_body::<PostedShop>()) .and(extract_body_bytes())
.and(warp::header::optional("api-key")) .and(warp::header::optional("api-key"))
.and(warp::header::optional("content-type")) .and(warp::header::optional("content-type"))
.and(with_env(env.clone())) .and(with_env(env.clone()))
@ -163,7 +153,7 @@ async fn main() -> Result<()> {
warp::path::param() warp::path::param()
.and(warp::path::end()) .and(warp::path::end())
.and(warp::patch()) .and(warp::patch())
.and(json_body::<PostedShop>()) .and(extract_body_bytes())
.and(warp::header::optional("api-key")) .and(warp::header::optional("api-key"))
.and(warp::header::optional("content-type")) .and(warp::header::optional("content-type"))
.and(with_env(env.clone())) .and(with_env(env.clone()))
@ -190,7 +180,7 @@ async fn main() -> Result<()> {
let create_interior_ref_list_handler = warp::path("interior_ref_lists").and( let create_interior_ref_list_handler = warp::path("interior_ref_lists").and(
warp::path::end() warp::path::end()
.and(warp::post()) .and(warp::post())
.and(json_body::<PostedInteriorRefList>()) .and(extract_body_bytes())
.and(warp::header::optional("api-key")) .and(warp::header::optional("api-key"))
.and(warp::header::optional("content-type")) .and(warp::header::optional("content-type"))
.and(with_env(env.clone())) .and(with_env(env.clone()))
@ -208,7 +198,7 @@ async fn main() -> Result<()> {
warp::path::param() warp::path::param()
.and(warp::path::end()) .and(warp::path::end())
.and(warp::patch()) .and(warp::patch())
.and(json_body::<PostedInteriorRefList>()) .and(extract_body_bytes())
.and(warp::header::optional("api-key")) .and(warp::header::optional("api-key"))
.and(warp::header::optional("content-type")) .and(warp::header::optional("content-type"))
.and(with_env(env.clone())) .and(with_env(env.clone()))
@ -219,7 +209,7 @@ async fn main() -> Result<()> {
.and(warp::path("interior_ref_list")) .and(warp::path("interior_ref_list"))
.and(warp::path::end()) .and(warp::path::end())
.and(warp::patch()) .and(warp::patch())
.and(json_body::<PostedInteriorRefList>()) .and(extract_body_bytes())
.and(warp::header::optional("api-key")) .and(warp::header::optional("api-key"))
.and(warp::header::optional("content-type")) .and(warp::header::optional("content-type"))
.and(with_env(env.clone())) .and(with_env(env.clone()))
@ -256,7 +246,7 @@ async fn main() -> Result<()> {
let create_merchandise_list_handler = warp::path("merchandise_lists").and( let create_merchandise_list_handler = warp::path("merchandise_lists").and(
warp::path::end() warp::path::end()
.and(warp::post()) .and(warp::post())
.and(json_body::<PostedMerchandiseList>()) .and(extract_body_bytes())
.and(warp::header::optional("api-key")) .and(warp::header::optional("api-key"))
.and(warp::header::optional("content-type")) .and(warp::header::optional("content-type"))
.and(with_env(env.clone())) .and(with_env(env.clone()))
@ -274,7 +264,7 @@ async fn main() -> Result<()> {
warp::path::param() warp::path::param()
.and(warp::path::end()) .and(warp::path::end())
.and(warp::patch()) .and(warp::patch())
.and(json_body::<PostedMerchandiseList>()) .and(extract_body_bytes())
.and(warp::header::optional("api-key")) .and(warp::header::optional("api-key"))
.and(warp::header::optional("content-type")) .and(warp::header::optional("content-type"))
.and(with_env(env.clone())) .and(with_env(env.clone()))
@ -285,7 +275,7 @@ async fn main() -> Result<()> {
.and(warp::path("merchandise_list")) .and(warp::path("merchandise_list"))
.and(warp::path::end()) .and(warp::path::end())
.and(warp::patch()) .and(warp::patch())
.and(json_body::<PostedMerchandiseList>()) .and(extract_body_bytes())
.and(warp::header::optional("api-key")) .and(warp::header::optional("api-key"))
.and(warp::header::optional("content-type")) .and(warp::header::optional("content-type"))
.and(with_env(env.clone())) .and(with_env(env.clone()))
@ -322,7 +312,7 @@ async fn main() -> Result<()> {
let create_transaction_handler = warp::path("transactions").and( let create_transaction_handler = warp::path("transactions").and(
warp::path::end() warp::path::end()
.and(warp::post()) .and(warp::post())
.and(json_body::<PostedTransaction>()) .and(extract_body_bytes())
.and(warp::header::optional("api-key")) .and(warp::header::optional("api-key"))
.and(warp::header::optional("content-type")) .and(warp::header::optional("content-type"))
.and(with_env(env.clone())) .and(with_env(env.clone()))
@ -395,6 +385,21 @@ async fn main() -> Result<()> {
.with(warp::compression::gzip()) .with(warp::compression::gzip())
.with(warp::trace::request()); .with(warp::trace::request());
if let Ok(tls_cert) = env::var("TLS_CERT") {
if let Ok(tls_key) = env::var("TLS_KEY") {
let port = env::var("PORT")
.unwrap_or_else(|_| "443".to_owned())
.parse()?;
warp::serve(routes)
.tls()
.cert_path(tls_cert)
.key_path(tls_key)
.run(([0, 0, 0, 0], port))
.await;
return Ok(());
}
}
let svc = warp::service(routes); let svc = warp::service(routes);
let make_svc = hyper::service::make_service_fn(|_: _| { let make_svc = hyper::service::make_service_fn(|_: _| {
let svc = svc.clone(); let svc = svc.clone();
@ -405,10 +410,12 @@ async fn main() -> Result<()> {
let server = if let Some(l) = listenfd.take_tcp_listener(0)? { let server = if let Some(l) = listenfd.take_tcp_listener(0)? {
Server::from_tcp(l)? Server::from_tcp(l)?
} else { } else {
Server::bind(&([0, 0, 0, 0], 3030).into()) let port = env::var("PORT")
.unwrap_or_else(|_| "3030".to_owned())
.parse()?;
Server::bind(&([0, 0, 0, 0], port).into())
}; };
// warp::serve(routes).run(([127, 0, 0, 1], 3030)).await;
server.serve(make_svc).await?; server.serve(make_svc).await?;
Ok(()) Ok(())
} }

View File

@ -9,12 +9,12 @@ use url::Url;
use super::ListParams; use super::ListParams;
use crate::problem::forbidden_permission; use crate::problem::forbidden_permission;
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(sqlx::FromRow, Debug, Serialize, Deserialize, Clone)]
pub struct InteriorRef { pub struct InteriorRef {
pub base_mod_name: String, pub base_mod_name: String,
pub base_local_form_id: i32, pub base_local_form_id: u32,
pub ref_mod_name: Option<String>, pub ref_mod_name: Option<String>,
pub ref_local_form_id: i32, pub ref_local_form_id: u32,
pub position_x: f32, pub position_x: f32,
pub position_y: f32, pub position_y: f32,
pub position_z: f32, pub position_z: f32,
@ -24,28 +24,41 @@ pub struct InteriorRef {
pub scale: u16, pub scale: u16,
} }
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(sqlx::FromRow, Debug, Serialize, Deserialize, Clone)]
pub struct Shelf {
pub shelf_type: u32,
pub position_x: f32,
pub position_y: f32,
pub position_z: f32,
pub angle_x: f32,
pub angle_y: f32,
pub angle_z: f32,
pub scale: u16,
pub page: u32,
pub filter_form_type: Option<u32>,
pub filter_is_food: bool,
pub search: Option<String>,
pub sort_on: Option<String>,
pub sort_asc: bool,
}
#[derive(sqlx::FromRow, Debug, Serialize, Deserialize, Clone)]
pub struct InteriorRefList { pub struct InteriorRefList {
pub id: i32, pub id: i32,
pub shop_id: i32, pub shop_id: i32,
pub owner_id: i32, pub owner_id: i32,
pub ref_list: serde_json::Value, pub ref_list: Json<Vec<InteriorRef>>,
pub shelves: Json<Vec<Shelf>>,
pub created_at: NaiveDateTime, pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime, pub updated_at: NaiveDateTime,
} }
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct UnsavedInteriorRefList {
pub shop_id: i32,
pub owner_id: i32,
pub ref_list: Json<Vec<InteriorRef>>,
}
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub struct PostedInteriorRefList { pub struct PostedInteriorRefList {
pub shop_id: i32, pub shop_id: i32,
pub owner_id: Option<i32>, pub owner_id: Option<i32>,
pub ref_list: Json<Vec<InteriorRef>>, pub ref_list: Json<Vec<InteriorRef>>,
pub shelves: Json<Vec<Shelf>>,
} }
impl InteriorRefList { impl InteriorRefList {
@ -64,26 +77,36 @@ impl InteriorRefList {
// TODO: this model will probably never need to be accessed through it's ID, should these methods be removed/unimplemented? // TODO: this model will probably never need to be accessed through it's ID, should these methods be removed/unimplemented?
#[instrument(level = "debug", skip(db))] #[instrument(level = "debug", skip(db))]
pub async fn get(db: impl Executor<'_, Database = Postgres>, id: i32) -> Result<Self> { pub async fn get(db: impl Executor<'_, Database = Postgres>, id: i32) -> Result<Self> {
sqlx::query_as!(Self, "SELECT * FROM interior_ref_lists WHERE id = $1", id) sqlx::query_as!(
.fetch_one(db) Self,
.await r#"SELECT id, shop_id, owner_id, created_at, updated_at,
.map_err(Error::new) ref_list as "ref_list: Json<Vec<InteriorRef>>",
shelves as "shelves: Json<Vec<Shelf>>"
FROM interior_ref_lists WHERE id = $1"#,
id
)
.fetch_one(db)
.await
.map_err(Error::new)
} }
#[instrument(level = "debug", skip(interior_ref_list, db))] #[instrument(level = "debug", skip(interior_ref_list, db))]
pub async fn create( pub async fn create(
interior_ref_list: UnsavedInteriorRefList, interior_ref_list: PostedInteriorRefList,
db: impl Executor<'_, Database = Postgres>, db: impl Executor<'_, Database = Postgres>,
) -> Result<Self> { ) -> Result<Self> {
Ok(sqlx::query_as!( Ok(sqlx::query_as!(
Self, Self,
"INSERT INTO interior_ref_lists r#"INSERT INTO interior_ref_lists
(shop_id, owner_id, ref_list, created_at, updated_at) (shop_id, owner_id, ref_list, shelves, created_at, updated_at)
VALUES ($1, $2, $3, now(), now()) VALUES ($1, $2, $3, $4, now(), now())
RETURNING *", RETURNING id, shop_id, owner_id, created_at, updated_at,
ref_list as "ref_list: Json<Vec<InteriorRef>>",
shelves as "shelves: Json<Vec<Shelf>>""#,
interior_ref_list.shop_id, interior_ref_list.shop_id,
interior_ref_list.owner_id, interior_ref_list.owner_id,
serde_json::json!(interior_ref_list.ref_list), serde_json::json!(interior_ref_list.ref_list),
serde_json::json!(interior_ref_list.shelves),
) )
.fetch_one(db) .fetch_one(db)
.await?) .await?)
@ -119,10 +142,12 @@ impl InteriorRefList {
let result = if let Some(order_by) = list_params.get_order_by() { let result = if let Some(order_by) = list_params.get_order_by() {
sqlx::query_as!( sqlx::query_as!(
Self, Self,
"SELECT * FROM interior_ref_lists r#"SELECT id, shop_id, owner_id, created_at, updated_at,
ref_list as "ref_list: Json<Vec<InteriorRef>>",
shelves as "shelves: Json<Vec<Shelf>>" FROM interior_ref_lists
ORDER BY $1 ORDER BY $1
LIMIT $2 LIMIT $2
OFFSET $3", OFFSET $3"#,
order_by, order_by,
list_params.limit.unwrap_or(10), list_params.limit.unwrap_or(10),
list_params.offset.unwrap_or(0), list_params.offset.unwrap_or(0),
@ -132,9 +157,11 @@ impl InteriorRefList {
} else { } else {
sqlx::query_as!( sqlx::query_as!(
Self, Self,
"SELECT * FROM interior_ref_lists r#"SELECT id, shop_id, owner_id, created_at, updated_at,
ref_list as "ref_list: Json<Vec<InteriorRef>>",
shelves as "shelves: Json<Vec<Shelf>>" FROM interior_ref_lists
LIMIT $1 LIMIT $1
OFFSET $2", OFFSET $2"#,
list_params.limit.unwrap_or(10), list_params.limit.unwrap_or(10),
list_params.offset.unwrap_or(0), list_params.offset.unwrap_or(0),
) )
@ -158,13 +185,17 @@ impl InteriorRefList {
if existing_interior_ref_list.owner_id == owner_id { if existing_interior_ref_list.owner_id == owner_id {
Ok(sqlx::query_as!( Ok(sqlx::query_as!(
Self, Self,
"UPDATE interior_ref_lists SET r#"UPDATE interior_ref_lists SET
ref_list = $2, ref_list = $2,
shelves = $3,
updated_at = now() updated_at = now()
WHERE id = $1 WHERE id = $1
RETURNING *", RETURNING id, shop_id, owner_id, created_at, updated_at,
ref_list as "ref_list: Json<Vec<InteriorRef>>",
shelves as "shelves: Json<Vec<Shelf>>""#,
id, id,
serde_json::json!(interior_ref_list.ref_list), serde_json::json!(interior_ref_list.ref_list),
serde_json::json!(interior_ref_list.shelves),
) )
.fetch_one(db) .fetch_one(db)
.await?) .await?)
@ -180,8 +211,10 @@ impl InteriorRefList {
) -> Result<Self> { ) -> Result<Self> {
sqlx::query_as!( sqlx::query_as!(
Self, Self,
"SELECT * FROM interior_ref_lists r#"SELECT id, shop_id, owner_id, created_at, updated_at,
WHERE shop_id = $1", ref_list as "ref_list: Json<Vec<InteriorRef>>",
shelves as "shelves: Json<Vec<Shelf>>" FROM interior_ref_lists
WHERE shop_id = $1"#,
shop_id, shop_id,
) )
.fetch_one(db) .fetch_one(db)
@ -205,13 +238,17 @@ impl InteriorRefList {
if existing_interior_ref_list.owner_id == owner_id { if existing_interior_ref_list.owner_id == owner_id {
Ok(sqlx::query_as!( Ok(sqlx::query_as!(
Self, Self,
"UPDATE interior_ref_lists SET r#"UPDATE interior_ref_lists SET
ref_list = $2, ref_list = $2,
shelves = $3,
updated_at = now() updated_at = now()
WHERE shop_id = $1 WHERE shop_id = $1
RETURNING *", RETURNING id, shop_id, owner_id, created_at, updated_at,
ref_list as "ref_list: Json<Vec<InteriorRef>>",
shelves as "shelves: Json<Vec<Shelf>>""#,
shop_id, shop_id,
serde_json::json!(interior_ref_list.ref_list), serde_json::json!(interior_ref_list.ref_list),
serde_json::json!(interior_ref_list.shelves),
) )
.fetch_one(db) .fetch_one(db)
.await?) .await?)

View File

@ -15,12 +15,13 @@ use crate::problem::forbidden_permission;
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Merchandise { pub struct Merchandise {
pub mod_name: String, pub mod_name: String,
pub local_form_id: i32, pub local_form_id: u32,
pub name: String, pub name: String,
pub quantity: i32, pub quantity: u32,
pub form_type: i32, pub form_type: u32,
pub is_food: bool, pub is_food: bool,
pub price: i32, pub price: u32,
pub keywords: Vec<String>,
} }
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
@ -28,18 +29,11 @@ pub struct MerchandiseList {
pub id: i32, pub id: i32,
pub shop_id: i32, pub shop_id: i32,
pub owner_id: i32, pub owner_id: i32,
pub form_list: serde_json::Value, pub form_list: Json<Vec<Merchandise>>,
pub created_at: NaiveDateTime, pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime, pub updated_at: NaiveDateTime,
} }
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct UnsavedMerchandiseList {
pub shop_id: i32,
pub owner_id: i32,
pub form_list: Json<Vec<Merchandise>>,
}
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub struct PostedMerchandiseList { pub struct PostedMerchandiseList {
pub shop_id: i32, pub shop_id: i32,
@ -63,23 +57,31 @@ impl MerchandiseList {
// TODO: this model will probably never need to be accessed through it's ID, should these methods be removed/unimplemented? // TODO: this model will probably never need to be accessed through it's ID, should these methods be removed/unimplemented?
#[instrument(level = "debug", skip(db))] #[instrument(level = "debug", skip(db))]
pub async fn get(db: impl Executor<'_, Database = Postgres>, id: i32) -> Result<Self> { pub async fn get(db: impl Executor<'_, Database = Postgres>, id: i32) -> Result<Self> {
sqlx::query_as!(Self, "SELECT * FROM merchandise_lists WHERE id = $1", id) sqlx::query_as!(
.fetch_one(db) Self,
.await r#"SELECT id, shop_id, owner_id, created_at, updated_at,
.map_err(Error::new) form_list as "form_list: Json<Vec<Merchandise>>"
FROM merchandise_lists
WHERE id = $1"#,
id,
)
.fetch_one(db)
.await
.map_err(Error::new)
} }
#[instrument(level = "debug", skip(merchandise_list, db))] #[instrument(level = "debug", skip(merchandise_list, db))]
pub async fn create( pub async fn create(
merchandise_list: UnsavedMerchandiseList, merchandise_list: PostedMerchandiseList,
db: impl Executor<'_, Database = Postgres>, db: impl Executor<'_, Database = Postgres>,
) -> Result<Self> { ) -> Result<Self> {
Ok(sqlx::query_as!( Ok(sqlx::query_as!(
Self, Self,
"INSERT INTO merchandise_lists r#"INSERT INTO merchandise_lists
(shop_id, owner_id, form_list, created_at, updated_at) (shop_id, owner_id, form_list, created_at, updated_at)
VALUES ($1, $2, $3, now(), now()) VALUES ($1, $2, $3, now(), now())
RETURNING *", RETURNING id, shop_id, owner_id, created_at, updated_at,
form_list as "form_list: Json<Vec<Merchandise>>""#,
merchandise_list.shop_id, merchandise_list.shop_id,
merchandise_list.owner_id, merchandise_list.owner_id,
serde_json::json!(merchandise_list.form_list), serde_json::json!(merchandise_list.form_list),
@ -118,10 +120,12 @@ impl MerchandiseList {
let result = if let Some(order_by) = list_params.get_order_by() { let result = if let Some(order_by) = list_params.get_order_by() {
sqlx::query_as!( sqlx::query_as!(
Self, Self,
"SELECT * FROM merchandise_lists r#"SELECT id, shop_id, owner_id, created_at, updated_at,
form_list as "form_list: Json<Vec<Merchandise>>"
FROM merchandise_lists
ORDER BY $1 ORDER BY $1
LIMIT $2 LIMIT $2
OFFSET $3", OFFSET $3"#,
order_by, order_by,
list_params.limit.unwrap_or(10), list_params.limit.unwrap_or(10),
list_params.offset.unwrap_or(0), list_params.offset.unwrap_or(0),
@ -131,9 +135,11 @@ impl MerchandiseList {
} else { } else {
sqlx::query_as!( sqlx::query_as!(
Self, Self,
"SELECT * FROM merchandise_lists r#"SELECT id, shop_id, owner_id, created_at, updated_at,
form_list as "form_list: Json<Vec<Merchandise>>"
FROM merchandise_lists
LIMIT $1 LIMIT $1
OFFSET $2", OFFSET $2"#,
list_params.limit.unwrap_or(10), list_params.limit.unwrap_or(10),
list_params.offset.unwrap_or(0), list_params.offset.unwrap_or(0),
) )
@ -157,11 +163,12 @@ impl MerchandiseList {
if existing_merchandise_list.owner_id == owner_id { if existing_merchandise_list.owner_id == owner_id {
Ok(sqlx::query_as!( Ok(sqlx::query_as!(
Self, Self,
"UPDATE merchandise_lists SET r#"UPDATE merchandise_lists SET
form_list = $2, form_list = $2,
updated_at = now() updated_at = now()
WHERE id = $1 WHERE id = $1
RETURNING *", RETURNING id, shop_id, owner_id, created_at, updated_at,
form_list as "form_list: Json<Vec<Merchandise>>""#,
id, id,
serde_json::json!(merchandise_list.form_list), serde_json::json!(merchandise_list.form_list),
) )
@ -179,8 +186,10 @@ impl MerchandiseList {
) -> Result<Self> { ) -> Result<Self> {
sqlx::query_as!( sqlx::query_as!(
Self, Self,
"SELECT * FROM merchandise_lists r#"SELECT id, shop_id, owner_id, created_at, updated_at,
WHERE shop_id = $1", form_list as "form_list: Json<Vec<Merchandise>>"
FROM merchandise_lists
WHERE shop_id = $1"#,
shop_id, shop_id,
) )
.fetch_one(db) .fetch_one(db)
@ -204,11 +213,12 @@ impl MerchandiseList {
if existing_merchandise_list.owner_id == owner_id { if existing_merchandise_list.owner_id == owner_id {
Ok(sqlx::query_as!( Ok(sqlx::query_as!(
Self, Self,
"UPDATE merchandise_lists SET r#"UPDATE merchandise_lists SET
form_list = $2, form_list = $2,
updated_at = now() updated_at = now()
WHERE shop_id = $1 WHERE shop_id = $1
RETURNING *", RETURNING id, shop_id, owner_id, created_at, updated_at,
form_list as "form_list: Json<Vec<Merchandise>>""#,
shop_id, shop_id,
serde_json::json!(merchandise_list.form_list), serde_json::json!(merchandise_list.form_list),
) )
@ -230,6 +240,7 @@ impl MerchandiseList {
is_food: bool, is_food: bool,
price: i32, price: i32,
quantity_delta: i32, quantity_delta: i32,
keywords: &[String],
) -> Result<Self> { ) -> Result<Self> {
let add_item = json!([{ let add_item = json!([{
"mod_name": mod_name, "mod_name": mod_name,
@ -239,10 +250,11 @@ impl MerchandiseList {
"form_type": form_type, "form_type": form_type,
"is_food": is_food, "is_food": is_food,
"price": price, "price": price,
"keywords": keywords,
}]); }]);
Ok(sqlx::query_as!( Ok(sqlx::query_as!(
Self, Self,
"UPDATE r#"UPDATE
merchandise_lists merchandise_lists
SET SET
form_list = CASE form_list = CASE
@ -277,7 +289,13 @@ impl MerchandiseList {
) sub ) sub
WHERE WHERE
shop_id = $1 shop_id = $1
RETURNING merchandise_lists.*", RETURNING
merchandise_lists.id,
merchandise_lists.shop_id,
merchandise_lists.owner_id,
merchandise_lists.created_at,
merchandise_lists.updated_at,
merchandise_lists.form_list as "form_list: Json<Vec<Merchandise>>""#,
shop_id, shop_id,
mod_name, mod_name,
&local_form_id.to_string(), &local_form_id.to_string(),

View File

@ -9,12 +9,12 @@ pub mod owner;
pub mod shop; pub mod shop;
pub mod transaction; pub mod transaction;
pub use interior_ref_list::{InteriorRefList, PostedInteriorRefList, UnsavedInteriorRefList}; pub use interior_ref_list::{InteriorRefList, PostedInteriorRefList};
pub use merchandise_list::{MerchandiseList, PostedMerchandiseList, UnsavedMerchandiseList}; pub use merchandise_list::{MerchandiseList, PostedMerchandiseList};
pub use model::{Model, UpdateableModel}; pub use model::{Model, UpdateableModel};
pub use owner::{Owner, PostedOwner, UnsavedOwner}; pub use owner::{FullPostedOwner, Owner, PostedOwner};
pub use shop::{PostedShop, Shop, UnsavedShop}; pub use shop::{PostedShop, Shop};
pub use transaction::{PostedTransaction, Transaction, UnsavedTransaction}; pub use transaction::{PostedTransaction, Transaction};
#[derive(Debug, Eq, PartialEq, Hash, Clone, Deserialize)] #[derive(Debug, Eq, PartialEq, Hash, Clone, Deserialize)]
pub enum Order { pub enum Order {

View File

@ -24,18 +24,16 @@ pub struct Owner {
} }
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub struct UnsavedOwner { pub struct PostedOwner {
pub name: String, pub name: String,
#[serde(skip_serializing)]
pub api_key: Uuid,
#[serde(skip_serializing)]
pub ip_address: Option<IpNetwork>,
pub mod_version: i32, pub mod_version: i32,
} }
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub struct PostedOwner { pub struct FullPostedOwner {
pub name: String, pub name: String,
pub api_key: Uuid,
pub ip_address: Option<IpNetwork>,
pub mod_version: i32, pub mod_version: i32,
} }
@ -62,7 +60,7 @@ impl Owner {
#[instrument(level = "debug", skip(owner, db))] #[instrument(level = "debug", skip(owner, db))]
pub async fn create( pub async fn create(
owner: UnsavedOwner, owner: FullPostedOwner,
db: impl Executor<'_, Database = Postgres>, db: impl Executor<'_, Database = Postgres>,
) -> Result<Self> { ) -> Result<Self> {
Ok(sqlx::query_as!( Ok(sqlx::query_as!(

View File

@ -14,22 +14,23 @@ pub struct Shop {
pub name: String, pub name: String,
pub owner_id: i32, pub owner_id: i32,
pub description: Option<String>, pub description: Option<String>,
pub gold: i32,
pub shop_type: String,
pub vendor_keywords: Vec<String>,
pub vendor_keywords_exclude: bool,
pub created_at: NaiveDateTime, pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime, pub updated_at: NaiveDateTime,
} }
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct UnsavedShop {
pub name: String,
pub owner_id: i32,
pub description: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub struct PostedShop { pub struct PostedShop {
pub name: String, pub name: String,
pub owner_id: Option<i32>, pub owner_id: Option<i32>,
pub description: Option<String>, pub description: Option<String>,
pub gold: Option<i32>,
pub shop_type: Option<String>,
pub vendor_keywords: Option<Vec<String>>,
pub vendor_keywords_exclude: Option<bool>,
} }
impl Shop { impl Shop {
@ -55,18 +56,25 @@ impl Shop {
#[instrument(level = "debug", skip(shop, db))] #[instrument(level = "debug", skip(shop, db))]
pub async fn create( pub async fn create(
shop: UnsavedShop, shop: PostedShop,
db: impl Executor<'_, Database = Postgres>, db: impl Executor<'_, Database = Postgres>,
) -> Result<Self> { ) -> Result<Self> {
Ok(sqlx::query_as!( Ok(sqlx::query_as!(
Self, Self,
"INSERT INTO shops "INSERT INTO shops
(name, owner_id, description, created_at, updated_at) (name, owner_id, description, gold, shop_type, vendor_keywords,
VALUES ($1, $2, $3, now(), now()) vendor_keywords_exclude, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, now(), now())
RETURNING *", RETURNING *",
shop.name, shop.name,
shop.owner_id, shop.owner_id,
shop.description, shop.description,
shop.gold.unwrap_or(0),
shop.shop_type.unwrap_or("general_store".to_string()),
&shop
.vendor_keywords
.unwrap_or_else(|| vec!["VendorItemKey".to_string(), "VendorNoSale".to_string()]),
shop.vendor_keywords_exclude.unwrap_or(true),
) )
.fetch_one(db) .fetch_one(db)
.await?) .await?)
@ -141,6 +149,10 @@ impl Shop {
name = $2, name = $2,
owner_id = $3, owner_id = $3,
description = $4, description = $4,
gold = $5,
shop_type = $6,
vendor_keywords = $7,
vendor_keywords_exclude = $8,
updated_at = now() updated_at = now()
WHERE id = $1 WHERE id = $1
RETURNING *", RETURNING *",
@ -148,6 +160,10 @@ impl Shop {
shop.name, shop.name,
shop.owner_id, shop.owner_id,
shop.description, shop.description,
shop.gold,
shop.shop_type,
&shop.vendor_keywords.unwrap_or_else(|| vec![]),
shop.vendor_keywords_exclude,
) )
.fetch_one(db) .fetch_one(db)
.await?) .await?)
@ -155,4 +171,48 @@ impl Shop {
return Err(forbidden_permission()); return Err(forbidden_permission());
} }
} }
#[instrument(level = "debug", skip(db))]
pub async fn accepts_keywords(
db: impl Executor<'_, Database = Postgres>,
id: i32,
keywords: &[String],
) -> Result<bool> {
// Macro not available, see: https://github.com/launchbadge/sqlx/issues/428
Ok(sqlx::query_scalar(
"SELECT EXISTS (
SELECT 1 FROM shops
WHERE id = $1
AND ((
vendor_keywords_exclude = true AND
NOT vendor_keywords && $2
) OR (
vendor_keywords_exclude = false AND
vendor_keywords && $2
))
)",
)
.bind(id)
.bind(keywords)
.fetch_one(db)
.await?)
}
#[instrument(level = "debug", skip(db))]
pub async fn update_gold(
db: impl Executor<'_, Database = Postgres>,
id: i32,
gold_delta: i32,
) -> Result<()> {
sqlx::query!(
"UPDATE shops SET
gold = gold + $2
WHERE id = $1",
id,
gold_delta,
)
.execute(db)
.await?;
Ok(())
}
} }

View File

@ -22,25 +22,11 @@ pub struct Transaction {
pub is_sell: bool, pub is_sell: bool,
pub quantity: i32, pub quantity: i32,
pub amount: i32, pub amount: i32,
pub keywords: Vec<String>,
pub created_at: NaiveDateTime, pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime, pub updated_at: NaiveDateTime,
} }
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct UnsavedTransaction {
pub shop_id: i32,
pub owner_id: i32,
pub mod_name: String,
pub local_form_id: i32,
pub name: String,
pub form_type: i32,
pub is_food: bool,
pub price: i32,
pub is_sell: bool,
pub quantity: i32,
pub amount: i32,
}
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub struct PostedTransaction { pub struct PostedTransaction {
pub shop_id: i32, pub shop_id: i32,
@ -54,6 +40,7 @@ pub struct PostedTransaction {
pub is_sell: bool, pub is_sell: bool,
pub quantity: i32, pub quantity: i32,
pub amount: i32, pub amount: i32,
pub keywords: Vec<String>,
} }
impl Transaction { impl Transaction {
@ -79,15 +66,15 @@ impl Transaction {
#[instrument(level = "debug", skip(db))] #[instrument(level = "debug", skip(db))]
pub async fn create( pub async fn create(
transaction: UnsavedTransaction, transaction: PostedTransaction,
db: impl Executor<'_, Database = Postgres>, db: impl Executor<'_, Database = Postgres>,
) -> Result<Self> { ) -> Result<Self> {
Ok(sqlx::query_as!( Ok(sqlx::query_as!(
Self, Self,
"INSERT INTO transactions "INSERT INTO transactions
(shop_id, owner_id, mod_name, local_form_id, name, form_type, is_food, price, (shop_id, owner_id, mod_name, local_form_id, name, form_type, is_food, price,
is_sell, quantity, amount, created_at, updated_at) is_sell, quantity, amount, keywords, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, now(), now()) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, now(), now())
RETURNING *", RETURNING *",
transaction.shop_id, transaction.shop_id,
transaction.owner_id, transaction.owner_id,
@ -100,6 +87,7 @@ impl Transaction {
transaction.is_sell, transaction.is_sell,
transaction.quantity, transaction.quantity,
transaction.amount, transaction.amount,
&transaction.keywords,
) )
.fetch_one(db) .fetch_one(db)
.await?) .await?)

View File

@ -1,3 +1,5 @@
use std::borrow::Borrow;
use anyhow::{anyhow, Error}; use anyhow::{anyhow, Error};
use http::StatusCode; use http::StatusCode;
use http_api_problem::HttpApiProblem; use http_api_problem::HttpApiProblem;
@ -31,56 +33,140 @@ pub fn from_anyhow(error: anyhow::Error) -> HttpApiProblem {
Err(error) => error, Err(error) => error,
}; };
// TODO: should probably decentralize all this error handling to the places where they are relevant
if let Some(sqlx_error) = error.downcast_ref::<sqlx::error::Error>() { if let Some(sqlx_error) = error.downcast_ref::<sqlx::error::Error>() {
match sqlx_error { match sqlx_error {
sqlx::error::Error::RowNotFound => { sqlx::error::Error::RowNotFound => {
return HttpApiProblem::with_title_and_type_from_status(StatusCode::NOT_FOUND) return HttpApiProblem::with_title_and_type_from_status(StatusCode::NOT_FOUND)
} }
sqlx::error::Error::Database(db_error) => {
let pg_error = db_error.downcast_ref::<sqlx::postgres::PgDatabaseError>();
error!(
"Database error: {}. {}",
pg_error.message(),
pg_error.detail().unwrap_or("")
);
dbg!(&pg_error);
let code = pg_error.code();
dbg!(&code);
if let Some(constraint) = pg_error.constraint() {
dbg!(&constraint);
if code == "23503"
&& (constraint == "shops_owner_id_fkey"
|| constraint == "interior_ref_lists_owner_id_fkey"
|| constraint == "merchandise_lists_owner_id_fkey"
|| constraint == "transactions_owner_id_fkey")
{
// foreign_key_violation
return HttpApiProblem::with_title_and_type_from_status(
StatusCode::BAD_REQUEST,
)
// TODO: better message when this is triggered by a non-cascading DELETE
.set_detail("Owner does not exist");
} else if code == "23503"
&& (constraint == "interior_ref_lists_shop_id_fkey"
|| constraint == "merchandise_lists_shop_id_fkey"
|| constraint == "transactions_shop_id_fkey")
{
// foreign_key_violation
return HttpApiProblem::with_title_and_type_from_status(
StatusCode::BAD_REQUEST,
)
.set_detail("Shop does not exist");
} else if code == "23505" && constraint == "owners_api_key_key" {
// unique_violation
return HttpApiProblem::with_title_and_type_from_status(
StatusCode::BAD_REQUEST,
)
.set_detail("Owner with Api-Key already exists");
} else if code == "23505" && constraint == "owners_unique_name_and_api_key" {
// unique_violation
return HttpApiProblem::with_title_and_type_from_status(
StatusCode::BAD_REQUEST,
)
.set_detail("Duplicate owner with same name and Api-Key exists");
} else if code == "23505" && constraint == "shops_unique_name_and_owner_id" {
// unique_violation
return HttpApiProblem::with_title_and_type_from_status(
StatusCode::BAD_REQUEST,
)
.set_detail("Owner already has a shop with that name");
} else if code == "23505" && constraint == "interior_ref_lists_shop_id_key" {
// unique_violation
return HttpApiProblem::with_title_and_type_from_status(
StatusCode::BAD_REQUEST,
)
.set_detail("Interior ref list already exists for that shop");
} else if code == "23505" && constraint == "merchandise_lists_shop_id_key" {
// unique_violation
return HttpApiProblem::with_title_and_type_from_status(
StatusCode::BAD_REQUEST,
)
.set_detail("Merchandise list already exists for that shop");
} else if code == "23514" && constraint == "merchandise_quantity_gt_zero" {
return HttpApiProblem::with_title_and_type_from_status(
StatusCode::BAD_REQUEST,
)
.set_detail("Quantity of merchandise must be greater than zero");
}
}
// Might possibly link sensitive info:
// let mut problem = HttpApiProblem::with_title_and_type_from_status(
// StatusCode::INTERNAL_SERVER_ERROR,
// )
// .set_title("Database Error")
// .set_detail(format!(
// "{}. {}",
// pg_error.message(),
// pg_error.detail().unwrap_or("")
// ));
// problem
// .set_value("code".to_string(), &code.to_string())
// .unwrap();
// return problem;
}
_ => {} _ => {}
} }
} }
if let Some(pg_error) = error.downcast_ref::<sqlx::postgres::PgDatabaseError>() { if let Some(json_error) = error.downcast_ref::<serde_json::Error>() {
error!( return HttpApiProblem::with_title_and_type_from_status(StatusCode::BAD_REQUEST)
"Database error: {}. {}", .set_title("Json Body Deserialization Error")
pg_error.message(), .set_detail(format!("{}", json_error));
pg_error.detail().unwrap_or("") }
);
dbg!(&pg_error); if let Some(bincode_error) = error.downcast_ref::<bincode::Error>() {
let code = pg_error.code(); return match bincode_error.borrow() {
dbg!(&code); bincode::ErrorKind::Io(io_error) => {
if let Some(constraint) = pg_error.constraint() { HttpApiProblem::with_title_and_type_from_status(StatusCode::BAD_REQUEST)
dbg!(&constraint); .set_title("Bincode Body Deserialization Error")
if code == "23503" && constraint == "shops_owner_id_fkey" { .set_detail(format!("io error ({:?}): {}", io_error.kind(), io_error))
// foreign_key_violation
return HttpApiProblem::with_title_and_type_from_status(StatusCode::BAD_REQUEST)
.set_detail("Owner does not exist");
} else if code == "23505" && constraint == "owners_api_key_key" {
// unique_violation
return HttpApiProblem::with_title_and_type_from_status(StatusCode::BAD_REQUEST)
.set_detail("Owner with Api-Key already exists");
} else if code == "23505" && constraint == "owners_unique_name_and_api_key" {
// unique_violation
return HttpApiProblem::with_title_and_type_from_status(StatusCode::BAD_REQUEST)
.set_detail("Duplicate owner with same name and Api-Key exists");
} else if code == "23505" && constraint == "shops_unique_name_and_owner_id" {
// unique_violation
return HttpApiProblem::with_title_and_type_from_status(StatusCode::BAD_REQUEST)
.set_detail("Owner already has a shop with that name");
} else if code == "23514" && constraint == "merchandise_quantity_gt_zero" {
return HttpApiProblem::with_title_and_type_from_status(StatusCode::BAD_REQUEST)
.set_detail("Quantity of merchandise must be greater than zero");
} }
} error => HttpApiProblem::with_title_and_type_from_status(StatusCode::BAD_REQUEST)
.set_title("Bincode Body Deserialization Error")
.set_detail(format!("{}", error)),
};
} }
error!("Recovering unhandled error: {:?}", error); error!("Recovering unhandled error: {:?}", error);
// TODO: this leaks internal info, should not stringify error HttpApiProblem::with_title_and_type_from_status(StatusCode::INTERNAL_SERVER_ERROR)
HttpApiProblem::new(format!("Internal Server Error: {:?}", error))
.set_status(StatusCode::INTERNAL_SERVER_ERROR)
} }
pub async fn unpack_problem(rejection: Rejection) -> Result<impl Reply, Rejection> { pub async fn unpack_problem(rejection: Rejection) -> Result<impl Reply, Rejection> {
if rejection.is_not_found() {
let reply = warp::reply::json(&HttpApiProblem::with_title_and_type_from_status(
StatusCode::NOT_FOUND,
));
let reply = warp::reply::with_status(reply, StatusCode::NOT_FOUND);
let reply = warp::reply::with_header(
reply,
warp::http::header::CONTENT_TYPE,
http_api_problem::PROBLEM_JSON_MEDIA_TYPE,
);
return Ok(reply);
}
if let Some(problem) = rejection.find::<HttpApiProblem>() { if let Some(problem) = rejection.find::<HttpApiProblem>() {
let code = problem.status.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR); let code = problem.status.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);

View File

@ -2602,5 +2602,23 @@
"angle_z": 1.006, "angle_z": 1.006,
"scale": 1 "scale": 1
} }
],
"shelves": [
{
"shelf_type": 1,
"position_x": 1.001,
"position_y": 1.002,
"position_z": 1.003,
"angle_x": 1.004,
"angle_y": 1.005,
"angle_z": 1.006,
"scale": 1,
"page": 1,
"filter_form_type": null,
"filter_is_food": false,
"search": null,
"sort_on": null,
"sort_asc": true
}
] ]
} }

View File

@ -9,7 +9,8 @@
"quantity": 1, "quantity": 1,
"form_type": 32, "form_type": 32,
"is_food": false, "is_food": false,
"price": 1 "price": 1,
"keywords": ["VendorItemMisc"]
}, },
{ {
"mod_name": "Skyrim.esm", "mod_name": "Skyrim.esm",
@ -18,7 +19,8 @@
"quantity": 2, "quantity": 2,
"form_type": 23, "form_type": 23,
"is_food": false, "is_food": false,
"price": 2 "price": 2,
"keywords": ["VendorItemScroll"]
}, },
{ {
"mod_name": "Skyrim.esm", "mod_name": "Skyrim.esm",
@ -27,7 +29,8 @@
"quantity": 3, "quantity": 3,
"form_type": 46, "form_type": 46,
"is_food": true, "is_food": true,
"price": 3 "price": 3,
"keywords": ["VendorItemIngredient"]
}, },
{ {
"mod_name": "Skyrim.esm", "mod_name": "Skyrim.esm",
@ -36,7 +39,8 @@
"quantity": 4, "quantity": 4,
"form_type": 41, "form_type": 41,
"is_food": false, "is_food": false,
"price": 4 "price": 4,
"keywords": ["VendorItemWeapon"]
} }
] ]
} }

View File

@ -8,5 +8,6 @@
"price": 100, "price": 100,
"is_sell": false, "is_sell": false,
"quantity": 1, "quantity": 1,
"amount": 100 "amount": 100,
"keywords": ["VendorItemMisc"]
} }