├── .envrc ├── .github └── workflows │ ├── publish.yaml │ ├── release_crate.yaml │ └── test.yaml ├── .gitignore ├── .vscode └── settings.json ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── src ├── json_util.rs ├── lib.rs ├── main.rs ├── notion_client.rs ├── notion_database.rs ├── notion_pages.rs └── sqlite.rs └── tests ├── common ├── fixtures.rs ├── helpers.rs └── mod.rs ├── fixtures └── snapshot1.txt ├── integration_test.rs ├── notion_database_test.rs ├── notion_pages_test.rs └── sqlite_test.rs /.envrc: -------------------------------------------------------------------------------- 1 | # Environment variables are needed just for integration test 2 | dotenv 3 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | publish: 10 | name: Publish for ${{ matrix.os }} 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | matrix: 14 | include: 15 | - os: ubuntu-latest 16 | artifact_name: notion-into-sqlite 17 | asset_name: notion-into-sqlite-linux-amd64 18 | - os: macos-latest 19 | artifact_name: notion-into-sqlite 20 | asset_name: notion-into-sqlite-macos-amd64 21 | 22 | steps: 23 | - uses: actions/checkout@v2 24 | - name: Build 25 | run: cargo build --release --locked 26 | - name: Upload binaries to release 27 | uses: svenstaro/upload-release-action@v2 28 | with: 29 | repo_token: ${{ secrets.GITHUB_TOKEN }} 30 | file: target/release/${{ matrix.artifact_name }} 31 | asset_name: ${{ matrix.asset_name }} 32 | tag: ${{ github.ref }} 33 | -------------------------------------------------------------------------------- /.github/workflows/release_crate.yaml: -------------------------------------------------------------------------------- 1 | name: Release Crate 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | test: 10 | name: Release Crate 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: actions-rs/cargo@v1 15 | with: 16 | command: publish 17 | env: 18 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 19 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | # Thanks to https://blog.takuchalle.dev/post/2020/10/22/github_actions_for_rust/ 2 | name: test 3 | 4 | on: [push] 5 | 6 | jobs: 7 | test: 8 | name: Run Test 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - uses: actions/cache@v2 13 | with: 14 | path: | 15 | ~/.cargo/bin/ 16 | ~/.cargo/registry/index/ 17 | ~/.cargo/registry/cache/ 18 | ~/.cargo/git/db/ 19 | target/ 20 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 21 | - uses: actions-rs/toolchain@v1 22 | with: 23 | toolchain: stable 24 | components: rustfmt, clippy 25 | - uses: actions-rs/cargo@v1 26 | with: 27 | command: fmt 28 | args: --all -- --check 29 | - uses: actions-rs/cargo@v1 30 | with: 31 | command: clippy 32 | args: -- -D warnings 33 | - uses: actions-rs/cargo@v1 34 | env: 35 | NOTION_API_KEY: ${{ secrets.NOTION_API_KEY }} 36 | NOTION_DATABASE_ID: ${{ secrets.NOTION_DATABASE_ID }} 37 | with: 38 | command: test 39 | args: -- --include-ignored 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /tmp 3 | .env 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 4 3 | } 4 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "ahash" 7 | version = "0.7.6" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" 10 | dependencies = [ 11 | "getrandom", 12 | "once_cell", 13 | "version_check", 14 | ] 15 | 16 | [[package]] 17 | name = "aho-corasick" 18 | version = "0.7.18" 19 | source = "registry+https://github.com/rust-lang/crates.io-index" 20 | checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" 21 | dependencies = [ 22 | "memchr", 23 | ] 24 | 25 | [[package]] 26 | name = "anyhow" 27 | version = "1.0.56" 28 | source = "registry+https://github.com/rust-lang/crates.io-index" 29 | checksum = "4361135be9122e0870de935d7c439aef945b9f9ddd4199a553b5270b49c82a27" 30 | 31 | [[package]] 32 | name = "atty" 33 | version = "0.2.14" 34 | source = "registry+https://github.com/rust-lang/crates.io-index" 35 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 36 | dependencies = [ 37 | "hermit-abi", 38 | "libc", 39 | "winapi", 40 | ] 41 | 42 | [[package]] 43 | name = "autocfg" 44 | version = "1.1.0" 45 | source = "registry+https://github.com/rust-lang/crates.io-index" 46 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 47 | 48 | [[package]] 49 | name = "base64" 50 | version = "0.13.0" 51 | source = "registry+https://github.com/rust-lang/crates.io-index" 52 | checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" 53 | 54 | [[package]] 55 | name = "bitflags" 56 | version = "1.3.2" 57 | source = "registry+https://github.com/rust-lang/crates.io-index" 58 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 59 | 60 | [[package]] 61 | name = "bumpalo" 62 | version = "3.9.1" 63 | source = "registry+https://github.com/rust-lang/crates.io-index" 64 | checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899" 65 | 66 | [[package]] 67 | name = "bytes" 68 | version = "1.1.0" 69 | source = "registry+https://github.com/rust-lang/crates.io-index" 70 | checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" 71 | 72 | [[package]] 73 | name = "cc" 74 | version = "1.0.73" 75 | source = "registry+https://github.com/rust-lang/crates.io-index" 76 | checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" 77 | 78 | [[package]] 79 | name = "cfg-if" 80 | version = "1.0.0" 81 | source = "registry+https://github.com/rust-lang/crates.io-index" 82 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 83 | 84 | [[package]] 85 | name = "clap" 86 | version = "3.1.6" 87 | source = "registry+https://github.com/rust-lang/crates.io-index" 88 | checksum = "d8c93436c21e4698bacadf42917db28b23017027a4deccb35dbe47a7e7840123" 89 | dependencies = [ 90 | "atty", 91 | "bitflags", 92 | "clap_derive", 93 | "indexmap", 94 | "lazy_static", 95 | "os_str_bytes", 96 | "strsim", 97 | "termcolor", 98 | "textwrap", 99 | ] 100 | 101 | [[package]] 102 | name = "clap_derive" 103 | version = "3.1.4" 104 | source = "registry+https://github.com/rust-lang/crates.io-index" 105 | checksum = "da95d038ede1a964ce99f49cbe27a7fb538d1da595e4b4f70b8c8f338d17bf16" 106 | dependencies = [ 107 | "heck", 108 | "proc-macro-error", 109 | "proc-macro2", 110 | "quote", 111 | "syn", 112 | ] 113 | 114 | [[package]] 115 | name = "core-foundation" 116 | version = "0.9.3" 117 | source = "registry+https://github.com/rust-lang/crates.io-index" 118 | checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" 119 | dependencies = [ 120 | "core-foundation-sys", 121 | "libc", 122 | ] 123 | 124 | [[package]] 125 | name = "core-foundation-sys" 126 | version = "0.8.3" 127 | source = "registry+https://github.com/rust-lang/crates.io-index" 128 | checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" 129 | 130 | [[package]] 131 | name = "encoding_rs" 132 | version = "0.8.30" 133 | source = "registry+https://github.com/rust-lang/crates.io-index" 134 | checksum = "7896dc8abb250ffdda33912550faa54c88ec8b998dec0b2c55ab224921ce11df" 135 | dependencies = [ 136 | "cfg-if", 137 | ] 138 | 139 | [[package]] 140 | name = "env_logger" 141 | version = "0.9.0" 142 | source = "registry+https://github.com/rust-lang/crates.io-index" 143 | checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3" 144 | dependencies = [ 145 | "atty", 146 | "humantime", 147 | "log", 148 | "regex", 149 | "termcolor", 150 | ] 151 | 152 | [[package]] 153 | name = "fallible-iterator" 154 | version = "0.2.0" 155 | source = "registry+https://github.com/rust-lang/crates.io-index" 156 | checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" 157 | 158 | [[package]] 159 | name = "fallible-streaming-iterator" 160 | version = "0.1.9" 161 | source = "registry+https://github.com/rust-lang/crates.io-index" 162 | checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" 163 | 164 | [[package]] 165 | name = "fastrand" 166 | version = "1.7.0" 167 | source = "registry+https://github.com/rust-lang/crates.io-index" 168 | checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf" 169 | dependencies = [ 170 | "instant", 171 | ] 172 | 173 | [[package]] 174 | name = "fnv" 175 | version = "1.0.7" 176 | source = "registry+https://github.com/rust-lang/crates.io-index" 177 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 178 | 179 | [[package]] 180 | name = "foreign-types" 181 | version = "0.3.2" 182 | source = "registry+https://github.com/rust-lang/crates.io-index" 183 | checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" 184 | dependencies = [ 185 | "foreign-types-shared", 186 | ] 187 | 188 | [[package]] 189 | name = "foreign-types-shared" 190 | version = "0.1.1" 191 | source = "registry+https://github.com/rust-lang/crates.io-index" 192 | checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" 193 | 194 | [[package]] 195 | name = "form_urlencoded" 196 | version = "1.0.1" 197 | source = "registry+https://github.com/rust-lang/crates.io-index" 198 | checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" 199 | dependencies = [ 200 | "matches", 201 | "percent-encoding", 202 | ] 203 | 204 | [[package]] 205 | name = "futures-channel" 206 | version = "0.3.21" 207 | source = "registry+https://github.com/rust-lang/crates.io-index" 208 | checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010" 209 | dependencies = [ 210 | "futures-core", 211 | ] 212 | 213 | [[package]] 214 | name = "futures-core" 215 | version = "0.3.21" 216 | source = "registry+https://github.com/rust-lang/crates.io-index" 217 | checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" 218 | 219 | [[package]] 220 | name = "futures-io" 221 | version = "0.3.21" 222 | source = "registry+https://github.com/rust-lang/crates.io-index" 223 | checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b" 224 | 225 | [[package]] 226 | name = "futures-sink" 227 | version = "0.3.21" 228 | source = "registry+https://github.com/rust-lang/crates.io-index" 229 | checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868" 230 | 231 | [[package]] 232 | name = "futures-task" 233 | version = "0.3.21" 234 | source = "registry+https://github.com/rust-lang/crates.io-index" 235 | checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a" 236 | 237 | [[package]] 238 | name = "futures-util" 239 | version = "0.3.21" 240 | source = "registry+https://github.com/rust-lang/crates.io-index" 241 | checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a" 242 | dependencies = [ 243 | "futures-core", 244 | "futures-io", 245 | "futures-task", 246 | "memchr", 247 | "pin-project-lite", 248 | "pin-utils", 249 | "slab", 250 | ] 251 | 252 | [[package]] 253 | name = "getrandom" 254 | version = "0.2.5" 255 | source = "registry+https://github.com/rust-lang/crates.io-index" 256 | checksum = "d39cd93900197114fa1fcb7ae84ca742095eed9442088988ae74fa744e930e77" 257 | dependencies = [ 258 | "cfg-if", 259 | "libc", 260 | "wasi 0.10.2+wasi-snapshot-preview1", 261 | ] 262 | 263 | [[package]] 264 | name = "h2" 265 | version = "0.3.12" 266 | source = "registry+https://github.com/rust-lang/crates.io-index" 267 | checksum = "62eeb471aa3e3c9197aa4bfeabfe02982f6dc96f750486c0bb0009ac58b26d2b" 268 | dependencies = [ 269 | "bytes", 270 | "fnv", 271 | "futures-core", 272 | "futures-sink", 273 | "futures-util", 274 | "http", 275 | "indexmap", 276 | "slab", 277 | "tokio", 278 | "tokio-util", 279 | "tracing", 280 | ] 281 | 282 | [[package]] 283 | name = "hashbrown" 284 | version = "0.11.2" 285 | source = "registry+https://github.com/rust-lang/crates.io-index" 286 | checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" 287 | dependencies = [ 288 | "ahash", 289 | ] 290 | 291 | [[package]] 292 | name = "hashlink" 293 | version = "0.7.0" 294 | source = "registry+https://github.com/rust-lang/crates.io-index" 295 | checksum = "7249a3129cbc1ffccd74857f81464a323a152173cdb134e0fd81bc803b29facf" 296 | dependencies = [ 297 | "hashbrown", 298 | ] 299 | 300 | [[package]] 301 | name = "heck" 302 | version = "0.4.0" 303 | source = "registry+https://github.com/rust-lang/crates.io-index" 304 | checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" 305 | 306 | [[package]] 307 | name = "hermit-abi" 308 | version = "0.1.19" 309 | source = "registry+https://github.com/rust-lang/crates.io-index" 310 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 311 | dependencies = [ 312 | "libc", 313 | ] 314 | 315 | [[package]] 316 | name = "http" 317 | version = "0.2.6" 318 | source = "registry+https://github.com/rust-lang/crates.io-index" 319 | checksum = "31f4c6746584866f0feabcc69893c5b51beef3831656a968ed7ae254cdc4fd03" 320 | dependencies = [ 321 | "bytes", 322 | "fnv", 323 | "itoa", 324 | ] 325 | 326 | [[package]] 327 | name = "http-body" 328 | version = "0.4.4" 329 | source = "registry+https://github.com/rust-lang/crates.io-index" 330 | checksum = "1ff4f84919677303da5f147645dbea6b1881f368d03ac84e1dc09031ebd7b2c6" 331 | dependencies = [ 332 | "bytes", 333 | "http", 334 | "pin-project-lite", 335 | ] 336 | 337 | [[package]] 338 | name = "httparse" 339 | version = "1.6.0" 340 | source = "registry+https://github.com/rust-lang/crates.io-index" 341 | checksum = "9100414882e15fb7feccb4897e5f0ff0ff1ca7d1a86a23208ada4d7a18e6c6c4" 342 | 343 | [[package]] 344 | name = "httpdate" 345 | version = "1.0.2" 346 | source = "registry+https://github.com/rust-lang/crates.io-index" 347 | checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" 348 | 349 | [[package]] 350 | name = "humantime" 351 | version = "2.1.0" 352 | source = "registry+https://github.com/rust-lang/crates.io-index" 353 | checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" 354 | 355 | [[package]] 356 | name = "hyper" 357 | version = "0.14.17" 358 | source = "registry+https://github.com/rust-lang/crates.io-index" 359 | checksum = "043f0e083e9901b6cc658a77d1eb86f4fc650bbb977a4337dd63192826aa85dd" 360 | dependencies = [ 361 | "bytes", 362 | "futures-channel", 363 | "futures-core", 364 | "futures-util", 365 | "h2", 366 | "http", 367 | "http-body", 368 | "httparse", 369 | "httpdate", 370 | "itoa", 371 | "pin-project-lite", 372 | "socket2", 373 | "tokio", 374 | "tower-service", 375 | "tracing", 376 | "want", 377 | ] 378 | 379 | [[package]] 380 | name = "hyper-tls" 381 | version = "0.5.0" 382 | source = "registry+https://github.com/rust-lang/crates.io-index" 383 | checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" 384 | dependencies = [ 385 | "bytes", 386 | "hyper", 387 | "native-tls", 388 | "tokio", 389 | "tokio-native-tls", 390 | ] 391 | 392 | [[package]] 393 | name = "idna" 394 | version = "0.2.3" 395 | source = "registry+https://github.com/rust-lang/crates.io-index" 396 | checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" 397 | dependencies = [ 398 | "matches", 399 | "unicode-bidi", 400 | "unicode-normalization", 401 | ] 402 | 403 | [[package]] 404 | name = "indexmap" 405 | version = "1.8.0" 406 | source = "registry+https://github.com/rust-lang/crates.io-index" 407 | checksum = "282a6247722caba404c065016bbfa522806e51714c34f5dfc3e4a3a46fcb4223" 408 | dependencies = [ 409 | "autocfg", 410 | "hashbrown", 411 | ] 412 | 413 | [[package]] 414 | name = "instant" 415 | version = "0.1.12" 416 | source = "registry+https://github.com/rust-lang/crates.io-index" 417 | checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" 418 | dependencies = [ 419 | "cfg-if", 420 | ] 421 | 422 | [[package]] 423 | name = "ipnet" 424 | version = "2.4.0" 425 | source = "registry+https://github.com/rust-lang/crates.io-index" 426 | checksum = "35e70ee094dc02fd9c13fdad4940090f22dbd6ac7c9e7094a46cf0232a50bc7c" 427 | 428 | [[package]] 429 | name = "itoa" 430 | version = "1.0.1" 431 | source = "registry+https://github.com/rust-lang/crates.io-index" 432 | checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" 433 | 434 | [[package]] 435 | name = "js-sys" 436 | version = "0.3.56" 437 | source = "registry+https://github.com/rust-lang/crates.io-index" 438 | checksum = "a38fc24e30fd564ce974c02bf1d337caddff65be6cc4735a1f7eab22a7440f04" 439 | dependencies = [ 440 | "wasm-bindgen", 441 | ] 442 | 443 | [[package]] 444 | name = "lazy_static" 445 | version = "1.4.0" 446 | source = "registry+https://github.com/rust-lang/crates.io-index" 447 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 448 | 449 | [[package]] 450 | name = "libc" 451 | version = "0.2.119" 452 | source = "registry+https://github.com/rust-lang/crates.io-index" 453 | checksum = "1bf2e165bb3457c8e098ea76f3e3bc9db55f87aa90d52d0e6be741470916aaa4" 454 | 455 | [[package]] 456 | name = "libsqlite3-sys" 457 | version = "0.24.1" 458 | source = "registry+https://github.com/rust-lang/crates.io-index" 459 | checksum = "cb644c388dfaefa18035c12614156d285364769e818893da0dda9030c80ad2ba" 460 | dependencies = [ 461 | "pkg-config", 462 | "vcpkg", 463 | ] 464 | 465 | [[package]] 466 | name = "log" 467 | version = "0.4.14" 468 | source = "registry+https://github.com/rust-lang/crates.io-index" 469 | checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" 470 | dependencies = [ 471 | "cfg-if", 472 | ] 473 | 474 | [[package]] 475 | name = "matches" 476 | version = "0.1.9" 477 | source = "registry+https://github.com/rust-lang/crates.io-index" 478 | checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" 479 | 480 | [[package]] 481 | name = "memchr" 482 | version = "2.4.1" 483 | source = "registry+https://github.com/rust-lang/crates.io-index" 484 | checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" 485 | 486 | [[package]] 487 | name = "mime" 488 | version = "0.3.16" 489 | source = "registry+https://github.com/rust-lang/crates.io-index" 490 | checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" 491 | 492 | [[package]] 493 | name = "mio" 494 | version = "0.8.1" 495 | source = "registry+https://github.com/rust-lang/crates.io-index" 496 | checksum = "7ba42135c6a5917b9db9cd7b293e5409e1c6b041e6f9825e92e55a894c63b6f8" 497 | dependencies = [ 498 | "libc", 499 | "log", 500 | "miow", 501 | "ntapi", 502 | "wasi 0.11.0+wasi-snapshot-preview1", 503 | "winapi", 504 | ] 505 | 506 | [[package]] 507 | name = "miow" 508 | version = "0.3.7" 509 | source = "registry+https://github.com/rust-lang/crates.io-index" 510 | checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" 511 | dependencies = [ 512 | "winapi", 513 | ] 514 | 515 | [[package]] 516 | name = "native-tls" 517 | version = "0.2.8" 518 | source = "registry+https://github.com/rust-lang/crates.io-index" 519 | checksum = "48ba9f7719b5a0f42f338907614285fb5fd70e53858141f69898a1fb7203b24d" 520 | dependencies = [ 521 | "lazy_static", 522 | "libc", 523 | "log", 524 | "openssl", 525 | "openssl-probe", 526 | "openssl-sys", 527 | "schannel", 528 | "security-framework", 529 | "security-framework-sys", 530 | "tempfile", 531 | ] 532 | 533 | [[package]] 534 | name = "notion-into-sqlite" 535 | version = "0.1.2" 536 | dependencies = [ 537 | "anyhow", 538 | "clap", 539 | "env_logger", 540 | "log", 541 | "regex", 542 | "reqwest", 543 | "rusqlite", 544 | "serde", 545 | "serde_json", 546 | ] 547 | 548 | [[package]] 549 | name = "ntapi" 550 | version = "0.3.7" 551 | source = "registry+https://github.com/rust-lang/crates.io-index" 552 | checksum = "c28774a7fd2fbb4f0babd8237ce554b73af68021b5f695a3cebd6c59bac0980f" 553 | dependencies = [ 554 | "winapi", 555 | ] 556 | 557 | [[package]] 558 | name = "num_cpus" 559 | version = "1.13.1" 560 | source = "registry+https://github.com/rust-lang/crates.io-index" 561 | checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" 562 | dependencies = [ 563 | "hermit-abi", 564 | "libc", 565 | ] 566 | 567 | [[package]] 568 | name = "once_cell" 569 | version = "1.10.0" 570 | source = "registry+https://github.com/rust-lang/crates.io-index" 571 | checksum = "87f3e037eac156d1775da914196f0f37741a274155e34a0b7e427c35d2a2ecb9" 572 | 573 | [[package]] 574 | name = "openssl" 575 | version = "0.10.38" 576 | source = "registry+https://github.com/rust-lang/crates.io-index" 577 | checksum = "0c7ae222234c30df141154f159066c5093ff73b63204dcda7121eb082fc56a95" 578 | dependencies = [ 579 | "bitflags", 580 | "cfg-if", 581 | "foreign-types", 582 | "libc", 583 | "once_cell", 584 | "openssl-sys", 585 | ] 586 | 587 | [[package]] 588 | name = "openssl-probe" 589 | version = "0.1.5" 590 | source = "registry+https://github.com/rust-lang/crates.io-index" 591 | checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" 592 | 593 | [[package]] 594 | name = "openssl-sys" 595 | version = "0.9.72" 596 | source = "registry+https://github.com/rust-lang/crates.io-index" 597 | checksum = "7e46109c383602735fa0a2e48dd2b7c892b048e1bf69e5c3b1d804b7d9c203cb" 598 | dependencies = [ 599 | "autocfg", 600 | "cc", 601 | "libc", 602 | "pkg-config", 603 | "vcpkg", 604 | ] 605 | 606 | [[package]] 607 | name = "os_str_bytes" 608 | version = "6.0.0" 609 | source = "registry+https://github.com/rust-lang/crates.io-index" 610 | checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64" 611 | dependencies = [ 612 | "memchr", 613 | ] 614 | 615 | [[package]] 616 | name = "percent-encoding" 617 | version = "2.1.0" 618 | source = "registry+https://github.com/rust-lang/crates.io-index" 619 | checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" 620 | 621 | [[package]] 622 | name = "pin-project-lite" 623 | version = "0.2.8" 624 | source = "registry+https://github.com/rust-lang/crates.io-index" 625 | checksum = "e280fbe77cc62c91527259e9442153f4688736748d24660126286329742b4c6c" 626 | 627 | [[package]] 628 | name = "pin-utils" 629 | version = "0.1.0" 630 | source = "registry+https://github.com/rust-lang/crates.io-index" 631 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 632 | 633 | [[package]] 634 | name = "pkg-config" 635 | version = "0.3.24" 636 | source = "registry+https://github.com/rust-lang/crates.io-index" 637 | checksum = "58893f751c9b0412871a09abd62ecd2a00298c6c83befa223ef98c52aef40cbe" 638 | 639 | [[package]] 640 | name = "proc-macro-error" 641 | version = "1.0.4" 642 | source = "registry+https://github.com/rust-lang/crates.io-index" 643 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" 644 | dependencies = [ 645 | "proc-macro-error-attr", 646 | "proc-macro2", 647 | "quote", 648 | "syn", 649 | "version_check", 650 | ] 651 | 652 | [[package]] 653 | name = "proc-macro-error-attr" 654 | version = "1.0.4" 655 | source = "registry+https://github.com/rust-lang/crates.io-index" 656 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 657 | dependencies = [ 658 | "proc-macro2", 659 | "quote", 660 | "version_check", 661 | ] 662 | 663 | [[package]] 664 | name = "proc-macro2" 665 | version = "1.0.36" 666 | source = "registry+https://github.com/rust-lang/crates.io-index" 667 | checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029" 668 | dependencies = [ 669 | "unicode-xid", 670 | ] 671 | 672 | [[package]] 673 | name = "quote" 674 | version = "1.0.15" 675 | source = "registry+https://github.com/rust-lang/crates.io-index" 676 | checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145" 677 | dependencies = [ 678 | "proc-macro2", 679 | ] 680 | 681 | [[package]] 682 | name = "redox_syscall" 683 | version = "0.2.11" 684 | source = "registry+https://github.com/rust-lang/crates.io-index" 685 | checksum = "8380fe0152551244f0747b1bf41737e0f8a74f97a14ccefd1148187271634f3c" 686 | dependencies = [ 687 | "bitflags", 688 | ] 689 | 690 | [[package]] 691 | name = "regex" 692 | version = "1.5.5" 693 | source = "registry+https://github.com/rust-lang/crates.io-index" 694 | checksum = "1a11647b6b25ff05a515cb92c365cec08801e83423a235b51e231e1808747286" 695 | dependencies = [ 696 | "aho-corasick", 697 | "memchr", 698 | "regex-syntax", 699 | ] 700 | 701 | [[package]] 702 | name = "regex-syntax" 703 | version = "0.6.25" 704 | source = "registry+https://github.com/rust-lang/crates.io-index" 705 | checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" 706 | 707 | [[package]] 708 | name = "remove_dir_all" 709 | version = "0.5.3" 710 | source = "registry+https://github.com/rust-lang/crates.io-index" 711 | checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" 712 | dependencies = [ 713 | "winapi", 714 | ] 715 | 716 | [[package]] 717 | name = "reqwest" 718 | version = "0.11.9" 719 | source = "registry+https://github.com/rust-lang/crates.io-index" 720 | checksum = "87f242f1488a539a79bac6dbe7c8609ae43b7914b7736210f239a37cccb32525" 721 | dependencies = [ 722 | "base64", 723 | "bytes", 724 | "encoding_rs", 725 | "futures-core", 726 | "futures-util", 727 | "h2", 728 | "http", 729 | "http-body", 730 | "hyper", 731 | "hyper-tls", 732 | "ipnet", 733 | "js-sys", 734 | "lazy_static", 735 | "log", 736 | "mime", 737 | "native-tls", 738 | "percent-encoding", 739 | "pin-project-lite", 740 | "serde", 741 | "serde_json", 742 | "serde_urlencoded", 743 | "tokio", 744 | "tokio-native-tls", 745 | "url", 746 | "wasm-bindgen", 747 | "wasm-bindgen-futures", 748 | "web-sys", 749 | "winreg", 750 | ] 751 | 752 | [[package]] 753 | name = "rusqlite" 754 | version = "0.27.0" 755 | source = "registry+https://github.com/rust-lang/crates.io-index" 756 | checksum = "85127183a999f7db96d1a976a309eebbfb6ea3b0b400ddd8340190129de6eb7a" 757 | dependencies = [ 758 | "bitflags", 759 | "fallible-iterator", 760 | "fallible-streaming-iterator", 761 | "hashlink", 762 | "libsqlite3-sys", 763 | "memchr", 764 | "smallvec", 765 | ] 766 | 767 | [[package]] 768 | name = "ryu" 769 | version = "1.0.9" 770 | source = "registry+https://github.com/rust-lang/crates.io-index" 771 | checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" 772 | 773 | [[package]] 774 | name = "schannel" 775 | version = "0.1.19" 776 | source = "registry+https://github.com/rust-lang/crates.io-index" 777 | checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75" 778 | dependencies = [ 779 | "lazy_static", 780 | "winapi", 781 | ] 782 | 783 | [[package]] 784 | name = "security-framework" 785 | version = "2.6.1" 786 | source = "registry+https://github.com/rust-lang/crates.io-index" 787 | checksum = "2dc14f172faf8a0194a3aded622712b0de276821addc574fa54fc0a1167e10dc" 788 | dependencies = [ 789 | "bitflags", 790 | "core-foundation", 791 | "core-foundation-sys", 792 | "libc", 793 | "security-framework-sys", 794 | ] 795 | 796 | [[package]] 797 | name = "security-framework-sys" 798 | version = "2.6.1" 799 | source = "registry+https://github.com/rust-lang/crates.io-index" 800 | checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556" 801 | dependencies = [ 802 | "core-foundation-sys", 803 | "libc", 804 | ] 805 | 806 | [[package]] 807 | name = "serde" 808 | version = "1.0.136" 809 | source = "registry+https://github.com/rust-lang/crates.io-index" 810 | checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789" 811 | 812 | [[package]] 813 | name = "serde_json" 814 | version = "1.0.79" 815 | source = "registry+https://github.com/rust-lang/crates.io-index" 816 | checksum = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95" 817 | dependencies = [ 818 | "itoa", 819 | "ryu", 820 | "serde", 821 | ] 822 | 823 | [[package]] 824 | name = "serde_urlencoded" 825 | version = "0.7.1" 826 | source = "registry+https://github.com/rust-lang/crates.io-index" 827 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 828 | dependencies = [ 829 | "form_urlencoded", 830 | "itoa", 831 | "ryu", 832 | "serde", 833 | ] 834 | 835 | [[package]] 836 | name = "slab" 837 | version = "0.4.5" 838 | source = "registry+https://github.com/rust-lang/crates.io-index" 839 | checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5" 840 | 841 | [[package]] 842 | name = "smallvec" 843 | version = "1.8.0" 844 | source = "registry+https://github.com/rust-lang/crates.io-index" 845 | checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" 846 | 847 | [[package]] 848 | name = "socket2" 849 | version = "0.4.4" 850 | source = "registry+https://github.com/rust-lang/crates.io-index" 851 | checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" 852 | dependencies = [ 853 | "libc", 854 | "winapi", 855 | ] 856 | 857 | [[package]] 858 | name = "strsim" 859 | version = "0.10.0" 860 | source = "registry+https://github.com/rust-lang/crates.io-index" 861 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 862 | 863 | [[package]] 864 | name = "syn" 865 | version = "1.0.86" 866 | source = "registry+https://github.com/rust-lang/crates.io-index" 867 | checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b" 868 | dependencies = [ 869 | "proc-macro2", 870 | "quote", 871 | "unicode-xid", 872 | ] 873 | 874 | [[package]] 875 | name = "tempfile" 876 | version = "3.3.0" 877 | source = "registry+https://github.com/rust-lang/crates.io-index" 878 | checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" 879 | dependencies = [ 880 | "cfg-if", 881 | "fastrand", 882 | "libc", 883 | "redox_syscall", 884 | "remove_dir_all", 885 | "winapi", 886 | ] 887 | 888 | [[package]] 889 | name = "termcolor" 890 | version = "1.1.3" 891 | source = "registry+https://github.com/rust-lang/crates.io-index" 892 | checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" 893 | dependencies = [ 894 | "winapi-util", 895 | ] 896 | 897 | [[package]] 898 | name = "textwrap" 899 | version = "0.15.0" 900 | source = "registry+https://github.com/rust-lang/crates.io-index" 901 | checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" 902 | 903 | [[package]] 904 | name = "tinyvec" 905 | version = "1.5.1" 906 | source = "registry+https://github.com/rust-lang/crates.io-index" 907 | checksum = "2c1c1d5a42b6245520c249549ec267180beaffcc0615401ac8e31853d4b6d8d2" 908 | dependencies = [ 909 | "tinyvec_macros", 910 | ] 911 | 912 | [[package]] 913 | name = "tinyvec_macros" 914 | version = "0.1.0" 915 | source = "registry+https://github.com/rust-lang/crates.io-index" 916 | checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" 917 | 918 | [[package]] 919 | name = "tokio" 920 | version = "1.17.0" 921 | source = "registry+https://github.com/rust-lang/crates.io-index" 922 | checksum = "2af73ac49756f3f7c01172e34a23e5d0216f6c32333757c2c61feb2bbff5a5ee" 923 | dependencies = [ 924 | "bytes", 925 | "libc", 926 | "memchr", 927 | "mio", 928 | "num_cpus", 929 | "pin-project-lite", 930 | "socket2", 931 | "winapi", 932 | ] 933 | 934 | [[package]] 935 | name = "tokio-native-tls" 936 | version = "0.3.0" 937 | source = "registry+https://github.com/rust-lang/crates.io-index" 938 | checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b" 939 | dependencies = [ 940 | "native-tls", 941 | "tokio", 942 | ] 943 | 944 | [[package]] 945 | name = "tokio-util" 946 | version = "0.6.9" 947 | source = "registry+https://github.com/rust-lang/crates.io-index" 948 | checksum = "9e99e1983e5d376cd8eb4b66604d2e99e79f5bd988c3055891dcd8c9e2604cc0" 949 | dependencies = [ 950 | "bytes", 951 | "futures-core", 952 | "futures-sink", 953 | "log", 954 | "pin-project-lite", 955 | "tokio", 956 | ] 957 | 958 | [[package]] 959 | name = "tower-service" 960 | version = "0.3.1" 961 | source = "registry+https://github.com/rust-lang/crates.io-index" 962 | checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" 963 | 964 | [[package]] 965 | name = "tracing" 966 | version = "0.1.32" 967 | source = "registry+https://github.com/rust-lang/crates.io-index" 968 | checksum = "4a1bdf54a7c28a2bbf701e1d2233f6c77f473486b94bee4f9678da5a148dca7f" 969 | dependencies = [ 970 | "cfg-if", 971 | "pin-project-lite", 972 | "tracing-core", 973 | ] 974 | 975 | [[package]] 976 | name = "tracing-core" 977 | version = "0.1.23" 978 | source = "registry+https://github.com/rust-lang/crates.io-index" 979 | checksum = "aa31669fa42c09c34d94d8165dd2012e8ff3c66aca50f3bb226b68f216f2706c" 980 | dependencies = [ 981 | "lazy_static", 982 | ] 983 | 984 | [[package]] 985 | name = "try-lock" 986 | version = "0.2.3" 987 | source = "registry+https://github.com/rust-lang/crates.io-index" 988 | checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" 989 | 990 | [[package]] 991 | name = "unicode-bidi" 992 | version = "0.3.7" 993 | source = "registry+https://github.com/rust-lang/crates.io-index" 994 | checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f" 995 | 996 | [[package]] 997 | name = "unicode-normalization" 998 | version = "0.1.19" 999 | source = "registry+https://github.com/rust-lang/crates.io-index" 1000 | checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" 1001 | dependencies = [ 1002 | "tinyvec", 1003 | ] 1004 | 1005 | [[package]] 1006 | name = "unicode-xid" 1007 | version = "0.2.2" 1008 | source = "registry+https://github.com/rust-lang/crates.io-index" 1009 | checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" 1010 | 1011 | [[package]] 1012 | name = "url" 1013 | version = "2.2.2" 1014 | source = "registry+https://github.com/rust-lang/crates.io-index" 1015 | checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" 1016 | dependencies = [ 1017 | "form_urlencoded", 1018 | "idna", 1019 | "matches", 1020 | "percent-encoding", 1021 | ] 1022 | 1023 | [[package]] 1024 | name = "vcpkg" 1025 | version = "0.2.15" 1026 | source = "registry+https://github.com/rust-lang/crates.io-index" 1027 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 1028 | 1029 | [[package]] 1030 | name = "version_check" 1031 | version = "0.9.4" 1032 | source = "registry+https://github.com/rust-lang/crates.io-index" 1033 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 1034 | 1035 | [[package]] 1036 | name = "want" 1037 | version = "0.3.0" 1038 | source = "registry+https://github.com/rust-lang/crates.io-index" 1039 | checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" 1040 | dependencies = [ 1041 | "log", 1042 | "try-lock", 1043 | ] 1044 | 1045 | [[package]] 1046 | name = "wasi" 1047 | version = "0.10.2+wasi-snapshot-preview1" 1048 | source = "registry+https://github.com/rust-lang/crates.io-index" 1049 | checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" 1050 | 1051 | [[package]] 1052 | name = "wasi" 1053 | version = "0.11.0+wasi-snapshot-preview1" 1054 | source = "registry+https://github.com/rust-lang/crates.io-index" 1055 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1056 | 1057 | [[package]] 1058 | name = "wasm-bindgen" 1059 | version = "0.2.79" 1060 | source = "registry+https://github.com/rust-lang/crates.io-index" 1061 | checksum = "25f1af7423d8588a3d840681122e72e6a24ddbcb3f0ec385cac0d12d24256c06" 1062 | dependencies = [ 1063 | "cfg-if", 1064 | "wasm-bindgen-macro", 1065 | ] 1066 | 1067 | [[package]] 1068 | name = "wasm-bindgen-backend" 1069 | version = "0.2.79" 1070 | source = "registry+https://github.com/rust-lang/crates.io-index" 1071 | checksum = "8b21c0df030f5a177f3cba22e9bc4322695ec43e7257d865302900290bcdedca" 1072 | dependencies = [ 1073 | "bumpalo", 1074 | "lazy_static", 1075 | "log", 1076 | "proc-macro2", 1077 | "quote", 1078 | "syn", 1079 | "wasm-bindgen-shared", 1080 | ] 1081 | 1082 | [[package]] 1083 | name = "wasm-bindgen-futures" 1084 | version = "0.4.29" 1085 | source = "registry+https://github.com/rust-lang/crates.io-index" 1086 | checksum = "2eb6ec270a31b1d3c7e266b999739109abce8b6c87e4b31fcfcd788b65267395" 1087 | dependencies = [ 1088 | "cfg-if", 1089 | "js-sys", 1090 | "wasm-bindgen", 1091 | "web-sys", 1092 | ] 1093 | 1094 | [[package]] 1095 | name = "wasm-bindgen-macro" 1096 | version = "0.2.79" 1097 | source = "registry+https://github.com/rust-lang/crates.io-index" 1098 | checksum = "2f4203d69e40a52ee523b2529a773d5ffc1dc0071801c87b3d270b471b80ed01" 1099 | dependencies = [ 1100 | "quote", 1101 | "wasm-bindgen-macro-support", 1102 | ] 1103 | 1104 | [[package]] 1105 | name = "wasm-bindgen-macro-support" 1106 | version = "0.2.79" 1107 | source = "registry+https://github.com/rust-lang/crates.io-index" 1108 | checksum = "bfa8a30d46208db204854cadbb5d4baf5fcf8071ba5bf48190c3e59937962ebc" 1109 | dependencies = [ 1110 | "proc-macro2", 1111 | "quote", 1112 | "syn", 1113 | "wasm-bindgen-backend", 1114 | "wasm-bindgen-shared", 1115 | ] 1116 | 1117 | [[package]] 1118 | name = "wasm-bindgen-shared" 1119 | version = "0.2.79" 1120 | source = "registry+https://github.com/rust-lang/crates.io-index" 1121 | checksum = "3d958d035c4438e28c70e4321a2911302f10135ce78a9c7834c0cab4123d06a2" 1122 | 1123 | [[package]] 1124 | name = "web-sys" 1125 | version = "0.3.56" 1126 | source = "registry+https://github.com/rust-lang/crates.io-index" 1127 | checksum = "c060b319f29dd25724f09a2ba1418f142f539b2be99fbf4d2d5a8f7330afb8eb" 1128 | dependencies = [ 1129 | "js-sys", 1130 | "wasm-bindgen", 1131 | ] 1132 | 1133 | [[package]] 1134 | name = "winapi" 1135 | version = "0.3.9" 1136 | source = "registry+https://github.com/rust-lang/crates.io-index" 1137 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1138 | dependencies = [ 1139 | "winapi-i686-pc-windows-gnu", 1140 | "winapi-x86_64-pc-windows-gnu", 1141 | ] 1142 | 1143 | [[package]] 1144 | name = "winapi-i686-pc-windows-gnu" 1145 | version = "0.4.0" 1146 | source = "registry+https://github.com/rust-lang/crates.io-index" 1147 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1148 | 1149 | [[package]] 1150 | name = "winapi-util" 1151 | version = "0.1.5" 1152 | source = "registry+https://github.com/rust-lang/crates.io-index" 1153 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 1154 | dependencies = [ 1155 | "winapi", 1156 | ] 1157 | 1158 | [[package]] 1159 | name = "winapi-x86_64-pc-windows-gnu" 1160 | version = "0.4.0" 1161 | source = "registry+https://github.com/rust-lang/crates.io-index" 1162 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1163 | 1164 | [[package]] 1165 | name = "winreg" 1166 | version = "0.7.0" 1167 | source = "registry+https://github.com/rust-lang/crates.io-index" 1168 | checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69" 1169 | dependencies = [ 1170 | "winapi", 1171 | ] 1172 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "notion-into-sqlite" 3 | version = "0.1.2" 4 | description = "Download your Notion's database and save it locally into SQLite" 5 | edition = "2021" 6 | license-file = "LICENSE" 7 | repository = "https://github.com/FujiHaruka/notion-into-sqlite" 8 | keywords = ["Notion", "SQLite"] 9 | include = [ 10 | "src/*.rs", 11 | "Cargo.toml", 12 | ] 13 | 14 | [dependencies] 15 | reqwest = { version = "0.11", features = ["blocking", "json"] } 16 | 17 | serde = "1.0.136" 18 | serde_json = "1.0" 19 | 20 | log = "0.4.0" 21 | env_logger = "0.9.0" 22 | 23 | rusqlite = "0.27.0" 24 | 25 | clap = { version = "3.1.6", features = ["derive"] } 26 | anyhow = "1.0.56" 27 | 28 | [dev-dependencies] 29 | regex = "1.5" 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Fuji Haruka 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # notion-into-sqlite 2 | 3 | A command line tool to download your Notion's database and save it locally into SQLite. 4 | 5 | ## Usage 6 | 7 | You need Notion API key and database ID you want to download. 8 | 9 | ``` 10 | notion-into-sqlite --api-key --database-id 11 | ``` 12 | 13 | For more detail, `$ notion-into-sqlite --help` shows available options. 14 | 15 | ## Installation 16 | 17 | Using [Eget](https://github.com/zyedidia/eget), which enables you to easiliy get pre-built binaries, is the most quick way to install. 18 | 19 | ``` 20 | eget FujiHaruka/notion-into-sqlite 21 | ``` 22 | 23 | You can directly download binaries from [Releases](https://github.com/FujiHaruka/notion-into-sqlite/releases/). 24 | 25 | Or you can install via cargo. 26 | 27 | ``` 28 | cargo install notion-into-sqlite 29 | ``` 30 | 31 | ## Development 32 | 33 | Release is managed by GitHub Actions. When a new tag is created, workflows for release will be triggered. Cross pre-built binaries will be uploaded to the release page, and a new version of crate will be released. 34 | -------------------------------------------------------------------------------- /src/json_util.rs: -------------------------------------------------------------------------------- 1 | use serde_json::Value; 2 | 3 | #[derive(Debug)] 4 | pub enum JsonKey<'a> { 5 | String(&'a str), 6 | Index(usize), 7 | } 8 | impl<'a> From<&'a str> for JsonKey<'a> { 9 | fn from(s: &'a str) -> Self { 10 | JsonKey::String(s) 11 | } 12 | } 13 | impl From for JsonKey<'_> { 14 | fn from(i: usize) -> Self { 15 | JsonKey::Index(i) 16 | } 17 | } 18 | 19 | pub fn dig_json<'a>(source: &'a Value, keys: &[JsonKey]) -> Option<&'a Value> { 20 | let mut value = source; 21 | for key in keys { 22 | value = match *key { 23 | JsonKey::String(k) => value.as_object()?.get(k)?, 24 | JsonKey::Index(index) => value.as_array()?.get(index)?, 25 | } 26 | } 27 | Some(value) 28 | } 29 | 30 | #[cfg(test)] 31 | mod tests { 32 | use super::*; 33 | 34 | #[test] 35 | fn test_dig_json() { 36 | let data = serde_json::from_str::("{}").unwrap(); 37 | let keys: Vec = vec!["foo".into(), "foo".into(), 1.into()]; 38 | assert!(dig_json(&data, &keys).is_none()); 39 | 40 | let data = serde_json::from_str::( 41 | r#"{ 42 | "foo": { 43 | "bar": [ 44 | { 45 | "id": "xxx" 46 | } 47 | ] 48 | } 49 | }"#, 50 | ) 51 | .unwrap(); 52 | let keys: Vec = vec!["foo".into(), "bar".into(), 0.into(), "id".into()]; 53 | assert_eq!(dig_json(&data, &keys).unwrap(), "xxx"); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod json_util; 2 | pub mod notion_client; 3 | pub mod notion_database; 4 | pub mod notion_pages; 5 | pub mod sqlite; 6 | 7 | #[macro_use] 8 | extern crate log; 9 | 10 | use crate::notion_client::NotionClient; 11 | use crate::sqlite::Sqlite; 12 | use anyhow::{Context, Result}; 13 | 14 | pub fn main(api_key: &str, database_id: &str, output: &str) -> Result<()> { 15 | env_logger::init(); 16 | 17 | Sqlite::validate_database_path(output) 18 | .with_context(|| format!("Failed to create a database file {}", output))?; 19 | 20 | let client = NotionClient { 21 | api_key: api_key.into(), 22 | }; 23 | 24 | let schema = client 25 | .get_database(database_id) 26 | .with_context(|| "Failed to fetch database schema")?; 27 | let pages = client 28 | .get_all_pages(database_id, &schema) 29 | .with_context(|| "Failed to fetch pages")?; 30 | 31 | let sqlite = Sqlite::new(output, &schema).with_context(|| "Failed to connect to sqlite")?; 32 | sqlite 33 | .create_tables() 34 | .with_context(|| "Failed to create tables")?; 35 | 36 | for page in pages { 37 | sqlite 38 | .insert(&page) 39 | .with_context(|| "Failed to insert pages to sqlite")?; 40 | } 41 | 42 | Ok(()) 43 | } 44 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | extern crate notion_into_sqlite; 2 | use anyhow::Result; 3 | use clap::Parser; 4 | 5 | #[derive(Parser, Debug)] 6 | #[clap(author, version, about, long_about = None)] 7 | struct Args { 8 | /// Notion API key 9 | #[clap(long)] 10 | api_key: String, 11 | 12 | /// Notion database ID 13 | #[clap(long)] 14 | database_id: String, 15 | 16 | /// Output path of sqlite database 17 | #[clap(long, default_value = "notion.db")] 18 | output: String, 19 | } 20 | 21 | fn main() -> Result<()> { 22 | let args = Args::parse(); 23 | let api_key = args.api_key; 24 | let database_id = args.database_id; 25 | let output = args.output; 26 | 27 | notion_into_sqlite::main(&api_key, &database_id, &output) 28 | } 29 | -------------------------------------------------------------------------------- /src/notion_client.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | use serde_json::{json, Value}; 3 | 4 | use crate::json_util::{dig_json, JsonKey}; 5 | use crate::notion_database::{parse_database_schema, NotionDatabaseSchema}; 6 | use crate::notion_pages::{parse_notion_page_list, NotionPage}; 7 | 8 | pub struct NotionClient { 9 | pub api_key: String, 10 | } 11 | 12 | impl NotionClient { 13 | pub fn get_database(&self, database_id: &str) -> Result { 14 | let url = format!("https://api.notion.com/v1/databases/{0}", database_id); 15 | let client = reqwest::blocking::Client::new(); 16 | info!("Requesting database schema. URL: {}", &url); 17 | let resp = client 18 | .get(url) 19 | .header("Authorization", "Bearer ".to_string() + &self.api_key) 20 | .header("Notion-Version", "2022-02-22") 21 | .send()? 22 | .json::()?; 23 | info!("Request done."); 24 | 25 | self.validate_response(&resp)?; 26 | 27 | let schema = parse_database_schema(&resp)?; 28 | info!("Database schema: {:?}", schema); 29 | Ok(schema) 30 | } 31 | 32 | pub fn get_all_pages( 33 | &self, 34 | database_id: &str, 35 | schema: &NotionDatabaseSchema, 36 | ) -> Result> { 37 | let url = format!("https://api.notion.com/v1/databases/{0}/query", database_id); 38 | let client = reqwest::blocking::Client::new(); 39 | 40 | let mut next_cursor: Option = None; 41 | let mut all_pages: Vec = vec![]; 42 | loop { 43 | let mut query = json!({ 44 | "page_size": 10i32, 45 | "sorts": [{ 46 | "timestamp": "created_time", 47 | "direction": "ascending", 48 | }] 49 | }); 50 | if let Some(cursor) = (&next_cursor).as_ref() { 51 | query 52 | .as_object_mut() 53 | .unwrap() 54 | .insert("start_cursor".into(), cursor.clone().into()); 55 | } 56 | let query_str = query.to_string(); 57 | 58 | info!("Requesting query: URL: {}, query: {}", &url, &query_str); 59 | let resp = client 60 | .post(&url) 61 | .header("Authorization", "Bearer ".to_string() + &self.api_key) 62 | .header("Notion-Version", "2022-02-22") 63 | .header("Content-Type", "application/json") 64 | .body(query_str) 65 | .send()? 66 | .json::()?; 67 | info!("Request done."); 68 | 69 | self.validate_response(&resp)?; 70 | 71 | let (mut pages, _next_cursor) = parse_notion_page_list(schema, &resp)?; 72 | info!("Pages: {:?}", pages.len()); 73 | all_pages.append(&mut pages); 74 | next_cursor = _next_cursor; 75 | 76 | if next_cursor.is_none() { 77 | info!("Fetched all items."); 78 | break; 79 | } else { 80 | info!("Has more items."); 81 | } 82 | } 83 | 84 | Ok(all_pages) 85 | } 86 | 87 | fn validate_response(&self, resp: &Value) -> Result<()> { 88 | let json_keys = vec![JsonKey::String("object")]; 89 | let object_field = dig_json(resp, &json_keys) 90 | .and_then(|o| o.as_str()) 91 | .ok_or_else(|| anyhow!("Unexpected response from Notion API: {}", resp))?; 92 | 93 | if object_field == "error" { 94 | Err(anyhow!("Error response from Notion API: {}", resp,)) 95 | } else { 96 | Ok(()) 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/notion_database.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | use serde_json::Value; 3 | use std::collections::HashMap; 4 | 5 | /// Types of property values 6 | /// See https://developers.notion.com/reference/property-value-object 7 | /// > Possible values are "rich_text", "number", "select", "multi_select", "date", 8 | /// > "formula", "relation", "rollup", "title", "people", "files", "checkbox","url", 9 | /// > "email", "phone_number", "created_time", "created_by", "last_edited_time", and "last_edited_by". 10 | #[derive(Debug, PartialEq)] 11 | pub enum NotionPropertyType { 12 | RichText, 13 | Number, 14 | Select, 15 | MultiSelect, 16 | Date, 17 | Formula, 18 | Relation, 19 | Rollup, 20 | Title, 21 | People, 22 | Files, 23 | Checkbox, 24 | Url, 25 | Email, 26 | PhoneNumber, 27 | CreatedTime, 28 | CreatedBy, 29 | LastEditedTime, 30 | LastEditedBy, 31 | Other, 32 | } 33 | 34 | #[derive(Debug)] 35 | pub struct NotionProperty { 36 | pub name: String, 37 | pub property_type: NotionPropertyType, 38 | pub property_raw_type: String, 39 | } 40 | 41 | #[derive(Debug)] 42 | pub struct NotionDatabaseSchema { 43 | pub properties: HashMap, 44 | } 45 | 46 | pub fn parse_database_schema(database_resp: &Value) -> Result { 47 | validate_object_type(database_resp)?; 48 | 49 | let raw_properties = database_resp 50 | .as_object() 51 | .and_then(|resp| resp.get("properties")) 52 | .and_then(|prop| prop.as_object()) 53 | .ok_or_else(|| anyhow!(r#"It must have "properties" object."#))?; 54 | 55 | let properties = raw_properties 56 | .keys() 57 | .filter_map(|key| { 58 | let property = raw_properties.get(key)?.as_object()?; 59 | let name = property.get("name")?.as_str()?; 60 | let property_raw_type = property.get("type")?.as_str()?; 61 | let property_type = match property_raw_type { 62 | "rich_text" => NotionPropertyType::RichText, 63 | "number" => NotionPropertyType::Number, 64 | "select" => NotionPropertyType::Select, 65 | "multi_select" => NotionPropertyType::MultiSelect, 66 | "date" => NotionPropertyType::Date, 67 | "formula" => NotionPropertyType::Formula, 68 | "relation" => NotionPropertyType::Relation, 69 | "rollup" => NotionPropertyType::Rollup, 70 | "title" => NotionPropertyType::Title, 71 | "people" => NotionPropertyType::People, 72 | "files" => NotionPropertyType::Files, 73 | "checkbox" => NotionPropertyType::Checkbox, 74 | "url" => NotionPropertyType::Url, 75 | "email" => NotionPropertyType::Email, 76 | "phone_number" => NotionPropertyType::PhoneNumber, 77 | "created_time" => NotionPropertyType::CreatedTime, 78 | "created_by" => NotionPropertyType::CreatedBy, 79 | "last_edited_time" => NotionPropertyType::LastEditedTime, 80 | "last_edited_by" => NotionPropertyType::LastEditedBy, 81 | _ => NotionPropertyType::Other, 82 | }; 83 | Some(( 84 | name.to_string(), 85 | NotionProperty { 86 | name: name.to_string(), 87 | property_raw_type: property_raw_type.to_string(), 88 | property_type, 89 | }, 90 | )) 91 | }) 92 | .collect::>(); 93 | 94 | Ok(NotionDatabaseSchema { properties }) 95 | } 96 | 97 | fn validate_object_type(database_resp: &Value) -> Result<()> { 98 | let object_field = database_resp 99 | .as_object() 100 | .and_then(|o| o.get("object")) 101 | .and_then(|o| o.as_str()) 102 | .ok_or_else(|| anyhow!(r#"It must have `"object": "database"`."#.to_string()))?; 103 | 104 | if object_field == "database" { 105 | Ok(()) 106 | } else { 107 | Err(anyhow!( 108 | r#"It must have `"object": "database"`, but was "{}""#, 109 | object_field 110 | )) 111 | } 112 | } 113 | 114 | #[cfg(test)] 115 | mod tests { 116 | use super::*; 117 | 118 | #[test] 119 | fn test_validate_object_type() { 120 | let data = r#" 121 | { 122 | "object": "database" 123 | } 124 | "#; 125 | let json = serde_json::from_str(data).unwrap(); 126 | assert!(validate_object_type(&json).is_ok()); 127 | 128 | let data = r#" 129 | { 130 | "object": "xxx" 131 | } 132 | "#; 133 | let json = serde_json::from_str(data).unwrap(); 134 | assert!(validate_object_type(&json).is_err()); 135 | 136 | let data = r#" 137 | {} 138 | "#; 139 | let json = serde_json::from_str(data).unwrap(); 140 | assert!(validate_object_type(&json).is_err()); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/notion_pages.rs: -------------------------------------------------------------------------------- 1 | use crate::json_util::{dig_json, JsonKey}; 2 | use crate::notion_database::{NotionDatabaseSchema, NotionPropertyType}; 3 | use anyhow::{anyhow, Result}; 4 | use rusqlite::ToSql; 5 | use serde_json::{Map, Value}; 6 | use std::collections::HashMap; 7 | 8 | #[derive(Debug, PartialEq)] 9 | pub enum NotionPropertyValue { 10 | Text(String), 11 | Number(f64), 12 | Json(Value), 13 | Boolean(bool), 14 | } 15 | impl ToSql for NotionPropertyValue { 16 | fn to_sql(&self) -> rusqlite::Result> { 17 | match self { 18 | NotionPropertyValue::Text(value) => value.to_sql(), 19 | NotionPropertyValue::Number(value) => value.to_sql(), 20 | NotionPropertyValue::Json(value) => Ok(rusqlite::types::ToSqlOutput::from( 21 | serde_json::to_string(value).unwrap(), 22 | )), 23 | NotionPropertyValue::Boolean(value) => value.to_sql(), 24 | } 25 | } 26 | } 27 | 28 | #[derive(Debug)] 29 | pub struct NotionPage { 30 | pub id: String, 31 | pub properties: HashMap, 32 | pub url: String, 33 | pub created_time: String, 34 | pub created_by: Value, 35 | pub last_edited_time: String, 36 | pub last_edited_by: Value, 37 | pub archived: bool, 38 | } 39 | 40 | #[allow(non_snake_case)] 41 | #[derive(Debug)] 42 | struct NotionPageBuilder<'a> { 43 | schema: &'a NotionDatabaseSchema, 44 | TITLE_JSON_PATH: Vec>, 45 | SELECT_JSON_PATH: Vec>, 46 | } 47 | impl NotionPageBuilder<'_> { 48 | fn new(schema: &NotionDatabaseSchema) -> NotionPageBuilder<'_> { 49 | NotionPageBuilder { 50 | schema, 51 | TITLE_JSON_PATH: vec!["title".into(), 0.into(), "plain_text".into()], 52 | SELECT_JSON_PATH: vec!["select".into(), "name".into()], 53 | } 54 | } 55 | 56 | fn from(&self, json_entry: &Map) -> Option { 57 | let id = json_entry.get("id")?.as_str()?.to_string(); 58 | 59 | let url = json_entry.get("url")?.as_str()?.to_string(); 60 | let created_time = json_entry.get("created_time")?.as_str()?.to_owned(); 61 | let created_by = json_entry.get("created_by")?.clone(); 62 | let last_edited_time = json_entry.get("last_edited_time")?.as_str()?.to_owned(); 63 | let last_edited_by = json_entry.get("last_edited_by")?.clone(); 64 | let archived = json_entry.get("archived")?.as_bool()?; 65 | 66 | let properties_object = json_entry.get("properties")?.as_object()?; 67 | let properties = properties_object 68 | .iter() 69 | .filter_map(|(key, property)| { 70 | let property_schema = self.schema.properties.get(key)?; 71 | let value: NotionPropertyValue = match property_schema.property_type { 72 | // TODO: convert to plain text 73 | NotionPropertyType::RichText => { 74 | NotionPropertyValue::Json(property.get("rich_text")?.clone()) 75 | } 76 | NotionPropertyType::Number => { 77 | NotionPropertyValue::Number(property.get("number")?.as_f64()?) 78 | } 79 | NotionPropertyType::Select => NotionPropertyValue::Text( 80 | dig_json(property, &self.SELECT_JSON_PATH)? 81 | .as_str()? 82 | .to_string(), 83 | ), 84 | NotionPropertyType::Title => NotionPropertyValue::Text( 85 | dig_json(property, &self.TITLE_JSON_PATH)? 86 | .as_str()? 87 | .to_string(), 88 | ), 89 | NotionPropertyType::Checkbox => { 90 | NotionPropertyValue::Boolean(property.get("checkbox")?.as_bool()?) 91 | } 92 | NotionPropertyType::Url => { 93 | NotionPropertyValue::Text(property.get("url")?.as_str()?.to_string()) 94 | } 95 | NotionPropertyType::Email => { 96 | NotionPropertyValue::Text(property.get("email")?.as_str()?.to_string()) 97 | } 98 | NotionPropertyType::PhoneNumber => NotionPropertyValue::Text( 99 | property.get("phone_number")?.as_str()?.to_string(), 100 | ), 101 | NotionPropertyType::CreatedTime => NotionPropertyValue::Text( 102 | property.get("created_time")?.as_str()?.to_string(), 103 | ), 104 | NotionPropertyType::LastEditedTime => NotionPropertyValue::Text( 105 | property.get("last_edited_time")?.as_str()?.to_string(), 106 | ), 107 | NotionPropertyType::Other => NotionPropertyValue::Json(property.clone()), 108 | _ => NotionPropertyValue::Json( 109 | property.get(&property_schema.property_raw_type)?.clone(), 110 | ), 111 | }; 112 | Some((key.to_string(), value)) 113 | }) 114 | .collect::>(); 115 | 116 | Some(NotionPage { 117 | id, 118 | properties, 119 | url, 120 | created_time, 121 | created_by, 122 | last_edited_by, 123 | last_edited_time, 124 | archived, 125 | }) 126 | } 127 | } 128 | 129 | pub fn parse_notion_page_list( 130 | schema: &NotionDatabaseSchema, 131 | query_resp: &Value, 132 | ) -> Result<(Vec, Option)> { 133 | validate_object_type(query_resp)?; 134 | 135 | let next_cursor = get_next_cursor(query_resp); 136 | 137 | let results_json_keys = vec![JsonKey::String("results")]; 138 | let results = dig_json(query_resp, &results_json_keys) 139 | .and_then(|results| results.as_array()) 140 | .map(|results| { 141 | results 142 | .iter() 143 | .filter_map(|r| r.as_object()) 144 | .collect::>() 145 | }) 146 | .ok_or_else(|| anyhow!(r#"It must have "results" as arrray of objects."#))?; 147 | 148 | let page_builder = NotionPageBuilder::new(schema); 149 | let pages: Vec = results 150 | .iter() 151 | .filter_map(|&result| page_builder.from(result)) 152 | .collect::>(); 153 | 154 | Ok((pages, next_cursor)) 155 | } 156 | 157 | fn validate_object_type(query_resp: &Value) -> Result<()> { 158 | let json_keys = vec![JsonKey::String("object")]; 159 | let object_field = dig_json(query_resp, &json_keys) 160 | .and_then(|o| o.as_str()) 161 | .ok_or_else(|| anyhow!(r#"It must have `"object": "list"`."#.to_string()))?; 162 | 163 | if object_field == "list" { 164 | Ok(()) 165 | } else { 166 | Err(anyhow!( 167 | r#"It must have `"object": "list"`, but was "{}""#, 168 | object_field 169 | )) 170 | } 171 | } 172 | 173 | fn get_next_cursor(query_resp: &Value) -> Option { 174 | let json_keys: Vec = vec!["next_cursor".into()]; 175 | Some(dig_json(query_resp, &json_keys)?.as_str()?.to_string()) 176 | } 177 | 178 | #[cfg(test)] 179 | mod tests { 180 | use super::*; 181 | 182 | #[test] 183 | fn test_validate_object_type() { 184 | let data = r#" 185 | { 186 | "object": "list" 187 | } 188 | "#; 189 | let json = serde_json::from_str(data).unwrap(); 190 | assert!(validate_object_type(&json).is_ok()); 191 | 192 | let data = r#" 193 | { 194 | "object": "xxx" 195 | } 196 | "#; 197 | let json = serde_json::from_str(data).unwrap(); 198 | assert!(validate_object_type(&json).is_err()); 199 | 200 | let data = r#" 201 | {} 202 | "#; 203 | let json = serde_json::from_str(data).unwrap(); 204 | assert!(validate_object_type(&json).is_err()); 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /src/sqlite.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, fs, path::Path}; 2 | 3 | use crate::{ 4 | notion_database::{NotionDatabaseSchema, NotionPropertyType}, 5 | notion_pages::{NotionPage, NotionPropertyValue}, 6 | }; 7 | use anyhow::{anyhow, Result}; 8 | use rusqlite::{params, params_from_iter, Connection}; 9 | 10 | pub static PAGE_METADATA_TABLE: &str = "page_metadata"; 11 | pub static PAGE_PROPERTIES_TABLE: &str = "pages"; 12 | pub static PAGE_ID_COLUMN: &str = "page_id"; 13 | 14 | /// Resolve SQLite's column name from Notion's property name 15 | struct ColumnNames { 16 | hash: HashMap, 17 | } 18 | impl ColumnNames { 19 | fn new(schema: &NotionDatabaseSchema) -> ColumnNames { 20 | let mut hash = HashMap::new(); 21 | for property in schema.properties.values() { 22 | let column = property.name.replace('"', "\"\""); 23 | hash.insert(property.name.to_string(), column); 24 | } 25 | ColumnNames { hash } 26 | } 27 | 28 | /// Resolve SQLite's column name 29 | fn resolve(&self, notion_property_name: &str) -> &str { 30 | self.hash.get(notion_property_name).unwrap() 31 | } 32 | } 33 | 34 | pub struct Sqlite<'a> { 35 | pub conn: Connection, 36 | pub schema: &'a NotionDatabaseSchema, 37 | column_names: ColumnNames, 38 | } 39 | impl Sqlite<'_> { 40 | pub fn new<'a>(path: &str, schema: &'a NotionDatabaseSchema) -> Result> { 41 | let conn = Connection::open(path)?; 42 | let column_names = ColumnNames::new(schema); 43 | Ok(Sqlite { 44 | conn, 45 | schema, 46 | column_names, 47 | }) 48 | } 49 | 50 | /// Check if database file can be created 51 | pub fn validate_database_path(path: &str) -> Result<()> { 52 | if Path::new(path).exists() { 53 | return Err(anyhow!("{} already exists", path)); 54 | } 55 | 56 | let conn = Connection::open(path)?; 57 | match conn.close() { 58 | Ok(_) => { 59 | // Delete file created by the connection because Connection::open() is just used for validation 60 | fs::remove_file(path).ok(); 61 | Ok(()) 62 | } 63 | Err((_, err)) => Err(anyhow!(err.to_string())), 64 | } 65 | } 66 | 67 | pub fn create_tables(&self) -> Result<()> { 68 | // Create page properties table 69 | let table_definition = self.table_definitin_from(); 70 | let sql = format!( 71 | "CREATE TABLE {table_name} ( 72 | {id_column} TEXT PRIMARY KEY, 73 | {definition} 74 | )", 75 | table_name = PAGE_PROPERTIES_TABLE, 76 | id_column = PAGE_ID_COLUMN, 77 | definition = table_definition, 78 | ); 79 | debug!("{}", sql); 80 | self.conn.execute(&sql, [])?; 81 | 82 | // Create page metadata table 83 | let sql = format!( 84 | "CREATE TABLE {table_name} ( 85 | id TEXT PRIMARY KEY, 86 | url TEXT, 87 | created_time TEXT, 88 | created_by JSON, 89 | last_edited_time TEXT, 90 | last_edited_by JSON, 91 | archived BOOLEAN 92 | )", 93 | table_name = PAGE_METADATA_TABLE, 94 | ); 95 | debug!("{}", sql); 96 | self.conn.execute(&sql, [])?; 97 | Ok(()) 98 | } 99 | 100 | pub fn insert(&self, page: &NotionPage) -> Result<()> { 101 | // Insert properties of page 102 | let mut property_names = vec![PAGE_ID_COLUMN]; 103 | for name in page.properties.keys() { 104 | property_names.push(name); 105 | } 106 | let sql = self.create_insert_sql_for(&property_names); 107 | debug!("{}", sql); 108 | let page_id = NotionPropertyValue::Text(page.id.clone()); 109 | let sql_params = params_from_iter(property_names.iter().map(|&column| { 110 | if column == PAGE_ID_COLUMN { 111 | &page_id 112 | } else { 113 | page.properties.get(column).unwrap() 114 | } 115 | })); 116 | debug!("Parameters: {:?}", sql_params); 117 | self.conn.execute(&sql, sql_params)?; 118 | 119 | // Insert page metadata 120 | let sql = format!( 121 | "INSERT INTO {table_name} ( 122 | id, 123 | url, 124 | created_time, 125 | created_by, 126 | last_edited_time, 127 | last_edited_by, 128 | archived 129 | ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)", 130 | table_name = PAGE_METADATA_TABLE, 131 | ); 132 | let sql_params = params![ 133 | page.id, 134 | page.url, 135 | page.created_time, 136 | page.created_by.to_string(), 137 | page.last_edited_time, 138 | page.last_edited_by.to_string(), 139 | page.archived 140 | ]; 141 | self.conn.execute(&sql, sql_params)?; 142 | 143 | Ok(()) 144 | } 145 | 146 | /// Get table definistion string from the schema object. 147 | /// It's a part of SQL query specified in {{}}: 148 | /// CREATE TABLE notion (page_id TEXT PRIMARY KEY, {{"Animal" TEXT, "Age" REAL, "Name" TEXT}}) 149 | fn table_definitin_from(&self) -> String { 150 | self.schema 151 | .properties 152 | .iter() 153 | .map(|(_, property)| { 154 | let column = self.column_names.resolve(&property.name); 155 | let data_type = match property.property_type { 156 | NotionPropertyType::Title => "TEXT", 157 | NotionPropertyType::Number => "REAL", 158 | NotionPropertyType::Select => "TEXT", 159 | NotionPropertyType::Checkbox => "BOOLEAN", 160 | NotionPropertyType::Other => "TEXT", 161 | _ => "TEXT", 162 | }; 163 | format!(r#""{column}" {data_type}"#) 164 | }) 165 | .collect::>() 166 | .join(", ") 167 | } 168 | 169 | /// Create sql like "INSERT INTO {} (id, title) values (?1, ?2)" 170 | fn create_insert_sql_for(&self, properties: &[&str]) -> String { 171 | let columns_formatted = properties 172 | .iter() 173 | .map(|&property_name| { 174 | if property_name == PAGE_ID_COLUMN { 175 | property_name.to_string() 176 | } else { 177 | let column = self.column_names.resolve(property_name); 178 | format!(r#""{column}""#) 179 | } 180 | }) 181 | .collect::>(); 182 | let placeholders = (1..(columns_formatted.len() + 1)) 183 | .map(|index| format!("?{}", index)) 184 | .collect::>(); 185 | 186 | format!( 187 | "INSERT INTO {table_name} ({columns}) VALUES ({values})", 188 | table_name = &PAGE_PROPERTIES_TABLE, 189 | columns = columns_formatted.join(", "), 190 | values = placeholders.join(", ") 191 | ) 192 | } 193 | } 194 | 195 | #[cfg(test)] 196 | mod tests { 197 | use super::*; 198 | use std::fs; 199 | 200 | #[test] 201 | fn validate_database_path() { 202 | fs::create_dir("tmp").ok(); 203 | 204 | let valid_path = "./tmp/a.db"; 205 | let result = Sqlite::validate_database_path(valid_path); 206 | assert!(result.is_ok()); 207 | let invalid_path = "tmp/foo/bar/a.db"; 208 | let result = Sqlite::validate_database_path(invalid_path); 209 | assert!(result.is_err()); 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /tests/common/fixtures.rs: -------------------------------------------------------------------------------- 1 | #[allow(dead_code)] 2 | pub static NOTION_DATABASE_JSON: &str = r#" 3 | { 4 | "object": "database", 5 | "id": "f2bf4cd7-b8d1-44fc-856e-8fe60c128b58", 6 | "cover": null, 7 | "icon": null, 8 | "created_time": "2022-03-12T00:15:00.000Z", 9 | "created_by": { 10 | "object": "user", 11 | "id": "9d069f8b-6223-4853-b7eb-8fe3dfe7d389" 12 | }, 13 | "last_edited_by": { 14 | "object": "user", 15 | "id": "9d069f8b-6223-4853-b7eb-8fe3dfe7d389" 16 | }, 17 | "last_edited_time": "2022-03-12T00:20:00.000Z", 18 | "title": [ 19 | { 20 | "type": "text", 21 | "text": { 22 | "content": "Animals", 23 | "link": null 24 | }, 25 | "annotations": { 26 | "bold": false, 27 | "italic": false, 28 | "strikethrough": false, 29 | "underline": false, 30 | "code": false, 31 | "color": "default" 32 | }, 33 | "plain_text": "Animals", 34 | "href": null 35 | } 36 | ], 37 | "properties": { 38 | "Age": { 39 | "id": "GPCK", 40 | "name": "Age", 41 | "type": "number", 42 | "number": { 43 | "format": "number" 44 | } 45 | }, 46 | "Animal": { 47 | "id": "wzVU", 48 | "name": "Animal", 49 | "type": "select", 50 | "select": { 51 | "options": [ 52 | { 53 | "id": "67fe1cf3-29f8-4cb7-9517-803e1d975e86", 54 | "name": "cat", 55 | "color": "green" 56 | }, 57 | { 58 | "id": "18ce9dcd-b7e1-4511-ad35-9420c0399e13", 59 | "name": "dog", 60 | "color": "orange" 61 | } 62 | ] 63 | } 64 | }, 65 | "Name": { 66 | "id": "title", 67 | "name": "Name", 68 | "type": "title", 69 | "title": {} 70 | } 71 | } 72 | } 73 | "#; 74 | 75 | #[allow(dead_code)] 76 | pub static NOTION_DATABASE_IRREGULAR_JSON: &str = r#" 77 | { 78 | "object": "database", 79 | "id": "f2bf4cd7-b8d1-44fc-856e-8fe60c128b58", 80 | "cover": null, 81 | "icon": null, 82 | "created_time": "2022-03-12T00:15:00.000Z", 83 | "created_by": { 84 | "object": "user", 85 | "id": "9d069f8b-6223-4853-b7eb-8fe3dfe7d389" 86 | }, 87 | "last_edited_by": { 88 | "object": "user", 89 | "id": "9d069f8b-6223-4853-b7eb-8fe3dfe7d389" 90 | }, 91 | "last_edited_time": "2022-03-12T00:20:00.000Z", 92 | "title": [ 93 | { 94 | "type": "text", 95 | "text": { 96 | "content": "Animals", 97 | "link": null 98 | }, 99 | "annotations": { 100 | "bold": false, 101 | "italic": false, 102 | "strikethrough": false, 103 | "underline": false, 104 | "code": false, 105 | "color": "default" 106 | }, 107 | "plain_text": "Animals", 108 | "href": null 109 | } 110 | ], 111 | "properties": { 112 | "あ&\";#' f _": { 113 | "id": "GPCK", 114 | "name": "あ&\";#' f _", 115 | "type": "number", 116 | "number": { 117 | "format": "number" 118 | } 119 | } 120 | } 121 | } 122 | "#; 123 | 124 | #[allow(dead_code)] 125 | pub static NOTION_DATABASE_ALL_TYPES_JSON: &str = r#" 126 | { 127 | "object": "database", 128 | "id": "8a281474-f071-4c54-8afc-17d8a4b7c782", 129 | "cover": null, 130 | "icon": null, 131 | "created_time": "2022-03-21T02:39:00.000Z", 132 | "created_by": { 133 | "object": "user", 134 | "id": "9d069f8b-6223-4853-b7eb-8fe3dfe7d389" 135 | }, 136 | "last_edited_by": { 137 | "object": "user", 138 | "id": "9d069f8b-6223-4853-b7eb-8fe3dfe7d389" 139 | }, 140 | "last_edited_time": "2022-03-21T03:37:00.000Z", 141 | "title": [ 142 | { 143 | "type": "text", 144 | "text": { 145 | "content": "All types", 146 | "link": null 147 | }, 148 | "annotations": { 149 | "bold": false, 150 | "italic": false, 151 | "strikethrough": false, 152 | "underline": false, 153 | "code": false, 154 | "color": "default" 155 | }, 156 | "plain_text": "All types", 157 | "href": null 158 | } 159 | ], 160 | "properties": { 161 | "Rollup": { 162 | "id": "%40%3FJb", 163 | "name": "Rollup", 164 | "type": "rollup", 165 | "rollup": { 166 | "rollup_property_name": "Age", 167 | "relation_property_name": "Relation", 168 | "rollup_property_id": "GPCK", 169 | "relation_property_id": "knfs", 170 | "function": "show_original" 171 | } 172 | }, 173 | "URL": { 174 | "id": "CaK%5B", 175 | "name": "URL", 176 | "type": "url", 177 | "url": {} 178 | }, 179 | "Select": { 180 | "id": "DSOY", 181 | "name": "Select", 182 | "type": "select", 183 | "select": { 184 | "options": [ 185 | { 186 | "id": "4c10699d-c938-4267-b91e-105a84d2c2e2", 187 | "name": "option", 188 | "color": "orange" 189 | } 190 | ] 191 | } 192 | }, 193 | "Phone": { 194 | "id": "IZjW", 195 | "name": "Phone", 196 | "type": "phone_number", 197 | "phone_number": {} 198 | }, 199 | "LastEditedTime": { 200 | "id": "O%3E%3B%7D", 201 | "name": "LastEditedTime", 202 | "type": "last_edited_time", 203 | "last_edited_time": {} 204 | }, 205 | "MultiSelect": { 206 | "id": "Pim~", 207 | "name": "MultiSelect", 208 | "type": "multi_select", 209 | "multi_select": { 210 | "options": [ 211 | { 212 | "id": "e1cb1dee-ff1a-47ac-81c9-7ce174a9e448", 213 | "name": "multi", 214 | "color": "default" 215 | }, 216 | { 217 | "id": "017e0af6-6e94-4f09-a43e-ce2131c81baa", 218 | "name": "select", 219 | "color": "brown" 220 | } 221 | ] 222 | } 223 | }, 224 | "CreatedTime": { 225 | "id": "RRxv", 226 | "name": "CreatedTime", 227 | "type": "created_time", 228 | "created_time": {} 229 | }, 230 | "Number": { 231 | "id": "SAc%3F", 232 | "name": "Number", 233 | "type": "number", 234 | "number": { 235 | "format": "number" 236 | } 237 | }, 238 | "LastEditedBy": { 239 | "id": "SWLu", 240 | "name": "LastEditedBy", 241 | "type": "last_edited_by", 242 | "last_edited_by": {} 243 | }, 244 | "Formula": { 245 | "id": "TzKS", 246 | "name": "Formula", 247 | "type": "formula", 248 | "formula": { 249 | "expression": "pi" 250 | } 251 | }, 252 | "Files": { 253 | "id": "b%5Be%3F", 254 | "name": "Files", 255 | "type": "files", 256 | "files": {} 257 | }, 258 | "Relation": { 259 | "id": "knfs", 260 | "name": "Relation", 261 | "type": "relation", 262 | "relation": { 263 | "database_id": "f2bf4cd7-b8d1-44fc-856e-8fe60c128b58", 264 | "synced_property_name": "Related to All types (Relation)", 265 | "synced_property_id": "JpOZ" 266 | } 267 | }, 268 | "Date": { 269 | "id": "oVB%5B", 270 | "name": "Date", 271 | "type": "date", 272 | "date": {} 273 | }, 274 | "RichText": { 275 | "id": "ozQm", 276 | "name": "RichText", 277 | "type": "rich_text", 278 | "rich_text": {} 279 | }, 280 | "CreatedBy": { 281 | "id": "ppm%5E", 282 | "name": "CreatedBy", 283 | "type": "created_by", 284 | "created_by": {} 285 | }, 286 | "Checkbox": { 287 | "id": "q%40Di", 288 | "name": "Checkbox", 289 | "type": "checkbox", 290 | "checkbox": {} 291 | }, 292 | "Email": { 293 | "id": "zEAy", 294 | "name": "Email", 295 | "type": "email", 296 | "email": {} 297 | }, 298 | "People": { 299 | "id": "%7BK%5D%60", 300 | "name": "People", 301 | "type": "people", 302 | "people": {} 303 | }, 304 | "Name": { 305 | "id": "title", 306 | "name": "Name", 307 | "type": "title", 308 | "title": {} 309 | } 310 | }, 311 | "parent": { 312 | "type": "page_id", 313 | "page_id": "5256af6e-80cc-4c63-a6f2-6fc9e4166239" 314 | }, 315 | "url": "https://www.notion.so/8a281474f0714c548afc17d8a4b7c782", 316 | "archived": false 317 | } 318 | "#; 319 | 320 | #[allow(dead_code)] 321 | pub static NOTION_LIST_JSON: &str = r#" 322 | { 323 | "object": "list", 324 | "results": [ 325 | { 326 | "object": "page", 327 | "id": "a75b9220-455d-48e1-a36b-c581a345f777", 328 | "created_time": "2022-03-12T00:15:00.000Z", 329 | "last_edited_time": "2022-03-12T00:16:00.000Z", 330 | "created_by": { 331 | "object": "user", 332 | "id": "9d069f8b-6223-4853-b7eb-8fe3dfe7d389" 333 | }, 334 | "last_edited_by": { 335 | "object": "user", 336 | "id": "9d069f8b-6223-4853-b7eb-8fe3dfe7d389" 337 | }, 338 | "cover": null, 339 | "icon": null, 340 | "parent": { 341 | "type": "database_id", 342 | "database_id": "f2bf4cd7-b8d1-44fc-856e-8fe60c128b58" 343 | }, 344 | "archived": false, 345 | "properties": { 346 | "Age": { 347 | "id": "GPCK", 348 | "type": "number", 349 | "number": 10 350 | }, 351 | "Animal": { 352 | "id": "wzVU", 353 | "type": "select", 354 | "select": { 355 | "id": "67fe1cf3-29f8-4cb7-9517-803e1d975e86", 356 | "name": "cat", 357 | "color": "green" 358 | } 359 | }, 360 | "Name": { 361 | "id": "title", 362 | "type": "title", 363 | "title": [ 364 | { 365 | "type": "text", 366 | "text": { 367 | "content": "Meu", 368 | "link": null 369 | }, 370 | "annotations": { 371 | "bold": false, 372 | "italic": false, 373 | "strikethrough": false, 374 | "underline": false, 375 | "code": false, 376 | "color": "default" 377 | }, 378 | "plain_text": "Meu", 379 | "href": null 380 | } 381 | ] 382 | } 383 | }, 384 | "url": "https://www.notion.so/Meu-a75b9220455d48e1a36bc581a345f777" 385 | } 386 | ], 387 | "next_cursor": "e6c9af10-44ec-4a48-a969-156ba5438ff0", 388 | "has_more": true, 389 | "type": "page", 390 | "page": {} 391 | } 392 | "#; 393 | 394 | #[allow(dead_code)] 395 | pub static NOTION_LIST_ALL_TYPES_JSON: &str = r#" 396 | { 397 | "object": "list", 398 | "results": [ 399 | { 400 | "object": "page", 401 | "id": "ce4593d9-0cfb-4659-8012-12594b723312", 402 | "created_time": "2022-03-21T02:39:00.000Z", 403 | "last_edited_time": "2022-03-21T03:36:00.000Z", 404 | "created_by": { 405 | "object": "user", 406 | "id": "9d069f8b-6223-4853-b7eb-8fe3dfe7d389" 407 | }, 408 | "last_edited_by": { 409 | "object": "user", 410 | "id": "9d069f8b-6223-4853-b7eb-8fe3dfe7d389" 411 | }, 412 | "cover": null, 413 | "icon": null, 414 | "parent": { 415 | "type": "database_id", 416 | "database_id": "8a281474-f071-4c54-8afc-17d8a4b7c782" 417 | }, 418 | "archived": false, 419 | "properties": { 420 | "Rollup": { 421 | "id": "%40%3FJb", 422 | "type": "rollup", 423 | "rollup": { 424 | "type": "array", 425 | "array": [ 426 | { 427 | "type": "number", 428 | "number": 10 429 | } 430 | ], 431 | "function": "show_original" 432 | } 433 | }, 434 | "URL": { 435 | "id": "CaK%5B", 436 | "type": "url", 437 | "url": "https://example.com" 438 | }, 439 | "Select": { 440 | "id": "DSOY", 441 | "type": "select", 442 | "select": { 443 | "id": "4c10699d-c938-4267-b91e-105a84d2c2e2", 444 | "name": "option", 445 | "color": "orange" 446 | } 447 | }, 448 | "Phone": { 449 | "id": "IZjW", 450 | "type": "phone_number", 451 | "phone_number": "09000000000" 452 | }, 453 | "LastEditedTime": { 454 | "id": "O%3E%3B%7D", 455 | "type": "last_edited_time", 456 | "last_edited_time": "2022-03-21T03:36:00.000Z" 457 | }, 458 | "MultiSelect": { 459 | "id": "Pim~", 460 | "type": "multi_select", 461 | "multi_select": [ 462 | { 463 | "id": "e1cb1dee-ff1a-47ac-81c9-7ce174a9e448", 464 | "name": "multi", 465 | "color": "default" 466 | }, 467 | { 468 | "id": "017e0af6-6e94-4f09-a43e-ce2131c81baa", 469 | "name": "select", 470 | "color": "brown" 471 | } 472 | ] 473 | }, 474 | "CreatedTime": { 475 | "id": "RRxv", 476 | "type": "created_time", 477 | "created_time": "2022-03-21T02:39:00.000Z" 478 | }, 479 | "Number": { 480 | "id": "SAc%3F", 481 | "type": "number", 482 | "number": 10 483 | }, 484 | "LastEditedBy": { 485 | "id": "SWLu", 486 | "type": "last_edited_by", 487 | "last_edited_by": { 488 | "object": "user", 489 | "id": "9d069f8b-6223-4853-b7eb-8fe3dfe7d389" 490 | } 491 | }, 492 | "Formula": { 493 | "id": "TzKS", 494 | "type": "formula", 495 | "formula": { 496 | "type": "number", 497 | "number": 3.14159265359 498 | } 499 | }, 500 | "Files": { 501 | "id": "b%5Be%3F", 502 | "type": "files", 503 | "files": [ 504 | { 505 | "name": "icon.png", 506 | "type": "file", 507 | "file": { 508 | "url": "https://example.com/file", 509 | "expiry_time": "2022-03-21T05:31:48.908Z" 510 | } 511 | } 512 | ] 513 | }, 514 | "Relation": { 515 | "id": "knfs", 516 | "type": "relation", 517 | "relation": [ 518 | { 519 | "id": "a75b9220-455d-48e1-a36b-c581a345f777" 520 | } 521 | ] 522 | }, 523 | "Date": { 524 | "id": "oVB%5B", 525 | "type": "date", 526 | "date": { 527 | "start": "2022-03-20", 528 | "end": null, 529 | "time_zone": null 530 | } 531 | }, 532 | "RichText": { 533 | "id": "ozQm", 534 | "type": "rich_text", 535 | "rich_text": [ 536 | { 537 | "type": "text", 538 | "text": { 539 | "content": "rich text ", 540 | "link": null 541 | }, 542 | "annotations": { 543 | "bold": false, 544 | "italic": false, 545 | "strikethrough": false, 546 | "underline": false, 547 | "code": false, 548 | "color": "default" 549 | }, 550 | "plain_text": "rich text ", 551 | "href": null 552 | }, 553 | { 554 | "type": "text", 555 | "text": { 556 | "content": "link", 557 | "link": { 558 | "url": "https://example.com" 559 | } 560 | }, 561 | "annotations": { 562 | "bold": false, 563 | "italic": false, 564 | "strikethrough": false, 565 | "underline": false, 566 | "code": false, 567 | "color": "default" 568 | }, 569 | "plain_text": "link", 570 | "href": "https://example.com" 571 | } 572 | ] 573 | }, 574 | "CreatedBy": { 575 | "id": "ppm%5E", 576 | "type": "created_by", 577 | "created_by": { 578 | "object": "user", 579 | "id": "9d069f8b-6223-4853-b7eb-8fe3dfe7d389" 580 | } 581 | }, 582 | "Checkbox": { 583 | "id": "q%40Di", 584 | "type": "checkbox", 585 | "checkbox": true 586 | }, 587 | "Email": { 588 | "id": "zEAy", 589 | "type": "email", 590 | "email": "someone@example.com" 591 | }, 592 | "People": { 593 | "id": "%7BK%5D%60", 594 | "type": "people", 595 | "people": [ 596 | { 597 | "object": "user", 598 | "id": "9d069f8b-6223-4853-b7eb-8fe3dfe7d389" 599 | } 600 | ] 601 | }, 602 | "Name": { 603 | "id": "title", 604 | "type": "title", 605 | "title": [ 606 | { 607 | "type": "text", 608 | "text": { 609 | "content": "name", 610 | "link": null 611 | }, 612 | "annotations": { 613 | "bold": false, 614 | "italic": false, 615 | "strikethrough": false, 616 | "underline": false, 617 | "code": false, 618 | "color": "default" 619 | }, 620 | "plain_text": "name", 621 | "href": null 622 | } 623 | ] 624 | } 625 | }, 626 | "url": "https://www.notion.so/name-ce4593d90cfb4659801212594b723312" 627 | } 628 | ], 629 | "next_cursor": "f02fa979-d029-4909-b95a-bcd4d18da7c6", 630 | "has_more": true, 631 | "type": "page", 632 | "page": {} 633 | }"#; 634 | -------------------------------------------------------------------------------- /tests/common/helpers.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | 3 | #[allow(dead_code)] 4 | pub fn before_db(database_path: &str) { 5 | fs::remove_file(database_path).ok(); 6 | fs::create_dir("tmp").ok(); 7 | } 8 | -------------------------------------------------------------------------------- /tests/common/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod fixtures; 2 | pub mod helpers; 3 | -------------------------------------------------------------------------------- /tests/fixtures/snapshot1.txt: -------------------------------------------------------------------------------- 1 | page_id|LastEditedTime|RichText|Relation|Phone|CreatedTime|Email|Formula|LastEditedBy|Files|Name|MultiSelect|Date|People|Select|CreatedBy|Number|Checkbox|WebSite|Rollup|id|url|created_time|created_by|last_edited_time|last_edited_by|archived 2 | ce4593d9-0cfb-4659-8012-12594b723312|2022-03-21T03:36:00.000Z|[{"annotations":{"bold":false,"code":false,"color":"default","italic":false,"strikethrough":false,"underline":false},"href":null,"plain_text":"rich text ","text":{"content":"rich text ","link":null},"type":"text"},{"annotations":{"bold":false,"code":false,"color":"default","italic":false,"strikethrough":false,"underline":false},"href":"https://example.com","plain_text":"link","text":{"content":"link","link":{"url":"https://example.com"}},"type":"text"}]|[{"id":"a75b9220-455d-48e1-a36b-c581a345f777"}]|09000000000|2022-03-21T02:39:00.000Z|someone@example.com|{"number":3.14159265359,"type":"number"}|{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}|[{"file":{"expiry_time":"2022-03-27T01:06:43.733Z","url":"https://s3.us-west-2.amazonaws.com/secure.notion-static.com/ae3df0dd-28e3-4f50-adc5-6c1a8a69ec60/fuji_icon.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=AKIAT73L2G45EIPT3X45%2F20220327%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20220327T000643Z&X-Amz-Expires=3600&X-Amz-Signature=33f8506f5ae4a7fdfb7b4f3ed724fdb92b921d46e287ba6dac58021cb3ecffaf&X-Amz-SignedHeaders=host&x-id=GetObject"},"name":"fuji_icon.png","type":"file"}]|name|[{"color":"default","id":"e1cb1dee-ff1a-47ac-81c9-7ce174a9e448","name":"multi"},{"color":"brown","id":"017e0af6-6e94-4f09-a43e-ce2131c81baa","name":"select"}]|{"end":null,"start":"2022-03-20","time_zone":null}|[{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}]|option|{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}|10.0|1|https://example.com|{"array":[{"number":10,"type":"number"}],"function":"show_original","type":"array"}|ce4593d9-0cfb-4659-8012-12594b723312|https://www.notion.so/name-ce4593d90cfb4659801212594b723312|2022-03-21T02:39:00.000Z|{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}|2022-03-21T03:36:00.000Z|{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}|0 3 | f02fa979-d029-4909-b95a-bcd4d18da7c6|2022-03-21T02:39:00.000Z|[]|[]||2022-03-21T02:39:00.000Z||{"number":3.14159265359,"type":"number"}|{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}|[]||[]|null|[]||{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}||0||{"array":[],"function":"show_original","type":"array"}|f02fa979-d029-4909-b95a-bcd4d18da7c6|https://www.notion.so/f02fa979d0294909b95abcd4d18da7c6|2022-03-21T02:39:00.000Z|{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}|2022-03-21T02:39:00.000Z|{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}|0 4 | 30e00681-448b-4ad2-a251-fc9e236973a9|2022-03-26T00:57:00.000Z|[]|[]||2022-03-26T00:52:00.000Z||{"number":3.14159265359,"type":"number"}|{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}|[]|日本語|[{"color":"default","id":"e1cb1dee-ff1a-47ac-81c9-7ce174a9e448","name":"multi"},{"color":"brown","id":"017e0af6-6e94-4f09-a43e-ce2131c81baa","name":"select"}]|null|[]|option|{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}|0.1|0||{"array":[],"function":"show_original","type":"array"}|30e00681-448b-4ad2-a251-fc9e236973a9|https://www.notion.so/30e00681448b4ad2a251fc9e236973a9|2022-03-26T00:52:00.000Z|{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}|2022-03-26T00:57:00.000Z|{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}|0 5 | 336f208e-f97a-49cb-8bfb-7fd70d2fdbe5|2022-03-26T00:53:00.000Z|[]|[]||2022-03-26T00:53:00.000Z||{"number":3.14159265359,"type":"number"}|{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}|[]|six|[]|null|[]||{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}||0||{"array":[],"function":"show_original","type":"array"}|336f208e-f97a-49cb-8bfb-7fd70d2fdbe5|https://www.notion.so/six-336f208ef97a49cb8bfb7fd70d2fdbe5|2022-03-26T00:53:00.000Z|{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}|2022-03-26T00:53:00.000Z|{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}|0 6 | 3b58e556-4555-4e8e-8c76-e6d44a75f94c|2022-03-26T00:53:00.000Z|[]|[]||2022-03-26T00:53:00.000Z||{"number":3.14159265359,"type":"number"}|{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}|[]|one|[]|null|[]||{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}||0||{"array":[],"function":"show_original","type":"array"}|3b58e556-4555-4e8e-8c76-e6d44a75f94c|https://www.notion.so/one-3b58e55645554e8e8c76e6d44a75f94c|2022-03-26T00:53:00.000Z|{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}|2022-03-26T00:53:00.000Z|{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}|0 7 | 40c91b87-3914-41e6-8911-5abb00a94974|2022-03-26T00:53:00.000Z|[]|[]||2022-03-26T00:53:00.000Z||{"number":3.14159265359,"type":"number"}|{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}|[]|five|[]|null|[]||{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}||0||{"array":[],"function":"show_original","type":"array"}|40c91b87-3914-41e6-8911-5abb00a94974|https://www.notion.so/five-40c91b87391441e689115abb00a94974|2022-03-26T00:53:00.000Z|{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}|2022-03-26T00:53:00.000Z|{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}|0 8 | 439128db-5546-4788-a491-a56cbbcd3d83|2022-03-26T00:53:00.000Z|[]|[]||2022-03-26T00:53:00.000Z||{"number":3.14159265359,"type":"number"}|{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}|[]|eleven|[]|null|[]||{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}||0||{"array":[],"function":"show_original","type":"array"}|439128db-5546-4788-a491-a56cbbcd3d83|https://www.notion.so/eleven-439128db55464788a491a56cbbcd3d83|2022-03-26T00:53:00.000Z|{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}|2022-03-26T00:53:00.000Z|{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}|0 9 | 4757ae2a-c381-4a35-9540-f1984cf2d6be|2022-03-26T00:53:00.000Z|[]|[]||2022-03-26T00:53:00.000Z||{"number":3.14159265359,"type":"number"}|{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}|[]|two|[]|null|[]||{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}||0||{"array":[],"function":"show_original","type":"array"}|4757ae2a-c381-4a35-9540-f1984cf2d6be|https://www.notion.so/two-4757ae2ac3814a359540f1984cf2d6be|2022-03-26T00:53:00.000Z|{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}|2022-03-26T00:53:00.000Z|{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}|0 10 | 6f46d5c5-9409-4db5-a9df-6357f2fb7fc0|2022-03-26T00:53:00.000Z|[]|[]||2022-03-26T00:53:00.000Z||{"number":3.14159265359,"type":"number"}|{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}|[]|seven|[]|null|[]||{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}||0||{"array":[],"function":"show_original","type":"array"}|6f46d5c5-9409-4db5-a9df-6357f2fb7fc0|https://www.notion.so/seven-6f46d5c594094db5a9df6357f2fb7fc0|2022-03-26T00:53:00.000Z|{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}|2022-03-26T00:53:00.000Z|{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}|0 11 | 77c89bea-1588-4bbd-b11d-3b99ac4b440e|2022-03-26T00:53:00.000Z|[]|[]||2022-03-26T00:53:00.000Z||{"number":3.14159265359,"type":"number"}|{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}|[]|ten|[]|null|[]||{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}||0||{"array":[],"function":"show_original","type":"array"}|77c89bea-1588-4bbd-b11d-3b99ac4b440e|https://www.notion.so/ten-77c89bea15884bbdb11d3b99ac4b440e|2022-03-26T00:53:00.000Z|{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}|2022-03-26T00:53:00.000Z|{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}|0 12 | 7bd83207-33d5-4260-9220-b1479d62d1f1|2022-03-26T00:54:00.000Z|[]|[]||2022-03-26T00:53:00.000Z||{"number":3.14159265359,"type":"number"}|{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}|[]|thirteen|[]|null|[]||{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}||0||{"array":[],"function":"show_original","type":"array"}|7bd83207-33d5-4260-9220-b1479d62d1f1|https://www.notion.so/thirteen-7bd8320733d542609220b1479d62d1f1|2022-03-26T00:53:00.000Z|{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}|2022-03-26T00:54:00.000Z|{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}|0 13 | 98fc61f8-0009-4d36-9bf6-57b735c94f0f|2022-03-26T00:53:00.000Z|[]|[]||2022-03-26T00:53:00.000Z||{"number":3.14159265359,"type":"number"}|{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}|[]|twelve|[]|null|[]||{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}||0||{"array":[],"function":"show_original","type":"array"}|98fc61f8-0009-4d36-9bf6-57b735c94f0f|https://www.notion.so/twelve-98fc61f800094d369bf657b735c94f0f|2022-03-26T00:53:00.000Z|{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}|2022-03-26T00:53:00.000Z|{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}|0 14 | d11f0348-4865-41e2-be30-c7c09f7fd907|2022-03-26T00:53:00.000Z|[]|[]||2022-03-26T00:53:00.000Z||{"number":3.14159265359,"type":"number"}|{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}|[]|four|[]|null|[]||{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}||0||{"array":[],"function":"show_original","type":"array"}|d11f0348-4865-41e2-be30-c7c09f7fd907|https://www.notion.so/four-d11f0348486541e2be30c7c09f7fd907|2022-03-26T00:53:00.000Z|{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}|2022-03-26T00:53:00.000Z|{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}|0 15 | d71b5119-20f9-4bf4-a3c4-bcf717fbf0ef|2022-03-26T00:53:00.000Z|[]|[]||2022-03-26T00:53:00.000Z||{"number":3.14159265359,"type":"number"}|{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}|[]|nine|[]|null|[]||{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}||0||{"array":[],"function":"show_original","type":"array"}|d71b5119-20f9-4bf4-a3c4-bcf717fbf0ef|https://www.notion.so/nine-d71b511920f94bf4a3c4bcf717fbf0ef|2022-03-26T00:53:00.000Z|{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}|2022-03-26T00:53:00.000Z|{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}|0 16 | e4989ccc-cec4-4d9d-8a60-f88fb23e2c17|2022-03-26T00:53:00.000Z|[]|[]||2022-03-26T00:53:00.000Z||{"number":3.14159265359,"type":"number"}|{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}|[]|eight|[]|null|[]||{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}||0||{"array":[],"function":"show_original","type":"array"}|e4989ccc-cec4-4d9d-8a60-f88fb23e2c17|https://www.notion.so/eight-e4989ccccec44d9d8a60f88fb23e2c17|2022-03-26T00:53:00.000Z|{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}|2022-03-26T00:53:00.000Z|{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}|0 17 | edf75f3b-70ad-49d8-a0b5-7e0d3aee4c0a|2022-03-26T00:53:00.000Z|[]|[]||2022-03-26T00:53:00.000Z||{"number":3.14159265359,"type":"number"}|{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}|[]|three|[]|null|[]||{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}||0||{"array":[],"function":"show_original","type":"array"}|edf75f3b-70ad-49d8-a0b5-7e0d3aee4c0a|https://www.notion.so/three-edf75f3b70ad49d8a0b57e0d3aee4c0a|2022-03-26T00:53:00.000Z|{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}|2022-03-26T00:53:00.000Z|{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}|0 18 | 01fa7fda-57b8-4292-ab0b-8db36ea4ddd9|2022-03-26T00:54:00.000Z|[]|[]||2022-03-26T00:54:00.000Z||{"number":3.14159265359,"type":"number"}|{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}|[]|fourteen|[]|null|[]||{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}||0||{"array":[],"function":"show_original","type":"array"}|01fa7fda-57b8-4292-ab0b-8db36ea4ddd9|https://www.notion.so/fourteen-01fa7fda57b84292ab0b8db36ea4ddd9|2022-03-26T00:54:00.000Z|{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}|2022-03-26T00:54:00.000Z|{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}|0 19 | 44172df6-ffcf-41df-8042-3c8e6a856d14|2022-03-26T00:54:00.000Z|[]|[]||2022-03-26T00:54:00.000Z||{"number":3.14159265359,"type":"number"}|{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}|[]|fifteen|[]|null|[]||{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}||0||{"array":[],"function":"show_original","type":"array"}|44172df6-ffcf-41df-8042-3c8e6a856d14|https://www.notion.so/fifteen-44172df6ffcf41df80423c8e6a856d14|2022-03-26T00:54:00.000Z|{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}|2022-03-26T00:54:00.000Z|{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}|0 20 | 4ad352a1-5125-4d34-a845-dbdc880d752b|2022-03-26T00:54:00.000Z|[]|[]||2022-03-26T00:54:00.000Z||{"number":3.14159265359,"type":"number"}|{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}|[]|nineteen|[]|null|[]||{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}||0||{"array":[],"function":"show_original","type":"array"}|4ad352a1-5125-4d34-a845-dbdc880d752b|https://www.notion.so/nineteen-4ad352a151254d34a845dbdc880d752b|2022-03-26T00:54:00.000Z|{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}|2022-03-26T00:54:00.000Z|{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}|0 21 | 739bf732-fa37-4972-9bf4-58ad4f7e6ec7|2022-03-26T00:54:00.000Z|[]|[]||2022-03-26T00:54:00.000Z||{"number":3.14159265359,"type":"number"}|{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}|[]|seventeen|[]|null|[]||{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}||0||{"array":[],"function":"show_original","type":"array"}|739bf732-fa37-4972-9bf4-58ad4f7e6ec7|https://www.notion.so/seventeen-739bf732fa3749729bf458ad4f7e6ec7|2022-03-26T00:54:00.000Z|{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}|2022-03-26T00:54:00.000Z|{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}|0 22 | 97838ac1-fdc2-41af-a953-01227a03423d|2022-03-26T00:54:00.000Z|[]|[]||2022-03-26T00:54:00.000Z||{"number":3.14159265359,"type":"number"}|{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}|[]|eighteen|[]|null|[]||{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}||0||{"array":[],"function":"show_original","type":"array"}|97838ac1-fdc2-41af-a953-01227a03423d|https://www.notion.so/eighteen-97838ac1fdc241afa95301227a03423d|2022-03-26T00:54:00.000Z|{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}|2022-03-26T00:54:00.000Z|{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}|0 23 | d192c30f-3ea3-45fd-8361-0b82580b2b18|2022-03-26T00:54:00.000Z|[]|[]||2022-03-26T00:54:00.000Z||{"number":3.14159265359,"type":"number"}|{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}|[]|sixteen|[]|null|[]||{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}||0||{"array":[],"function":"show_original","type":"array"}|d192c30f-3ea3-45fd-8361-0b82580b2b18|https://www.notion.so/sixteen-d192c30f3ea345fd83610b82580b2b18|2022-03-26T00:54:00.000Z|{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}|2022-03-26T00:54:00.000Z|{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}|0 24 | fba0fb7e-72ac-4900-8013-5b53291f08d5|2022-03-26T00:54:00.000Z|[]|[]||2022-03-26T00:54:00.000Z||{"number":3.14159265359,"type":"number"}|{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}|[]|twenty|[]|null|[]||{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}||0||{"array":[],"function":"show_original","type":"array"}|fba0fb7e-72ac-4900-8013-5b53291f08d5|https://www.notion.so/twenty-fba0fb7e72ac490080135b53291f08d5|2022-03-26T00:54:00.000Z|{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}|2022-03-26T00:54:00.000Z|{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}|0 25 | 5b694dd6-7d3f-43c4-976b-1883f5c352ba|2022-03-26T00:56:00.000Z|[{"annotations":{"bold":false,"code":false,"color":"default","italic":false,"strikethrough":false,"underline":false},"href":null,"plain_text":"I can ","text":{"content":"I can ","link":null},"type":"text"},{"annotations":{"bold":true,"code":false,"color":"default","italic":false,"strikethrough":false,"underline":false},"href":null,"plain_text":"write","text":{"content":"write","link":null},"type":"text"},{"annotations":{"bold":false,"code":false,"color":"default","italic":false,"strikethrough":false,"underline":false},"href":null,"plain_text":" rich ","text":{"content":" rich ","link":null},"type":"text"},{"annotations":{"bold":false,"code":false,"color":"default","italic":false,"strikethrough":false,"underline":true},"href":null,"plain_text":"text","text":{"content":"text","link":null},"type":"text"},{"annotations":{"bold":false,"code":false,"color":"default","italic":false,"strikethrough":false,"underline":false},"href":null,"plain_text":" ","text":{"content":" ","link":null},"type":"text"},{"annotations":{"bold":false,"code":false,"color":"orange","italic":false,"strikethrough":false,"underline":false},"href":null,"plain_text":"here","text":{"content":"here","link":null},"type":"text"}]|[]||2022-03-26T00:55:00.000Z||{"number":3.14159265359,"type":"number"}|{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}|[]|article|[]|null|[]||{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}||1||{"array":[],"function":"show_original","type":"array"}|5b694dd6-7d3f-43c4-976b-1883f5c352ba|https://www.notion.so/article-5b694dd67d3f43c4976b1883f5c352ba|2022-03-26T00:55:00.000Z|{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}|2022-03-26T00:56:00.000Z|{"id":"9d069f8b-6223-4853-b7eb-8fe3dfe7d389","object":"user"}|0 26 | -------------------------------------------------------------------------------- /tests/integration_test.rs: -------------------------------------------------------------------------------- 1 | mod common; 2 | 3 | extern crate notion_into_sqlite; 4 | 5 | use regex::Regex; 6 | use std::env; 7 | use std::error::Error; 8 | use std::fs; 9 | use std::process::Command; 10 | 11 | use common::helpers::before_db; 12 | 13 | // Ignore this locally and run only CI 14 | // To update snapshot file, 15 | // > sqlite3 tmp/snapshot1.db -header "select page_id, LastEditedTime, RichText, Relation, Phone, CreatedTime, Email, Formula, LastEditedBy, Files, Name, MultiSelect, Date, People, \"Select\", CreatedBy, Number, Checkbox, WebSite, Rollup, id, url, created_time, created_by, last_edited_time, last_edited_by, archived from pages inner join page_metadata on pages.page_id = page_metadata.id" > tests/fixtures/snapshot1.txt 16 | #[ignore] 17 | #[test] 18 | fn snapshot_test() -> Result<(), Box> { 19 | let api_key = env::var("NOTION_API_KEY")?; 20 | let database_id = env::var("NOTION_DATABASE_ID")?; 21 | let output = "tmp/snapshot1.db"; 22 | before_db(output); 23 | 24 | notion_into_sqlite::main(&api_key, &database_id, output)?; 25 | 26 | // depending on sqlite3 command is not so good 27 | let dump = Command::new("sqlite3") 28 | .args([output, "-header", "select page_id, LastEditedTime, RichText, Relation, Phone, CreatedTime, Email, Formula, LastEditedBy, Files, Name, MultiSelect, Date, People, \"Select\", CreatedBy, Number, Checkbox, WebSite, Rollup, id, url, created_time, created_by, last_edited_time, last_edited_by, archived from pages inner join page_metadata on pages.page_id = page_metadata.id"]) 29 | .output()?; 30 | if !dump.status.success() { 31 | panic!("{}", String::from_utf8(dump.stderr).unwrap()); 32 | } 33 | let snapshot = sanitize_file_object(&String::from_utf8(dump.stdout)?); 34 | 35 | // compare result line by line 36 | let snapshot_lines = snapshot.split("\n").collect::>(); 37 | 38 | let expected_snapshot = 39 | sanitize_file_object(&fs::read_to_string("tests/fixtures/snapshot1.txt")?); 40 | let expected_snapshot_lines = expected_snapshot.split("\n").collect::>(); 41 | for (i, line) in expected_snapshot_lines.iter().enumerate() { 42 | assert_eq!(line, snapshot_lines.get(i).unwrap()); 43 | } 44 | 45 | Ok(()) 46 | } 47 | 48 | fn sanitize_file_object(text: &str) -> String { 49 | let file_url_pattern = Regex::new("https://s3[^\"]+").unwrap(); 50 | let mut result = file_url_pattern.replace(text, "").to_string(); 51 | 52 | // "expiry_time\":\"2022-03-26T13:55:14.661Z\" 53 | let file_expiration_pattern = Regex::new(r#""expiry_time":"[^"]+""#).unwrap(); 54 | result = file_expiration_pattern 55 | .replace(&result, r#""expiry_time":"""#) 56 | .to_string(); 57 | result 58 | } 59 | -------------------------------------------------------------------------------- /tests/notion_database_test.rs: -------------------------------------------------------------------------------- 1 | mod common; 2 | 3 | extern crate notion_into_sqlite; 4 | 5 | use common::fixtures; 6 | use notion_into_sqlite::notion_database::{parse_database_schema, NotionPropertyType}; 7 | use std::error::Error; 8 | 9 | #[test] 10 | fn it_parses_database_json() -> Result<(), Box> { 11 | let json = serde_json::from_str::(fixtures::NOTION_DATABASE_JSON)?; 12 | let schema = parse_database_schema(&json)?; 13 | let properties = schema.properties; 14 | assert_eq!(properties.len(), 3); 15 | 16 | let name_property = properties.get("Name").unwrap(); 17 | assert_eq!(name_property.name, "Name"); 18 | assert_eq!(name_property.property_raw_type, "title"); 19 | assert_eq!(name_property.property_type, NotionPropertyType::Title); 20 | 21 | let age_property = properties.get("Age").unwrap(); 22 | assert_eq!(age_property.name, "Age"); 23 | assert_eq!(age_property.property_raw_type, "number"); 24 | assert_eq!(age_property.property_type, NotionPropertyType::Number); 25 | 26 | let age_property = properties.get("Animal").unwrap(); 27 | assert_eq!(age_property.name, "Animal"); 28 | assert_eq!(age_property.property_raw_type, "select"); 29 | assert_eq!(age_property.property_type, NotionPropertyType::Select); 30 | Ok(()) 31 | } 32 | 33 | #[test] 34 | fn it_parses_database_json_with_all_property_types() -> Result<(), Box> { 35 | let json = serde_json::from_str::(fixtures::NOTION_DATABASE_ALL_TYPES_JSON)?; 36 | let schema = parse_database_schema(&json)?; 37 | let properties = schema.properties; 38 | assert_eq!(properties.len(), 19); 39 | 40 | Ok(()) 41 | } 42 | -------------------------------------------------------------------------------- /tests/notion_pages_test.rs: -------------------------------------------------------------------------------- 1 | mod common; 2 | 3 | extern crate notion_into_sqlite; 4 | 5 | use common::fixtures; 6 | use notion_into_sqlite::notion_database::parse_database_schema; 7 | use notion_into_sqlite::notion_pages::{parse_notion_page_list, NotionPropertyValue}; 8 | use std::error::Error; 9 | 10 | #[test] 11 | fn it_parses_notion_page_list() -> Result<(), Box> { 12 | let json = serde_json::from_str::(fixtures::NOTION_DATABASE_JSON)?; 13 | let schema = parse_database_schema(&json)?; 14 | let pages_json = serde_json::from_str::(fixtures::NOTION_LIST_JSON)?; 15 | let (pages, next_cursor) = parse_notion_page_list(&schema, &pages_json)?; 16 | assert_eq!(next_cursor.unwrap(), "e6c9af10-44ec-4a48-a969-156ba5438ff0"); 17 | assert_eq!(pages.len(), 1); 18 | 19 | let entry = pages.first().unwrap(); 20 | assert_eq!(entry.id, "a75b9220-455d-48e1-a36b-c581a345f777"); 21 | assert_eq!(entry.properties.len(), 3); 22 | 23 | let properties = &entry.properties; 24 | assert_eq!( 25 | properties.get("Name").unwrap(), 26 | &NotionPropertyValue::Text("Meu".to_string()) 27 | ); 28 | assert_eq!( 29 | properties.get("Age").unwrap(), 30 | &NotionPropertyValue::Number(10.0) 31 | ); 32 | assert_eq!( 33 | properties.get("Animal").unwrap(), 34 | &NotionPropertyValue::Text("cat".to_string()) 35 | ); 36 | Ok(()) 37 | } 38 | 39 | #[test] 40 | fn it_parses_notion_page_list_with_all_types() -> Result<(), Box> { 41 | let json = serde_json::from_str::(fixtures::NOTION_DATABASE_ALL_TYPES_JSON)?; 42 | let schema = parse_database_schema(&json)?; 43 | let pages_json = 44 | serde_json::from_str::(fixtures::NOTION_LIST_ALL_TYPES_JSON)?; 45 | let (pages, _) = parse_notion_page_list(&schema, &pages_json)?; 46 | assert_eq!(pages.len(), 1); 47 | 48 | let entry = pages.first().unwrap(); 49 | assert_eq!(entry.id, "ce4593d9-0cfb-4659-8012-12594b723312"); 50 | assert_eq!(entry.properties.len(), 19); 51 | 52 | Ok(()) 53 | } 54 | -------------------------------------------------------------------------------- /tests/sqlite_test.rs: -------------------------------------------------------------------------------- 1 | mod common; 2 | 3 | extern crate notion_into_sqlite; 4 | 5 | use rusqlite::params; 6 | use std::collections::HashMap; 7 | 8 | use common::{fixtures, helpers}; 9 | use notion_into_sqlite::notion_database::parse_database_schema; 10 | use notion_into_sqlite::notion_pages::{NotionPage, NotionPropertyValue}; 11 | use notion_into_sqlite::sqlite::{ 12 | Sqlite, PAGE_ID_COLUMN, PAGE_METADATA_TABLE, PAGE_PROPERTIES_TABLE, 13 | }; 14 | use std::error::Error; 15 | 16 | #[test] 17 | fn it_creates_tables() -> Result<(), Box> { 18 | let database_path = "tmp/test1.db"; 19 | helpers::before_db(database_path); 20 | 21 | let json = serde_json::from_str::(fixtures::NOTION_DATABASE_JSON)?; 22 | let schema = parse_database_schema(&json)?; 23 | let sqlite = Sqlite::new(database_path, &schema)?; 24 | sqlite.create_tables()?; 25 | 26 | let table_def_sql: String = sqlite.conn.query_row( 27 | "SELECT sql FROM sqlite_master where name=?1", 28 | params![PAGE_PROPERTIES_TABLE], 29 | |row| Ok(row.get(0)?), 30 | )?; 31 | assert!(table_def_sql.contains(&format!( 32 | "{id_column} TEXT PRIMARY KEY", 33 | id_column = PAGE_ID_COLUMN 34 | ))); 35 | assert!(table_def_sql.contains(r#""Name" TEXT"#)); 36 | assert!(table_def_sql.contains(r#""Animal" TEXT"#)); 37 | assert!(table_def_sql.contains(r#""Age" REAL"#)); 38 | 39 | let table_def_sql: String = sqlite.conn.query_row( 40 | "SELECT sql FROM sqlite_master where name=?1", 41 | params![PAGE_METADATA_TABLE], 42 | |row| Ok(row.get(0)?), 43 | )?; 44 | assert!(table_def_sql.contains(r#"url TEXT"#)); 45 | 46 | Ok(()) 47 | } 48 | 49 | #[test] 50 | fn it_creates_table_when_column_name_includes_double_quote() -> Result<(), Box> { 51 | let database_path = "tmp/test2.db"; 52 | helpers::before_db(database_path); 53 | 54 | let json = serde_json::from_str::(fixtures::NOTION_DATABASE_IRREGULAR_JSON)?; 55 | let schema = parse_database_schema(&json)?; 56 | let sqlite = Sqlite::new(database_path, &schema)?; 57 | sqlite.create_tables()?; 58 | 59 | let (table_name, sql): (String, String) = 60 | sqlite 61 | .conn 62 | .query_row("SELECT name, sql FROM sqlite_master", [], |row| { 63 | Ok((row.get(0)?, row.get(1)?)) 64 | })?; 65 | assert_eq!(table_name, PAGE_PROPERTIES_TABLE); 66 | assert!(sql.contains(r#""あ&"";#' f _" REAL"#)); 67 | Ok(()) 68 | } 69 | 70 | #[test] 71 | fn it_inserts_notion_entry() -> Result<(), Box> { 72 | let database_path = "tmp/test3.db"; 73 | helpers::before_db(database_path); 74 | 75 | let json = serde_json::from_str::(fixtures::NOTION_DATABASE_JSON)?; 76 | let schema = parse_database_schema(&json)?; 77 | let sqlite = Sqlite::new(database_path, &schema)?; 78 | sqlite.create_tables()?; 79 | 80 | let page = NotionPage { 81 | id: "xxxx".to_string(), 82 | properties: HashMap::from([ 83 | ( 84 | "Name".to_string(), 85 | NotionPropertyValue::Text("Meu".to_string()), 86 | ), 87 | ("Age".to_string(), NotionPropertyValue::Number(5.0)), 88 | ]), 89 | url: "https://www.notion.so/xxxx".to_string(), 90 | created_time: "2022-03-12T00:15:00.000Z".to_string(), 91 | created_by: serde_json::from_str( 92 | r#"{ 93 | "object": "user", 94 | "id": "9d069f8b-6223-4853-b7eb-8fe3dfe7d389" 95 | }"#, 96 | ) 97 | .unwrap(), 98 | last_edited_time: "2022-03-12T00:16:00.000Z".to_string(), 99 | last_edited_by: serde_json::from_str( 100 | r#"{ 101 | "object": "user", 102 | "id": "9d069f8b-6223-4853-b7eb-8fe3dfe7d389" 103 | }"#, 104 | ) 105 | .unwrap(), 106 | archived: false, 107 | }; 108 | sqlite.insert(&page)?; 109 | 110 | let (page_id, name, age): (String, String, f64) = sqlite.conn.query_row( 111 | format!( 112 | r#"SELECT page_id,"Name","Age" from {table_name}"#, 113 | table_name = PAGE_PROPERTIES_TABLE 114 | ) 115 | .as_str(), 116 | [], 117 | |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)), 118 | )?; 119 | assert_eq!(page_id, "xxxx"); 120 | assert_eq!(name, "Meu"); 121 | assert_eq!(age, 5.0); 122 | 123 | let (page_id, url, created_time, created_by): (String, String, String, String) = 124 | sqlite.conn.query_row( 125 | format!( 126 | r#"SELECT id, url, created_time, created_by from {table_name}"#, 127 | table_name = PAGE_METADATA_TABLE 128 | ) 129 | .as_str(), 130 | [], 131 | |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?)), 132 | )?; 133 | assert_eq!(page_id, "xxxx"); 134 | assert_eq!(url, "https://www.notion.so/xxxx"); 135 | assert_eq!(created_time, "2022-03-12T00:15:00.000Z"); 136 | assert!(serde_json::from_str::(&created_by).is_ok()); 137 | 138 | Ok(()) 139 | } 140 | --------------------------------------------------------------------------------