├── .gitignore ├── .rspec ├── .rubocop.yml ├── .rubocop_todo.yml ├── .travis.yml ├── .vscode └── settings.json ├── Gemfile ├── Gemfile.lock ├── LICENSE.md ├── README.md ├── Rakefile ├── _config.yml ├── gifs ├── change block title.gif ├── change to todo, check.gif ├── change_title.gif ├── create collection.gif ├── create.gif ├── duplicate.gif ├── get_children_title.gif └── move_before_and_after.gif ├── lib ├── notion_api.rb └── notion_api │ ├── blocks.rb │ ├── client.rb │ ├── core.rb │ ├── markdown.rb │ ├── notion_types │ ├── bulleted_block.rb │ ├── callout_block.rb │ ├── code_block.rb │ ├── collection_view_blocks.rb │ ├── column_list_block.rb │ ├── divider_block.rb │ ├── header_block.rb │ ├── image_block.rb │ ├── latex_block.rb │ ├── link_block.rb │ ├── numbered_block.rb │ ├── page_block.rb │ ├── quote_block.rb │ ├── sub_header_block.rb │ ├── sub_sub_header.rb │ ├── table_of_contents_block.rb │ ├── template.rb │ ├── text_block.rb │ ├── todo_block.rb │ └── toggle_block.rb │ ├── utils.rb │ └── version.rb ├── notion.gemspec ├── notion_api_block_spec.yaml └── spec ├── blocks_spec.rb ├── client_spec.rb ├── core_spec.rb ├── fixtures ├── emoji_data.json ├── notion_block_one_response.json ├── notion_block_two_response.json ├── notion_collection_view_one_response.json ├── notion_collection_view_two_response.json ├── notion_page_response.json └── vauto_inventory.csv ├── spec_helper.rb ├── spec_variables.rb └── utils_spec.rb /.gitignore: -------------------------------------------------------------------------------- 1 | ruby/* 2 | .env 3 | .vscode -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --require spec_helper 2 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: .rubocop_todo.yml 2 | -------------------------------------------------------------------------------- /.rubocop_todo.yml: -------------------------------------------------------------------------------- 1 | # This configuration was generated by 2 | # `rubocop --auto-gen-config` 3 | # on 2020-11-24 19:55:59 UTC using RuboCop version 1.4.1. 4 | # The point is for the user to remove these configuration records 5 | # one by one as the offenses are removed from the code base. 6 | # Note that changes in the inspected code, or installation of new 7 | # versions of RuboCop, may require this file to be generated again. 8 | 9 | # Offense count: 2 10 | Lint/MissingSuper: 11 | Exclude: 12 | - 'blocks.rb' 13 | 14 | # Offense count: 12 15 | # Configuration parameters: IgnoredMethods. 16 | Metrics/AbcSize: 17 | Max: 59 18 | 19 | # Offense count: 5 20 | # Configuration parameters: CountComments, CountAsOne. 21 | Metrics/ClassLength: 22 | Max: 259 23 | 24 | # Offense count: 1 25 | # Configuration parameters: IgnoredMethods. 26 | Metrics/CyclomaticComplexity: 27 | Max: 9 28 | 29 | # Offense count: 36 30 | # Configuration parameters: CountComments, CountAsOne, ExcludedMethods. 31 | Metrics/MethodLength: 32 | Max: 57 33 | 34 | # Offense count: 1 35 | # Configuration parameters: CountKeywordArgs. 36 | Metrics/ParameterLists: 37 | Max: 6 38 | 39 | # Offense count: 1 40 | # Configuration parameters: IgnoredMethods. 41 | Metrics/PerceivedComplexity: 42 | Max: 10 43 | 44 | # Offense count: 1 45 | Naming/AccessorMethodName: 46 | Exclude: 47 | - 'utils.rb' 48 | 49 | # Offense count: 2 50 | Style/ClassVars: 51 | Exclude: 52 | - 'core.rb' 53 | 54 | # Offense count: 3 55 | Style/Documentation: 56 | Exclude: 57 | - 'spec/**/*' 58 | - 'test/**/*' 59 | - 'utils.rb' 60 | 61 | # Offense count: 2 62 | # Configuration parameters: AllowedVariables. 63 | Style/GlobalVars: 64 | Exclude: 65 | - 'utils.rb' 66 | 67 | # Offense count: 2 68 | # Cop supports --auto-correct. 69 | Style/IfUnlessModifier: 70 | Exclude: 71 | - 'blocks.rb' 72 | 73 | # Offense count: 2 74 | # Cop supports --auto-correct. 75 | # Configuration parameters: EnforcedStyle, ConsistentQuotesInMultiline. 76 | # SupportedStyles: single_quotes, double_quotes 77 | Style/StringLiterals: 78 | Exclude: 79 | - 'client.rb' 80 | 81 | # Offense count: 33 82 | # Cop supports --auto-correct. 83 | # Configuration parameters: AutoCorrect, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns. 84 | # URISchemes: http, https 85 | Layout/LineLength: 86 | Max: 193 87 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | dist: bionic 3 | rvm: 4 | # using .filter, so must be >= 2.6... will update to be .select later... 5 | - 2.6 6 | - 2.7 7 | branches: 8 | only: 9 | - master 10 | - "/^deploy-.*$/" 11 | before_install: 12 | - gem install bundler 13 | - rvm list known 14 | install: bundle install 15 | script: rspec spec 16 | notifications: 17 | email: 18 | recipients: 19 | - danielmurph8@gmail.com 20 | on_success: never 21 | on_failure: always 22 | env: 23 | global: 24 | - secure: ISJIBqqiLGQbuGLf+5HsyME7uDVDft0Ah6JJ+bynve1dxJS3God/VqOv0VpiV6KnDqY6ivqb6/AcabAnDcs/mBCiEcC9t2Y/fmN60SEpz7Mdw1B/NUie+zB2Bu2o/BckrOj2HA+ok4bjZgdv4EZV20QK33gmYYMIsANz760EMTJmaluDC52TSHBEzXEPOfa3qf20WPAcW8cFuSHkFwZ3ynoMqlD2RfGvYbJYsCs2O3ugtWIb7cQTkFOJvEAAhWpelnsAxRaztCK6IKYCTKQ8N1GESvu3zApXzzHrsYimO1RWb1FahHAfQ+BB9tGPOgyRdJ242vyxybHr8kCYW44DDccM1RDH9iKw5DIoKE507YrWmYq+LiPkMXDlJeqlBCeIYTjyxJO8lLS+gdHjPw1CuWZk1OQh0bNqOoAaEs3PEi3kyTHoXX0LZHCboVRAsMDjXpW0UAYQqWz604JA0/FXwSvUr8K8wX1R+axEh07yQKKLWOO+O0z6aUsD8HZa8723lHXwfiOl+bC+/a4TIvOJ9fqn5zm4TlXw40wocWpwR2SssZ7N1XpVfe650ZQ/HwdGMKXhmSHwSiWdhCmICBJQB6Gfp1p3rqFFdhmyzEmYRrQj+x+Q3QHKvyaCCiatrbb/iDLlkonBxqLtJD9HusHmWrg9iXBBu+5Kv1VY6NRtXOY= 25 | - secure: Ek538gssOTnkUqxHYqpn6izFZbib3fdk5ohcKB0LFyDBl7g8ifCFKDp5MXuldVFJZ8ZHiAiaRLzPEY1t1mU4lVOM8TvjX8TcussmU4yntlvsHyByQm2YLQU+xMQ0nUSNsMMN7Q2k532mLs1UoEvuQu/XmUrqJp4O5h+cdp6Q3ZCqNqYGotn9UtSFN7lQjuQoWqyPiV13F4n+4nuX1QFVUkpv4/iC6rrqb+TvkYShEYbnCrYbNooxnE2QDHAOLFhZOTHH4gteopuk2Fs+WgEliEpPpDzHjpS62yJo8HruvaCU5a9sEH9PppnxlP1aHx/XvHP5N+MG/KkHjxL+i/uvrw1+CjIwIvqFRAJHr0rY/wXyAT3tTk4Tr/SnPBK7xgaAOnhUzHNqwGGTPGcUehScg3pngXWZ8KToqokgJH1fRWvsh0jpK9PSe0mpGVVbzYbZeCPQZIBE1Gnpdl7Nu6aSK8TdMPKkFMGe329eSKoj3gNyX+bSkWnJyBoG9rpx27CTYAQWmcFKC31J6dkHNR1KV4UbqaAYZ8UB6A9ylLrJ0GutGh1Jniw0tI+PyYOoFg4sdVRwa62kxNAq5/ytvBXteco+QJFAmJDhbIDqIF5z1j2JePb4hDSSW/B93wTUazxYTsfPOyAcpxuEiwajEfe2kYUMhnNT3WBIHMSsRinNEXc= 26 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.pythonPath": "/usr/local/bin/python3", 3 | "workbench.colorCustomizations": { 4 | "activityBar.activeBackground": "#65c89b", 5 | "activityBar.activeBorder": "#945bc4", 6 | "activityBar.background": "#65c89b", 7 | "activityBar.foreground": "#15202b", 8 | "activityBar.inactiveForeground": "#15202b99", 9 | "activityBarBadge.background": "#945bc4", 10 | "activityBarBadge.foreground": "#e7e7e7", 11 | "statusBar.background": "#42b883", 12 | "statusBar.foreground": "#15202b", 13 | "statusBarItem.hoverBackground": "#359268", 14 | "titleBar.activeBackground": "#42b883", 15 | "titleBar.activeForeground": "#15202b", 16 | "titleBar.inactiveBackground": "#42b88399", 17 | "titleBar.inactiveForeground": "#15202b99" 18 | }, 19 | "peacock.color": "#42b883", 20 | "rufo.exe": "rufo", // can be an absolute path 21 | "rufo.useBundler": false, 22 | } -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gemspec # dependencies specified in notion_cli.gemspec -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | notion (1.1.3) 5 | httparty (~> 0.17) 6 | json (~> 2.2) 7 | 8 | GEM 9 | remote: https://rubygems.org/ 10 | specs: 11 | ast (2.4.1) 12 | diff-lcs (1.4.4) 13 | httparty (0.18.1) 14 | mime-types (~> 3.0) 15 | multi_xml (>= 0.5.2) 16 | json (2.5.1) 17 | mime-types (3.3.1) 18 | mime-types-data (~> 3.2015) 19 | mime-types-data (3.2020.1104) 20 | multi_xml (0.6.0) 21 | parallel (1.20.1) 22 | parser (2.7.2.0) 23 | ast (~> 2.4.1) 24 | rainbow (3.0.0) 25 | rake (13.0.1) 26 | regexp_parser (1.8.2) 27 | rexml (3.2.4) 28 | rspec (3.9.0) 29 | rspec-core (~> 3.9.0) 30 | rspec-expectations (~> 3.9.0) 31 | rspec-mocks (~> 3.9.0) 32 | rspec-core (3.9.3) 33 | rspec-support (~> 3.9.3) 34 | rspec-expectations (3.9.4) 35 | diff-lcs (>= 1.2.0, < 2.0) 36 | rspec-support (~> 3.9.0) 37 | rspec-mocks (3.9.1) 38 | diff-lcs (>= 1.2.0, < 2.0) 39 | rspec-support (~> 3.9.0) 40 | rspec-support (3.9.4) 41 | rubocop (1.4.1) 42 | parallel (~> 1.10) 43 | parser (>= 2.7.1.5) 44 | rainbow (>= 2.2.2, < 4.0) 45 | regexp_parser (>= 1.8) 46 | rexml 47 | rubocop-ast (>= 1.1.1) 48 | ruby-progressbar (~> 1.7) 49 | unicode-display_width (>= 1.4.0, < 2.0) 50 | rubocop-ast (1.1.1) 51 | parser (>= 2.7.1.5) 52 | ruby-progressbar (1.10.1) 53 | unicode-display_width (1.7.0) 54 | 55 | PLATFORMS 56 | ruby 57 | 58 | DEPENDENCIES 59 | bundler 60 | notion! 61 | rake (~> 13.0.0) 62 | rspec (~> 3.9.0) 63 | rubocop (~> 1.4.0) 64 | 65 | BUNDLED WITH 66 | 2.1.4 67 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Dan Murphy 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 | 2 | 3 | # Unofficial Notion Client for Ruby. 4 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/f13e49a8807e4fe297273f48bd8d7a61)](https://app.codacy.com/gh/danmurphy1217/notion-ruby?utm_source=github.com&utm_medium=referral&utm_content=danmurphy1217/notion-ruby&utm_campaign=Badge_Grade) 5 | [![Build Status](https://travis-ci.com/danmurphy1217/notion-ruby.svg?branch=master)](https://travis-ci.com/danmurphy1217/notion-ruby) [![Ruby Style Guide](https://img.shields.io/badge/code_style-rubocop-brightgreen.svg)](https://github.com/rubocop-hq/rubocop) [![Gem Version](https://badge.fury.io/rb/notion.svg)](https://badge.fury.io/rb/notion) 6 | 7 | - Read the [blog post](https://towardsdatascience.com/introducing-the-notion-api-ruby-gem-d47d4a6ef0ca), which outlines why I built this and some of the functionality. 8 | - Check out the [Gem](https://rubygems.org/gems/notion)! 9 | 10 | ## Table of Contents 11 | - [Unofficial Notion Client for Ruby.](#unofficial-notion-client-for-ruby) 12 | - [Table of Contents](#table-of-contents) 13 | - [Getting Started](#getting-started) 14 | - [Installation](#installation) 15 | - [Retrieving a Page](#retrieving-a-page) 16 | - [Retrieving a CollectionView Page](#retrieving-a-collectionview-page) 17 | - [Retrieving a Block within the Page](#retrieving-a-block-within-the-page) 18 | - [Get a Block](#get-a-block) 19 | - [Get a Collection View](#get-a-collection-view) 20 | - [Creating New Blocks](#creating-new-blocks) 21 | - [Create a block whose parent is the page](#create-a-block-whose-parent-is-the-page) 22 | - [Create a block whose parent is another block](#create-a-block-whose-parent-is-another-block) 23 | - [Creating New Collections](#creating-new-collections) 24 | - [Updating Collection View Cells](#updating-collection-view-cells) 25 | - [Troubleshooting](#troubleshooting) 26 | - [No results returned when attempting to get a page](#no-results-returned-when-attempting-to-get-a-page) 27 | - [Retrieve a full-page Collection View](#retrieve-a-full-page-collection-view) 28 | - [Linking to another page](#linking-to-another-page) 29 | 30 | ## Getting Started 31 | ### Installation 32 | to install the gem: 33 | ```ruby 34 | gem install notion 35 | ``` 36 | Then, place this at the top of your file: 37 | ```ruby 38 | require 'notion_api' 39 | ``` 40 | To get started using the gem, you'll first need to retrieve your token_v2 credentials by signing into Notion online, navigating to the developer tools, inspecting the cookies, and finding the value associated with the **token_v2** key. 41 | 42 | From here, you can instantiate the Notion Client with the following code: 43 | ```ruby 44 | >>> @client = NotionAPI::Client.new("") 45 | ``` 46 | ## Retrieving a Page 47 | A typical starting point is the `get_page` method, which returns a Notion Page Block. The `get_page` method accepts the ID (formatted or not) or the URL of the page: 48 | 1. URL → https://www.notion.so/danmurphy/TEST-PAGE-d2ce338f19e847f586bd17679f490e66 49 | 2. ID → d2ce338f19e847f586bd17679f490e66 50 | 3. Formatted ID → d2ce338f-19e8-47f5-86bd-17679f490e66 51 | ```ruby 52 | >>> @client.get_page("https://www.notion.so/danmurphy/TEST-PAGE-d2ce338f19e847f586bd17679f490e66") 53 | >>> @client.get_page("d2ce338f19e847f586bd17679f490e66") 54 | >>> @client.get_page("d2ce338f-19e8-47f5-86bd-17679f490e66") 55 | ``` 56 | All three of these will return the same block instance: 57 | ```ruby 58 | # 59 | ``` 60 | The following attributes can be read from any block class instance: 61 | 1. `id`: the ID associated with the block. 62 | 2. `title`: the title associated with the block. 63 | 3. `parent_id`: the parent ID of the block. 64 | 4. `type`: the type of the block. 65 | 66 | To update the title of the page: 67 | ![Update the title of a page](https://github.com/danmurphy1217/notion-ruby/blob/master/gifs/change_title.gif) 68 | 69 | ## Retrieving a CollectionView Page 70 | This is achieved by passing the ID of the Collection View to the `get_page` method. Currently, the full URL of a Collection View Page is not supported (next up on the features list!). Once you retrieve the Collection View Page, all of the methods exposed to a normal Collection View instance are available (such as `.rows`, `.row()`, and all else outlined in [Updating a Collection](#updating-collection-view-cells)). 71 | ## Retrieving a Block within the Page 72 | Now that you have retrieved a Notion Page, you have full access to the blocks on that page. You can retrieve a specific block or collection view, retrieve all children IDs (array of children IDs), or retrieve all children (array of children class instances). 73 | 74 | ### Get a Block 75 | To retrieve a specific block, you can use the `get_block` method. This method accepts the ID of the block (formatted or not), and will return the block as an instantiated class instance: 76 | ```ruby 77 | @page = @client.get_page("https://www.notion.so/danmurphy/TEST-PAGE-d2ce338f19e847f586bd17679f490e66") 78 | @page.get_block("2cbbe0bf-34cd-409b-9162-64284b33e526") 79 | # 80 | ``` 81 | Any Notion Block has access to the following methods: 82 | 83 | 1. `title=` → change the title of a block. 84 | ```ruby 85 | >>> @block = @client.get_block("2cbbe0bf-34cd-409b-9162-64284b33e526") 86 | >>> @block.title # get the current title... 87 | "TEST" 88 | >>> @block.title= "New Title Here" # lets update it... 89 | >>> @block.title 90 | "New Title Here" 91 | ``` 92 | For example: 93 | ![Update the title of a block](https://github.com/danmurphy1217/notion-ruby/blob/master/gifs/change%20block%20title.gif) 94 | 2. `convert` → convert a block to a different type. 95 | ```ruby 96 | >>> @block = @client.get_block("2cbbe0bf-34cd-409b-9162-64284b33e526") 97 | >>> @block.type 98 | "text" 99 | >>> @new_block = @block.convert(NotionAPI::CalloutBlock) 100 | >>> @new_block.type 101 | "callout" 102 | >>> @new_block # new class instance returned... 103 | # 104 | ``` 105 | For example: 106 | ![Convert a page](https://github.com/danmurphy1217/notion-ruby/blob/master/gifs/change%20to%20todo%2C%20check.gif) 107 | 108 | 3. `duplicate`→ duplicate the current block. 109 | ```ruby 110 | >>> @block = @client.get_block("2cbbe0bf-34cd-409b-9162-64284b33e526") 111 | >>> @block.duplicate # block is duplicated and placed directly after the current block 112 | >>> @block.duplicate("f13da22b-9012-4c49-ac41-6b7f97bd519e") # the duplicated block is placed after 'f13da22b-9012-4c49-ac41-6b7f97bd519e' 113 | ``` 114 | For example: 115 | ![Convert a page](https://github.com/danmurphy1217/notion-ruby/blob/master/gifs/duplicate.gif) 116 | 4. `move` → move a block to another location. 117 | ```ruby 118 | >>> @block = @client.get_block("2cbbe0bf-34cd-409b-9162-64284b33e526") 119 | >>> @target_block = @client.get_block("c3ce468f-11e3-48g5-87be-27679g491e66") 120 | >>> @block.move(@target_block) # @block moved to **after** @target_block 121 | >>> @block.move(@target_block, "before") # @block moved to **before** @target_block 122 | ``` 123 | For example: 124 | ![move a block](https://github.com/danmurphy1217/notion-ruby/blob/master/gifs/move_before_and_after.gif) 125 | ### Get a Collection View 126 | To retrieve a collection, you use the `get_collection` method. This method is designed to work with Table collections, but the codebase is actively being updated to support others: 127 | ```ruby 128 | >>> @page = @client.get_page("https://www.notion.so/danmurphy/TEST-PAGE-d2ce338f19e847f586bd17679f490e66") 129 | >>> @page.get_collection("34d03794-ecdd-e42d-bb76-7e4aa77b6503") 130 | # 131 | ``` 132 | Any Notion Block has access to the following methods: 133 | 1. `row_ids` → retrieve the IDs associated with each row. 134 | ```ruby 135 | >>> @collection = @page.get_collection("34d03794-ecdd-e42d-bb76-7e4aa77b6503") 136 | >>> @collection.row_ids 137 | ent.rb 138 | ["785f4e24-e489-a316-50cf-b0b100c6673a", "78642d95-da23-744c-b084-46d039927bba", "96dff83c-6961-894c-39c2-c2c8bfcbfa90", "87ae8ae7-5518-fbe1-748e-eb690c707fac",..., "5a50bdd4-69c5-0708-5093-b135676e83c1", "ff9b8b89-1fed-f955-4afa-5a071198b0ee", "721fe76a-9e3c-d348-8324-994c95d77b2e"] 139 | ``` 140 | 2. `rows` → retrieve each Row, returned as an array of TableRowInstance classes. 141 | ```ruby 142 | >>> @collection = @page.get_collection("34d03794-ecdd-e42d-bb76-7e4aa77b6503") 143 | >>> @collection.rows 144 | #,..., #] 145 | ``` 146 | 3. `row("")` → retrieve a specific row. 147 | ```ruby 148 | >>> @collection = @page.get_collection("34d03794-ecdd-e42d-bb76-7e4aa77b6503") 149 | >>> @collection.row("f1c7077f-44a9-113d-a156-90ab6880c3e2") 150 | {"age"=>[9], "vin"=>["1C6SRFLT1MN591852"], "body"=>["4D Crew Cab"], "fuel"=>["Gasoline"], "make"=>["Ram"], "msrp"=>[64935], "year"=>[2021], "model"=>[1500], "price"=>[59688], "stock"=>["21R14"], "dealerid"=>["MP2964D"], "colour"=>["Bright White Clearcoat"], "engine"=>["HEMI 5.7L V8 Multi Displacement VVT"], "photos"=>["http://vehicle-photos-published.vauto.com/d0/c2/dd/8b-1307-4c67-8d31-5a301764b875/image-1.jpg"], "series"=>["Rebel"], "newused"=>["N"], "city_mpg"=>[""],...,"engine_cylinder_ct"=>[8], "engine_displacement"=>[5.7], "photos_last_modified_date"=>["11/13/2020 8:16:56 AM"]} 151 | ``` 152 | 153 | ## Creating New Blocks 154 | Here's a high-level example: 155 | ![create a callout a block](https://github.com/danmurphy1217/notion-ruby/blob/master/gifs/create.gif) 156 | The block types available to the `create` method are: 157 | 1. `DividerBlock` 158 | 2. `TodoBlock` 159 | 3. `CodeBlock` 160 | 4. `HeaderBlock` 161 | 5. `SubHeaderBlock` 162 | 6. `SubSubHeaderBlock` 163 | 7. `PageBlock` 164 | 8. `ToggleBlock` 165 | 9. `BulletedBlock` 166 | 10. `NumberedBlock` 167 | 11. `QuoteBlock` 168 | 12. `CalloutBlock` 169 | 13. `LatexBlock` 170 | 14. `TextBlock` 171 | 15. `ImageBlock` 172 | 16. `TableOfContentsBlock` and 173 | 17. `LinkBlock` 174 | If you want to create a collection, utilize the `create_collection` method [defined below]. 175 | 176 | To create a new block, you have a few options: 177 | ### Create a block whose parent is the page 178 | If you want to create a new block whose parent ID is the **page**, call the `create` method on the PageBlock instance. 179 | 1. `@page.create(">> @page = @client.get_page("https://www.notion.so/danmurphy/Notion-API-Testing-66447bc817f044bc81ed3cf4802e9b00") 182 | >>> @page.create(NotionAPI::TextBlock, "Hiya!") 183 | # 184 | ``` 185 | 2. `@page.create(">> @page = @client.get_page("https://www.notion.so/danmurphy/Notion-API-Testing-66447bc817f044bc81ed3cf4802e9b00") 188 | >>> @page.create(NotionAPI::TextBlock, "Hiya!", "ee0a6531-44cd-439f-a68c-1bdccbebfc8a") 189 | # 190 | ``` 191 | 3. `@page.create(">> @page = @client.get_page("https://www.notion.so/danmurphy/Notion-API-Testing-66447bc817f044bc81ed3cf4802e9b00") 194 | >>> @page.create(NotionAPI::TextBlock, "Hiya!", "ee0a6531-44cd-439f-a68c-1bdccbebfc8a", "before") 195 | # 196 | ``` 197 | 4. `@page.create(, "title of block", options: { emoji: "chosen emoji" })` → create a new block with a chosen emoji for blocks with emojis (`PageBlock` and `CalloutBlock`). 198 | ```ruby 199 | >>> @page = @client.get_page("https://www.notion.so/danmurphy/Notion-API-Testing-66447bc817f044bc81ed3cf4802e9b00") 200 | >>> @page.create(NotionAPI::PageBlock, "Hiya!", options: { emoji: "🚀" }) 201 | # 202 | ``` 203 | 5. `@page.create(, "title of block", options: { content: "text or markdown" })` → create a new block with text or markdown content. 204 | ```ruby 205 | >>> @page = @client.get_page("https://www.notion.so/danmurphy/Notion-API-Testing-66447bc817f044bc81ed3cf4802e9b00") 206 | >>> @page.create(NotionAPI::PageBlock, "Hiya!", options: { content: "# Yippee Ki Yay\n\nJohn McClane" }) 207 | # 208 | ``` 209 | ### Create a block whose parent is another block 210 | If you want to create a nested block whose parent ID is **another block**, call the `create` method on that block. 211 | 1. `@block.create(">> @page = @client.get_page("https://www.notion.so/danmurphy/Notion-API-Testing-66447bc817f044bc81ed3cf4802e9b00") 214 | >>> @block = @page.get_block("2cbbe0bf-34cd-409b-9162-64284b33e526") 215 | >>> @block.create(NotionAPI::TextBlock, "Hiya!") # create a nested text block 216 | # 217 | ``` 218 | 2. `@block.create(">> @page = @client.get_page("https://www.notion.so/danmurphy/Notion-API-Testing-66447bc817f044bc81ed3cf4802e9b00") 221 | >>> @block = @page.get_block("2cbbe0bf-34cd-409b-9162-64284b33e526") 222 | >>> @block.create(NotionAPI::TextBlock, "Hiya!" "ae3d1c60-b9d1-0ac0-0fff-16d3fc8907a2") # create a nested text block after a specific child 223 | # 224 | ``` 225 | 3. `@block.create(">> @page = @client.get_page("https://www.notion.so/danmurphy/Notion-API-Testing-66447bc817f044bc81ed3cf4802e9b00") 228 | >>> @block = @page.get_block("2cbbe0bf-34cd-409b-9162-64284b33e526") 229 | >>> @block.create(NotionAPI::TextBlock, "Hiya!" "ae3d1c60-b9d1-0ac0-0fff-16d3fc8907a2", "before") # create a nested text block before a specific child 230 | # 231 | ``` 232 | The simplest way to describe this: the parent ID of the created block is the ID of the block the `create` method is invoked on. If the `create` method is invoked on a **PageBlock**, the block is a child of that page. If the `create` method is invoked on a block within the page, the block is a child of that block. 233 | 234 | ** NOTE: Notion only supports 'nesting' certain block types. If you try to nest a block that cannot be nested, it will fail. ** 235 | ## Creating New Collections 236 | Let's say we have the following JSON data: 237 | ```json 238 | [ 239 | { 240 | "emoji": "😀", 241 | "description": "grinning face", 242 | "category": "Smileys & Emotion", 243 | "aliases": ["grinning"], 244 | "tags": ["smile", "happy"], 245 | "unicode_version": "6.1", 246 | "ios_version": "6.0" 247 | }, 248 | { 249 | "emoji": "😃", 250 | "description": "grinning face with big eyes", 251 | "category": "Smileys & Emotion", 252 | "aliases": ["smiley"], 253 | "tags": ["happy", "joy", "haha"], 254 | "unicode_version": "6.0", 255 | "ios_version": "6.0" 256 | } 257 | ] 258 | ``` 259 | A new table collection view containing this data is created with the following code: 260 | ```ruby 261 | >>> @page = @client.get_page("https://www.notion.so/danmurphy/Notion-API-Testing-66447bc817f044bc81ed3cf4802e9b00") 262 | >>> @page.create_collection("table", "title for table", JSON.parse(File.read("./path/to/emoji_json_data.json"))) 263 | ``` 264 | Here's an example with a larger dataset: 265 | ![create a collection view table](https://github.com/danmurphy1217/notion-ruby/blob/master/gifs/create%20collection.gif) 266 | 267 | Additionally, say you already have a Table and want to add a new row with it containing the following data: 268 | ```ruby 269 | { 270 | "emoji": "😉", 271 | "description": "winking face", 272 | "category": "Smileys & Emotion", 273 | "aliases": ["wink"], 274 | "tags": ["flirt"], 275 | "unicode_version": "6.0", 276 | "ios_version": "6.0" 277 | } 278 | ``` 279 | ```ruby 280 | >>> @page = @client.get_page("https://www.notion.so/danmurphy/Notion-API-Testing-66447bc817f044bc81ed3cf4802e9b00") 281 | >>> @collection = @page.get_collection("f1664a99-165b-49cc-811c-84f37655908a") 282 | >>> @collection.add_row(JSON.parse(File.read("path/to/new_emoji_row.json"))) 283 | ``` 284 | 285 | The first argument passed to `create_collection` determines which type of collection view to create. In the above example, a "table" is created, but other supported options are: 286 | 1. list 287 | 2. board 288 | 3. calendar 289 | 4. timeline 290 | 5. gallery 291 | 292 | ## Updating Collection View Cells 293 | When you retrieve a `CollectionViewRow` instance with `.row()` or a list of `CollectionViewRow` instances with `.rows`, a handful of methods are created. Each row instance has access attributes that represent the properties in the Notion Collection View. So, let's say we are working with the following Notion Collection View: 294 | | emoji | description | category | aliases | tags | unicode_version | ios_version | 295 | |-------|--------------|---------------------|---------|---------|-----------------|-------------| 296 | | 😉 | "winking face" | "Smileys & Emotion" | "wink" | "flirt" | "6.0" | "6.0" | 297 | 298 | If you wanted to update the unicode and ios versions, you could use the following code: 299 | ```ruby 300 | >>> collection_view = @page.get_collection("1234567") # the ID of the collection block is 1234567 301 | >>> rows = collection_view.rows 302 | >>> row[0].unicode_version = "updated version here!" 303 | >>> row[0].ios_version = "I was updated too!" 304 | ``` 305 | Now, your Collection View will look like this: 306 | | emoji | description | category | aliases | tags | unicode_version | ios_version | 307 | |-------|--------------|---------------------|---------|---------|-----------------|-------------| 308 | | 😉 | "winking face" | "Smileys & Emotion" | "wink" | "flirt" | "updated version here!" | "I was updated too!" | 309 | 310 | You can also add new rows with the `.add_row({})` method and add new properties with the `.add_property("name_of_property", "type_of_property")` method. 311 | 312 | **One important thing to be aware of:** 313 | When adding a row with `.add_row`, the hash of data passed must be in the same order as it appears in your Notion Collection View. 314 | ## Troubleshooting 315 | ### No results returned when attempting to get a page 316 | If an empty hash is returned when you attempt to retrieve a Notion page, you'll need to include the `x-notion-active-user-header` when instantiating the Notion Client. 317 | The endpoint used by this wrapper to load a page is `/loadPageChunk`, check out the request headers in your developer tools Network tab. 318 | 319 | From here, you can instantiate the Notion Client with the following code: 320 | ```ruby 321 | >>> @client = NotionAPI::Client.new( 322 | "", 323 | "" 324 | ) 325 | ``` 326 | ### Retrieve a full-page Collection View 327 | A full-page collection view must have a URL that follows the below pattern: 328 | https://www.notion.so/danmurphy/[page-id]?v=[view-id] 329 | Then, it can be retrieved with the following code: 330 | ```ruby 331 | >>> @client = NotionAPI::Client.new( 332 | "" 333 | ) 334 | >>> @client.get_page("https://www.notion.so/danmurphy/[page-id]?v=[view-id]") 335 | ``` 336 | ### Linking to another page 337 | You can create a block that links to another notion page with the following syntax: 338 | ```ruby 339 | @client = NotionAPI::Client.new(ENV["token_v2"]) 340 | @page = @client.get_page("https://www.notion.so/danmurphy/Testing-227581d35fc94fa1a5f9fda1e8478d1e") 341 | @page.create(NotionAPI::LinkBlock, "ea93213d1f21439c870fbe91503e76fe") 342 | ``` 343 | This example will create a `LinkBlock` on the `Testing` page to the page with ID `ea93213d1f21439c870fbe91503e76fe`. 344 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake/testtask' 2 | 3 | Rake::TestTask.new do |t| 4 | t.libs << 'test' 5 | t.verbose = true 6 | end 7 | 8 | desc "Run Unit Tests" 9 | task :default => :test -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | markdown: GFM 2 | plugins: 3 | - jekyll-relative-links 4 | relative_links: 5 | enabled: true 6 | collections: true 7 | include: 8 | - README.md 9 | - LICENSE.md 10 | -------------------------------------------------------------------------------- /gifs/change block title.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danmurphy1217/notion-ruby/4698b79ab6359dc63be1e44743cf7cc6fdedd05d/gifs/change block title.gif -------------------------------------------------------------------------------- /gifs/change to todo, check.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danmurphy1217/notion-ruby/4698b79ab6359dc63be1e44743cf7cc6fdedd05d/gifs/change to todo, check.gif -------------------------------------------------------------------------------- /gifs/change_title.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danmurphy1217/notion-ruby/4698b79ab6359dc63be1e44743cf7cc6fdedd05d/gifs/change_title.gif -------------------------------------------------------------------------------- /gifs/create collection.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danmurphy1217/notion-ruby/4698b79ab6359dc63be1e44743cf7cc6fdedd05d/gifs/create collection.gif -------------------------------------------------------------------------------- /gifs/create.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danmurphy1217/notion-ruby/4698b79ab6359dc63be1e44743cf7cc6fdedd05d/gifs/create.gif -------------------------------------------------------------------------------- /gifs/duplicate.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danmurphy1217/notion-ruby/4698b79ab6359dc63be1e44743cf7cc6fdedd05d/gifs/duplicate.gif -------------------------------------------------------------------------------- /gifs/get_children_title.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danmurphy1217/notion-ruby/4698b79ab6359dc63be1e44743cf7cc6fdedd05d/gifs/get_children_title.gif -------------------------------------------------------------------------------- /gifs/move_before_and_after.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danmurphy1217/notion-ruby/4698b79ab6359dc63be1e44743cf7cc6fdedd05d/gifs/move_before_and_after.gif -------------------------------------------------------------------------------- /lib/notion_api.rb: -------------------------------------------------------------------------------- 1 | require "notion_api/client" 2 | require "notion_api/core" 3 | require "notion_api/blocks" 4 | require "notion_api/utils" 5 | require "notion_api/markdown" -------------------------------------------------------------------------------- /lib/notion_api/blocks.rb: -------------------------------------------------------------------------------- 1 | require_relative "notion_types/template" 2 | require_relative "notion_types/bulleted_block" 3 | require_relative "notion_types/callout_block" 4 | require_relative "notion_types/code_block" 5 | require_relative "notion_types/collection_view_blocks" 6 | require_relative "notion_types/column_list_block" 7 | require_relative "notion_types/divider_block" 8 | require_relative "notion_types/quote_block" 9 | require_relative "notion_types/page_block" 10 | require_relative "notion_types/image_block" 11 | require_relative "notion_types/latex_block" 12 | require_relative "notion_types/numbered_block" 13 | require_relative "notion_types/header_block" 14 | require_relative "notion_types/sub_header_block" 15 | require_relative "notion_types/sub_sub_header" 16 | require_relative "notion_types/table_of_contents_block" 17 | require_relative "notion_types/text_block" 18 | require_relative "notion_types/todo_block" 19 | require_relative "notion_types/toggle_block" 20 | require_relative "notion_types/link_block" 21 | 22 | Classes = NotionAPI.constants.select { |c| NotionAPI.const_get(c).is_a? Class and c.to_s != 'BlockTemplate' and c.to_s != 'Core' and c.to_s !='Client' } 23 | notion_types = [] 24 | Classes.each { |cls| notion_types.push(NotionAPI.const_get(cls).notion_type) } 25 | BLOCK_TYPES = notion_types.zip(Classes).to_h -------------------------------------------------------------------------------- /lib/notion_api/client.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "core" 4 | require_relative "blocks" 5 | 6 | module NotionAPI 7 | # acts as the 'main interface' to the methods of this package. 8 | class Client < Core 9 | attr_reader :token_v2, :active_user_header 10 | 11 | def initialize(token_v2, active_user_header = nil) 12 | @token_v2 = token_v2 13 | @active_user_header = active_user_header 14 | super(token_v2, active_user_header) 15 | end 16 | end 17 | end -------------------------------------------------------------------------------- /lib/notion_api/core.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "utils" 4 | require "httparty" 5 | 6 | module NotionAPI 7 | # the initial methods available to an instantiated Cloent object are defined 8 | class Core 9 | include Utils 10 | @options = { "cookies" => { :token_v2 => nil, "x-active-user-header" => nil }, "headers" => { "Content-Type" => "application/json" } } 11 | @type_whitelist = "divider" 12 | 13 | class << self 14 | attr_reader :options, :type_whitelist, :token_v2, :active_user_header 15 | end 16 | 17 | attr_reader :clean_id, :cookies, :headers 18 | 19 | def initialize(token_v2, active_user_header) 20 | @@token_v2 = token_v2 21 | @@active_user_header = active_user_header 22 | end 23 | 24 | def get_page(url_or_id) 25 | # ! retrieve a Notion Page Block and return its instantiated class object. 26 | # ! url_or_id -> the block ID or URL : ``str`` 27 | clean_id = extract_id(url_or_id) 28 | 29 | request_body = { 30 | pageId: clean_id, 31 | chunkNumber: 0, 32 | limit: 100, 33 | verticalColumns: false, 34 | } 35 | jsonified_record_response = get_all_block_info(request_body) 36 | 37 | block_type = extract_type(clean_id, jsonified_record_response) 38 | block_parent_id = extract_parent_id(clean_id, jsonified_record_response) 39 | 40 | raise ArgumentError, "the URL or ID passed to the get_page method must be that of a Page Block." if !["collection_view_page", "page"].include?(block_type) 41 | 42 | get_instantiated_instance_for(url_or_id, block_type, clean_id, block_parent_id, jsonified_record_response) 43 | end 44 | 45 | def children(url_or_id = @id) 46 | # ! retrieve the children of a block. If the block has no children, return []. If it does, return the instantiated class objects associated with each child. 47 | # ! url_or_id -> the block ID or URL : ``str`` 48 | 49 | children_ids = children_ids(url_or_id) 50 | if children_ids.empty? 51 | [] 52 | else 53 | children_class_instances = [] 54 | children_ids.each { |child| children_class_instances.push(get(child)) } 55 | children_class_instances 56 | end 57 | end 58 | 59 | def children_ids(url_or_id = @id) 60 | # ! retrieve the children IDs of a block. 61 | # ! url_or_id -> the block ID or URL : ``str`` 62 | clean_id = extract_id(url_or_id) 63 | request_body = { 64 | pageId: clean_id, 65 | chunkNumber: 0, 66 | limit: 100, 67 | verticalColumns: false, 68 | } 69 | jsonified_record_response = get_all_block_info(request_body) 70 | 71 | # if no content, returns empty list 72 | jsonified_record_response["block"][clean_id]["value"]["content"] || [] 73 | end 74 | 75 | def extract_id(url_or_id) 76 | # ! parse and clean the URL or ID object provided. 77 | # ! url_or_id -> the block ID or URL : ``str`` 78 | http_or_https = url_or_id.match(/^(http|https)/) # true if http or https in url_or_id... 79 | collection_view_match = url_or_id.match(/(\?v=)/) 80 | 81 | if (url_or_id.length == 36) && ((url_or_id.split("-").length == 5) && !http_or_https) 82 | # passes if url_or_id is perfectly formatted already... 83 | url_or_id 84 | elsif (http_or_https && (url_or_id.split("-").last.length == 32)) || (!http_or_https && (url_or_id.length == 32)) || (collection_view_match) 85 | # passes if either: 86 | # 1. a URL is passed as url_or_id and the ID at the end is 32 characters long or 87 | # 2. a URL is not passed and the ID length is 32 [aka unformatted] 88 | pattern = [8, 13, 18, 23] 89 | if collection_view_match 90 | id_without_view = url_or_id.split("?")[0] 91 | clean_id = id_without_view.split("/").last 92 | pattern.each { |index| clean_id.insert(index, "-") } 93 | clean_id 94 | else 95 | id = url_or_id.split("-").last 96 | pattern.each { |index| id.insert(index, "-") } 97 | id 98 | end 99 | else 100 | raise ArgumentError, "Expected a Notion page URL or a page ID. Please consult the documentation for further information." 101 | end 102 | end 103 | 104 | private 105 | 106 | def get_notion_id(body) 107 | # ! retrieves a users ID from the headers of a Notion response object. 108 | # ! body -> the body to send in the request : ``Hash`` 109 | Core.options["cookies"][:token_v2] = @@token_v2 110 | Core.options["headers"]["x-notion-active-user-header"] = @@active_user_header 111 | cookies = Core.options["cookies"] 112 | headers = Core.options["headers"] 113 | request_url = URLS[:GET_BLOCK] 114 | 115 | response = HTTParty.post( 116 | request_url, 117 | body: body.to_json, 118 | cookies: cookies, 119 | headers: headers, 120 | ) 121 | response.headers["x-notion-user-id"] 122 | end 123 | 124 | def get_last_page_block_id(url_or_id) 125 | # ! retrieve and return the last child ID of a block. 126 | # ! url_or_id -> the block ID or URL : ``str`` 127 | children_ids(url_or_id).empty? ? [] : children_ids(url_or_id)[-1] 128 | end 129 | 130 | def get_block_props_and_format(clean_id, block_title) 131 | request_body = { 132 | pageId: clean_id, 133 | chunkNumber: 0, 134 | limit: 100, 135 | verticalColumns: false, 136 | } 137 | jsonified_record_response = get_all_block_info(request_body) 138 | 139 | properties = jsonified_record_response["block"][clean_id]["value"]["properties"] 140 | formats = jsonified_record_response["block"][clean_id]["value"]["format"] 141 | return { 142 | :properties => properties, 143 | :format => formats, 144 | } 145 | end 146 | 147 | def get_all_block_info(body, i = 0) 148 | # ! retrieves all info pertaining to a block Id. 149 | # ! clean_id -> the block ID or URL cleaned : ``str`` 150 | Core.options["cookies"][:token_v2] = @@token_v2 151 | Core.options["headers"]["x-notion-active-user-header"] = @@active_user_header 152 | cookies = Core.options["cookies"] 153 | headers = Core.options["headers"] 154 | request_url = URLS[:GET_BLOCK] 155 | 156 | response = HTTParty.post( 157 | request_url, 158 | body: body.to_json, 159 | cookies: cookies, 160 | headers: headers, 161 | ) 162 | 163 | jsonified_record_response = JSON.parse(response.body)["recordMap"] 164 | response_invalid = (!jsonified_record_response || jsonified_record_response.empty? || jsonified_record_response["block"].empty?) 165 | 166 | if i < 10 && response_invalid 167 | i = i + 1 168 | return get_all_block_info(body, i) 169 | else 170 | if i == 10 && response_invalid 171 | raise InvalidClientInstantiationError, "Attempted to retrieve block 10 times and received an empty response each time. Please make sure you have a valid token_v2 value set. If you do, then try setting the 'active_user_header' variable as well." 172 | else 173 | return jsonified_record_response 174 | end 175 | end 176 | end 177 | 178 | def filter_nil_blocks(jsonified_record_response) 179 | # ! removes any blocks that are empty [i.e. have no title / content] 180 | # ! jsonified_record_responses -> parsed JSON representation of a notion response object : ``Json`` 181 | jsonified_record_response.empty? || jsonified_record_response["block"].empty? ? nil : jsonified_record_response["block"] 182 | end 183 | 184 | def extract_title(clean_id, jsonified_record_response) 185 | # ! extract title from core JSON Notion response object. 186 | # ! clean_id -> the cleaned block ID: ``str`` 187 | # ! jsonified_record_response -> parsed JSON representation of a notion response object : ``Json`` 188 | filter_nil_blocks = filter_nil_blocks(jsonified_record_response) 189 | if filter_nil_blocks.nil? || filter_nil_blocks[clean_id].nil? || filter_nil_blocks[clean_id]["value"]["properties"].nil? 190 | nil 191 | else 192 | # titles for images are called source, while titles for text-based blocks are called title, so lets dynamically grab it 193 | # https://stackoverflow.com/questions/23765996/get-all-keys-from-ruby-hash/23766007 194 | title_value = filter_nil_blocks[clean_id]["value"]["properties"].keys[0] 195 | Core.type_whitelist.include?(filter_nil_blocks[clean_id]["value"]["type"]) ? nil : jsonified_record_response["block"][clean_id]["value"]["properties"][title_value].flatten[0] 196 | end 197 | end 198 | 199 | def extract_collection_title(_clean_id, collection_id, jsonified_record_response) 200 | # ! extract title from core JSON Notion response object. 201 | # ! clean_id -> the cleaned block ID: ``str`` 202 | # ! collection_id -> the collection ID: ``str`` 203 | # ! jsonified_record_response -> parsed JSON representation of a notion response object : ``Json`` 204 | jsonified_record_response["collection"][collection_id]["value"]["name"].flatten.join if jsonified_record_response["collection"] and jsonified_record_response["collection"][collection_id]["value"]["name"] 205 | end 206 | 207 | def extract_type(clean_id, jsonified_record_response) 208 | # ! extract type from core JSON response object. 209 | # ! clean_id -> the block ID or URL cleaned : ``str`` 210 | # ! jsonified_record_response -> parsed JSON representation of a notion response object : ``Json`` 211 | filter_nil_blocks = filter_nil_blocks(jsonified_record_response) 212 | if filter_nil_blocks.nil? 213 | nil 214 | else 215 | filter_nil_blocks[clean_id]["value"]["type"] 216 | end 217 | end 218 | 219 | def extract_parent_id(clean_id, jsonified_record_response) 220 | # ! extract parent ID from core JSON response object. 221 | # ! clean_id -> the block ID or URL cleaned : ``str`` 222 | # ! jsonified_record_response -> parsed JSON representation of a notion response object : ``Json`` 223 | jsonified_record_response.empty? || jsonified_record_response["block"].empty? ? {} : jsonified_record_response["block"][clean_id]["value"]["parent_id"] 224 | end 225 | 226 | def extract_collection_id(clean_id, jsonified_record_response) 227 | # ! extract the collection ID 228 | # ! clean_id -> the block ID or URL cleaned : ``str`` 229 | # ! jsonified_record_response -> parsed JSON representation of a notion response object : ``Json`` 230 | jsonified_record_response["block"][clean_id]["value"]["collection_id"] 231 | end 232 | 233 | def extract_view_ids(clean_id, jsonified_record_response) 234 | jsonified_record_response["block"][clean_id]["value"]["view_ids"] || [] 235 | end 236 | 237 | def extract_view_id(url_or_id, clean_id, jsonified_record_response) 238 | # ! extract the view ID 239 | # ! clean_id -> the block ID or URL cleaned : ``str`` 240 | # ! jsonified_record_response -> parsed JSON representation of a notion response object : ``Json`` 241 | collection_view_match = /^https?:\/\/www.notion.so\/(.+\/)?(?.+)\?v=(?.+)$/i.match(url_or_id) 242 | view_ids = extract_view_ids(clean_id, jsonified_record_response) 243 | view_id = view_ids[0] 244 | 245 | if collection_view_match && collection_view_match[:id] && collection_view_match[:view_id] 246 | clean_match_id = extract_id(collection_view_match[:id]) 247 | clean_match_view_id = extract_id(collection_view_match[:view_id]) 248 | 249 | if clean_id == clean_match_id && view_ids.include?(clean_match_view_id) 250 | view_id = clean_match_view_id 251 | end 252 | end 253 | 254 | view_id 255 | end 256 | 257 | def extract_query(view_id, schema, jsonified_record_response) 258 | # ! extract the query 259 | # ! view_id -> the view ID : ``str`` 260 | # ! jsonified_record_response -> parsed JSON representation of a notion response object : ``Json`` 261 | query = jsonified_record_response["collection_view"][view_id]["value"]["query2"] || {} 262 | return query unless query.dig("filter", "filters") 263 | 264 | properties = schema.keys 265 | query["filter"]["filters"] = query.dig("filter", "filters").filter do |filter| 266 | properties.include?(filter["property"]) 267 | end 268 | 269 | query 270 | end 271 | 272 | # def extract_id(url_or_id) 273 | # # ! parse and clean the URL or ID object provided. 274 | # # ! url_or_id -> the block ID or URL : ``str`` 275 | # http_or_https = url_or_id.match(/^(http|https)/) # true if http or https in url_or_id... 276 | # collection_view_match = url_or_id.match(/(\?v=)/) 277 | 278 | # if (url_or_id.length == 36) && ((url_or_id.split("-").length == 5) && !http_or_https) 279 | # # passes if url_or_id is perfectly formatted already... 280 | # url_or_id 281 | # elsif (http_or_https && (url_or_id.split("-").last.length == 32)) || (!http_or_https && (url_or_id.length == 32)) || (collection_view_match) 282 | # # passes if either: 283 | # # 1. a URL is passed as url_or_id and the ID at the end is 32 characters long or 284 | # # 2. a URL is not passed and the ID length is 32 [aka unformatted] 285 | # pattern = [8, 13, 18, 23] 286 | # if collection_view_match 287 | # id_without_view = url_or_id.split("?")[0] 288 | # clean_id = id_without_view.split("/").last 289 | # pattern.each { |index| clean_id.insert(index, "-") } 290 | # clean_id 291 | # else 292 | # id = url_or_id.split("-").last 293 | # pattern.each { |index| id.insert(index, "-") } 294 | # id 295 | # end 296 | # else 297 | # raise ArgumentError, "Expected a Notion page URL or a page ID. Please consult the documentation for further information." 298 | # end 299 | # end 300 | 301 | def query_collection(collection_id, view_id, options: {}) 302 | # ! retrieve the collection scehma. Useful for 'building' the backbone for a table. 303 | # ! collection_id -> the collection ID : ``str`` 304 | # ! view_id -> the view ID : ``str`` 305 | request_body = Utils::CollectionViewComponents.query_collection_body( 306 | collection_id, view_id, options: options 307 | ) 308 | 309 | request_url = URLS[:GET_COLLECTION] 310 | cookies = Core.options["cookies"] 311 | headers = Core.options["headers"] 312 | 313 | HTTParty.post( 314 | request_url, 315 | body: request_body.to_json, 316 | cookies: cookies, 317 | headers: headers, 318 | ) 319 | end 320 | 321 | def extract_collection_schema(collection_id, view_id, response = {}) 322 | # ! retrieve the collection schema. Useful for 'building' the backbone for a table. 323 | # ! collection_id -> the collection ID : ``str`` 324 | # ! view_id -> the view ID : ``str`` 325 | if response.empty? 326 | response = query_collection(collection_id, view_id) 327 | response["recordMap"]["collection"][collection_id]["value"]["schema"] 328 | else 329 | response["collection"][collection_id]["value"]["schema"] 330 | end 331 | end 332 | 333 | def extract_collection_data(collection_id, view_id) 334 | # ! retrieve the collection schema. Useful for 'building' the backbone for a table. 335 | # ! collection_id -> the collection ID : ``str`` 336 | # ! view_id -> the view ID : ``str`` 337 | response = query_collection(collection_id, view_id) 338 | 339 | response["recordMap"] 340 | end 341 | 342 | def extract_page_information(page_meta = {}) 343 | # ! helper method for extracting information about a page block 344 | # ! page_meta -> hash containing data points useful for the extraction of a page blocks information. 345 | # ! This should include clean_id, jsonified_record_response, and parent_id 346 | clean_id = page_meta.fetch(:clean_id) 347 | jsonified_record_response = page_meta.fetch(:jsonified_record_response) 348 | block_parent_id = page_meta.fetch(:parent_id) 349 | 350 | block_title = extract_title(clean_id, jsonified_record_response) 351 | PageBlock.new(clean_id, block_title, block_parent_id) 352 | end 353 | 354 | def extract_collection_view_page_information(page_meta = {}) 355 | # ! helper method for extracting information about a Collection View page block 356 | # ! page_meta -> hash containing data points useful for the extraction of a page blocks information. 357 | # ! This should include clean_id, jsonified_record_response, and parent_id 358 | url_or_id = page_meta.fetch(:url_or_id) 359 | clean_id = page_meta.fetch(:clean_id) 360 | jsonified_record_response = page_meta.fetch(:jsonified_record_response) 361 | block_parent_id = page_meta.fetch(:parent_id) 362 | 363 | collection_id = extract_collection_id(clean_id, jsonified_record_response) 364 | block_title = extract_collection_title(clean_id, collection_id, jsonified_record_response) 365 | view_id = extract_view_id(url_or_id, clean_id, jsonified_record_response) 366 | schema = extract_collection_schema(collection_id, view_id, jsonified_record_response) 367 | query = extract_query(view_id, schema, jsonified_record_response) 368 | column_names = NotionAPI::CollectionView.extract_collection_view_column_names(schema) 369 | 370 | collection_view_page = CollectionViewPage.new(clean_id, block_title, block_parent_id, collection_id, view_id, query) 371 | collection_view_page.instance_variable_set(:@column_names, column_names) 372 | CollectionView.class_eval { attr_reader :column_names } 373 | collection_view_page 374 | end 375 | 376 | def get_instantiated_instance_for(url_or_id, block_type, clean_id, parent_id, jsonified_record_response) 377 | case block_type 378 | when "page" then extract_page_information(clean_id: clean_id, parent_id: parent_id, jsonified_record_response: jsonified_record_response) 379 | when "collection_view_page" then extract_collection_view_page_information(url_or_id: url_or_id, clean_id: clean_id, parent_id: parent_id, jsonified_record_response: jsonified_record_response) 380 | end 381 | end 382 | 383 | class InvalidClientInstantiationError < StandardError 384 | def initialize(msg = "Custom exception that is raised when an invalid property type is passed as a mapping.", exception_type = "instantiation_type") 385 | @exception_type = exception_type 386 | super(msg) 387 | end 388 | end 389 | end 390 | end 391 | -------------------------------------------------------------------------------- /lib/notion_api/markdown.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Notion 4 | class Markdown 5 | # ! convert user input formatted with markdown (**hi** is bold, *hi* is italic, etc.) -> notion-understood stylings. 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/notion_api/notion_types/bulleted_block.rb: -------------------------------------------------------------------------------- 1 | require_relative "template" 2 | 3 | module NotionAPI 4 | # Bullet list block: best for an unordered list 5 | class BulletedBlock < BlockTemplate 6 | @notion_type = "bulleted_list" 7 | @type = "bulleted_list" 8 | 9 | def type 10 | NotionAPI::BulletedBlock.notion_type 11 | end 12 | 13 | class << self 14 | attr_reader :notion_type, :type 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/notion_api/notion_types/callout_block.rb: -------------------------------------------------------------------------------- 1 | module NotionAPI 2 | 3 | # same as quote... works similarly to page block 4 | class CalloutBlock < BlockTemplate 5 | @notion_type = 'callout' 6 | @type = 'callout' 7 | 8 | def type 9 | NotionAPI::CalloutBlock.notion_type 10 | end 11 | 12 | class << self 13 | attr_reader :notion_type, :type 14 | end 15 | end 16 | end -------------------------------------------------------------------------------- /lib/notion_api/notion_types/code_block.rb: -------------------------------------------------------------------------------- 1 | module NotionAPI 2 | 3 | # Code block: used to store code, should be assigned a coding language. 4 | class CodeBlock < BlockTemplate 5 | @notion_type = "code" 6 | @type = "code" 7 | 8 | def type 9 | NotionAPI::CodeBlock.notion_type 10 | end 11 | 12 | class << self 13 | attr_reader :notion_type, :type 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/notion_api/notion_types/collection_view_blocks.rb: -------------------------------------------------------------------------------- 1 | module NotionAPI 2 | # collection views such as tables and timelines. 3 | class CollectionView < Core 4 | attr_reader :id, :title, :parent_id, :collection_id, :view_id, :query 5 | 6 | @notion_type = "collection_view" 7 | @type = "collection_view" 8 | 9 | def type 10 | NotionAPI::CollectionView.notion_type 11 | end 12 | 13 | class << self 14 | attr_reader :notion_type, :type 15 | end 16 | 17 | def initialize(id, title, parent_id, collection_id, view_id, query = {}) 18 | @id = id 19 | @title = title 20 | @parent_id = parent_id 21 | @collection_id = collection_id 22 | @view_id = view_id 23 | @query = query 24 | end 25 | 26 | def add_row(data) 27 | # ! add new row to Collection View table. 28 | # ! data -> data to add to table : ``hash`` 29 | 30 | cookies = Core.options["cookies"] 31 | headers = Core.options["headers"] 32 | 33 | request_id = extract_id(SecureRandom.hex(16)) 34 | transaction_id = extract_id(SecureRandom.hex(16)) 35 | space_id = extract_id(SecureRandom.hex(16)) 36 | new_block_id = extract_id(SecureRandom.hex(16)) 37 | collection_data = extract_collection_data(collection_id, view_id) 38 | last_row_id = collection_data["collection_view"][@view_id]["value"]["page_sort"][-1] 39 | schema = collection_data["collection"][collection_id]["value"]["schema"] 40 | keys = schema.keys 41 | col_map = {} 42 | keys.map { |key| col_map[schema[key]["name"]] = key } 43 | 44 | request_ids = { 45 | request_id: request_id, 46 | transaction_id: transaction_id, 47 | space_id: space_id, 48 | } 49 | 50 | instantiate_row = Utils::CollectionViewComponents.add_new_row(new_block_id) 51 | set_block_alive = Utils::CollectionViewComponents.set_collection_blocks_alive(new_block_id, @collection_id) 52 | new_block_edited_time = Utils::BlockComponents.last_edited_time(new_block_id) 53 | page_sort = Utils::BlockComponents.row_location_add(last_row_id, new_block_id, @view_id) 54 | 55 | operations = [ 56 | instantiate_row, 57 | set_block_alive, 58 | new_block_edited_time, 59 | page_sort, 60 | ] 61 | 62 | data.keys.each_with_index do |col_name, j| 63 | unless col_map.keys.include?(col_name.to_s); raise ArgumentError, "Column '#{col_name.to_s}' does not exist." end 64 | if %q[select multi_select].include?(schema[col_map[col_name.to_s]]["type"]) 65 | options = schema[col_map[col_name.to_s]]["options"].nil? ? [] : schema[col_map[col_name.to_s]]["options"].map { |option| option["value"] } 66 | multi_select_multi_options = data[col_name].split(",") 67 | multi_select_multi_options.each do |option| 68 | if !options.include?(option.strip) 69 | create_new_option = Utils::CollectionViewComponents.add_new_option(col_map[col_name.to_s], option.strip, @collection_id) 70 | operations.push(create_new_option) 71 | end 72 | end 73 | end 74 | child_component = Utils::CollectionViewComponents.insert_data(new_block_id, col_map[col_name.to_s], data[col_name], schema[col_map[col_name.to_s]]["type"]) 75 | operations.push(child_component) 76 | end 77 | 78 | request_url = URLS[:UPDATE_BLOCK] 79 | request_body = build_payload(operations, request_ids) 80 | response = HTTParty.post( 81 | request_url, 82 | body: request_body.to_json, 83 | cookies: cookies, 84 | headers: headers, 85 | ) 86 | 87 | unless response.code == 200; raise "There was an issue completing your request. Here is the response from Notion: #{response.body}, and here is the payload that was sent: #{operations}. 88 | Please try again, and if issues persist open an issue in GitHub."; end 89 | 90 | collection_row = NotionAPI::CollectionViewRow.new(new_block_id, @parent_id, @collection_id, @view_id) 91 | 92 | properties = {} 93 | data.keys.each do |col| 94 | properties[col_map[col.to_s]] = [[data[col]]] 95 | end 96 | 97 | collection_data["block"][collection_row.id] = { "role" => "editor", "value" => { "id" => collection_row.id, "version" => 12, "type" => "page", "properties" => properties, "created_time" => 1607253360000, "last_edited_time" => 1607253360000, "parent_id" => "dde513c6-2428-4a5d-a830-7a67fdbf6b48", "parent_table" => "collection", "alive" => true, "created_by_table" => "notion_user", "created_by_id" => "0c5f02f3-495d-4b73-b1c5-9f6fe03a8c26", "last_edited_by_table" => "notion_user", "last_edited_by_id" => "0c5f02f3-495d-4b73-b1c5-9f6fe03a8c26", "shard_id" => 955090, "space_id" => "f687f7de-7f4c-4a86-b109-941a8dae92d2" } } 98 | row_data = collection_data["block"][collection_row.id] 99 | create_singleton_methods_and_instance_variables(collection_row, row_data) 100 | 101 | collection_row 102 | end 103 | 104 | def add_property(name, type) 105 | # ! add a property (column) to the table. 106 | # ! name -> name of the property : ``str`` 107 | # ! type -> type of the property : ``str`` 108 | cookies = Core.options["cookies"] 109 | headers = Core.options["headers"] 110 | 111 | request_id = extract_id(SecureRandom.hex(16)) 112 | transaction_id = extract_id(SecureRandom.hex(16)) 113 | space_id = extract_id(SecureRandom.hex(16)) 114 | 115 | request_ids = { 116 | request_id: request_id, 117 | transaction_id: transaction_id, 118 | space_id: space_id, 119 | } 120 | 121 | # create updated schema 122 | schema = extract_collection_schema(@collection_id, @view_id) 123 | schema[name] = { 124 | name: name, 125 | type: type, 126 | } 127 | new_schema = { 128 | schema: schema, 129 | } 130 | 131 | add_collection_property = Utils::CollectionViewComponents.add_collection_property(@collection_id, new_schema) 132 | 133 | operations = [ 134 | add_collection_property, 135 | ] 136 | 137 | request_url = URLS[:UPDATE_BLOCK] 138 | request_body = build_payload(operations, request_ids) 139 | response = HTTParty.post( 140 | request_url, 141 | body: request_body.to_json, 142 | cookies: cookies, 143 | headers: headers, 144 | ) 145 | unless response.code == 200; raise "There was an issue completing your request. Here is the response from Notion: #{response.body}, and here is the payload that was sent: #{operations}. 146 | Please try again, and if issues persist open an issue in GitHub."; end 147 | 148 | true 149 | end 150 | 151 | def row(row_id) 152 | # ! retrieve a row from a CollectionView Table. 153 | # ! row_id -> the ID for the row to retrieve: ``str`` 154 | clean_id = extract_id(row_id) 155 | 156 | request_body = { 157 | pageId: clean_id, 158 | chunkNumber: 0, 159 | limit: 100, 160 | verticalColumns: false, 161 | } 162 | jsonified_record_response = get_all_block_info(request_body) 163 | 164 | collection_data = extract_collection_data(@collection_id, @view_id) 165 | schema = collection_data["collection"][collection_id]["value"]["schema"] 166 | column_names = NotionAPI::CollectionView.extract_collection_view_column_names(schema) 167 | 168 | collection_row = CollectionViewRow.new(row_id, @parent_id, @collection_id, @view_id) 169 | collection_row.instance_variable_set(:@column_names, column_names) 170 | CollectionViewRow.class_eval { attr_reader :column_names } 171 | 172 | row_data = collection_data["block"][collection_row.id] 173 | create_singleton_methods_and_instance_variables(collection_row, row_data) 174 | 175 | collection_row 176 | end 177 | 178 | def query_collection(collection_id = @collection_id, view_id = @view_id, options: {}) 179 | options[:query] = options[:query] || @query 180 | 181 | super(collection_id, view_id, options: options) 182 | end 183 | 184 | def complete_collection 185 | # ! retrieve all Collection View table rows recurcisely. 186 | @complete_collection ||= query_collection(@collection_id, @view_id) 187 | end 188 | 189 | def row_ids 190 | complete_collection['result']['blockIds'] 191 | end 192 | 193 | def rows 194 | # ! returns all rows as instantiated class instances. 195 | collection_data = complete_collection['recordMap'] 196 | schema = collection_data['collection'][collection_id]['value']['schema'] 197 | column_names = NotionAPI::CollectionView.extract_collection_view_column_names(schema) 198 | row_instances = row_ids.map { |row_id| NotionAPI::CollectionViewRow.new(row_id, @parent_id, @collection_id, @view_id) } 199 | clean_row_instances = row_instances.filter { |row| collection_data['block'][row.id] } 200 | clean_row_instances.each { |row| row.instance_variable_set(:@column_names, column_names) } 201 | CollectionViewRow.class_eval { attr_reader :column_names } 202 | 203 | clean_row_instances.each do |collection_row| 204 | row_data = collection_data['block'][collection_row.id] 205 | create_singleton_methods_and_instance_variables(collection_row, row_data, schema) 206 | end 207 | 208 | clean_row_instances 209 | end 210 | 211 | def create_singleton_methods_and_instance_variables(row, row_data, schema = nil) 212 | # ! creates singleton methods for each property in a CollectionView. 213 | # ! row -> the block ID of the 'row' to retrieve: ``str`` 214 | # ! row_data -> the data corresponding to that row, should be key-value pairs where the keys are the columns: ``hash`` 215 | unless schema 216 | collection_data = extract_collection_data(@collection_id, @view_id) 217 | schema = collection_data['collection'][collection_id]['value']['schema'] 218 | end 219 | 220 | column_mappings = schema.keys 221 | column_hash = {} 222 | column_names = column_mappings.map { |mapping| column_hash[mapping] = schema[mapping]["name"].downcase } 223 | 224 | column_hash.keys.each_with_index do |column, i| 225 | # loop over the column names... 226 | # set instance variables for each column, allowing the dev to 'read' the column value 227 | cleaned_column = clean_property_names(column_hash, column) 228 | 229 | if row_data["value"]["properties"].nil? or row_data["value"]["properties"][column].nil? 230 | value = "" 231 | else 232 | value = row_data["value"]["properties"][column][0][0] 233 | if ["‣"].include?(value.to_s) 234 | value = row_data["value"]["properties"][column][0][1].flatten[-1] 235 | end 236 | end 237 | 238 | row.instance_variable_set("@#{cleaned_column}", value) 239 | CollectionViewRow.class_eval { attr_reader cleaned_column } 240 | # then, define singleton methods for each column that are used to update the table cell 241 | row.define_singleton_method("#{cleaned_column}=") do |new_value| 242 | # neat way to get the name of the currently invoked method... 243 | parsed_method = __method__.to_s[0...-1].split("_").join(" ") 244 | cookies = Core.options["cookies"] 245 | headers = Core.options["headers"] 246 | 247 | request_id = extract_id(SecureRandom.hex(16)) 248 | transaction_id = extract_id(SecureRandom.hex(16)) 249 | space_id = extract_id(SecureRandom.hex(16)) 250 | 251 | request_ids = { 252 | request_id: request_id, 253 | transaction_id: transaction_id, 254 | space_id: space_id, 255 | } 256 | 257 | update_property_value_hash = Utils::CollectionViewComponents.update_property_value(@id, column_hash.key(parsed_method), new_value, schema[column_hash.key(parsed_method)]["type"]) 258 | 259 | operations = [ 260 | update_property_value_hash, 261 | ] 262 | 263 | if %q[select multi_select].include?(schema[column_hash.key(parsed_method)]["type"]) 264 | options = schema[column_hash.key(parsed_method)]["options"].nil? ? [] : schema[column_hash.key(parsed_method)]["options"].map { |option| option["value"] } 265 | multi_select_multi_options = new_value.split(",") 266 | multi_select_multi_options.each do |option| 267 | if !options.include?(option.strip) 268 | create_new_option = Utils::CollectionViewComponents.add_new_option(column_hash.key(parsed_method), option.strip, @collection_id) 269 | operations.push(create_new_option) 270 | end 271 | end 272 | end 273 | 274 | request_url = URLS[:UPDATE_BLOCK] 275 | request_body = build_payload(operations, request_ids) 276 | response = HTTParty.post( 277 | request_url, 278 | body: request_body.to_json, 279 | cookies: cookies, 280 | headers: headers, 281 | ) 282 | unless response.code == 200; raise "There was an issue completing your request. Here is the response from Notion: #{response.body}, and here is the payload that was sent: #{operations}. 283 | Please try again, and if issues persist open an issue in GitHub."; end 284 | 285 | # set the instance variable to the updated value! 286 | _ = row.instance_variable_set("@#{__method__.to_s[0...-1]}", new_value) 287 | row 288 | end 289 | end 290 | end 291 | 292 | def clean_property_names(prop_hash, prop_notion_name) 293 | # ! standardize property names by splitting the words in the property name into an array, removing non-alphanumeric 294 | # ! characters, downcasing, and then re-joining the array with underscores. 295 | # ! prop_hash -> hash of property notion names and property textual names: ``str`` 296 | # ! prop_notion_name -> the four-character long name of the notion property: ``str`` 297 | 298 | prop_hash[prop_notion_name].split(" ").map { |word| word.gsub(/[^a-z0-9]/i, "").downcase }.join("_").to_sym 299 | end 300 | 301 | def self.extract_collection_view_column_names(schema) 302 | # ! extract the column names of a Collection View 303 | # ! schema: the schema of the collection view 304 | column_mappings = schema.keys 305 | 306 | column_mappings.map { |mapping| schema[mapping]["name"] } 307 | end 308 | end 309 | 310 | # class that represents each row in a CollectionView 311 | class CollectionViewRow < Core 312 | @notion_type = "table_row" 313 | @type = "table_row" 314 | 315 | def type 316 | NotionAPI::CollectionViewRow.notion_type 317 | end 318 | 319 | def inspect 320 | "CollectionViewRow - id: #{self.id} - parent id: #{self.parent_id}" 321 | end 322 | 323 | class << self 324 | attr_reader :notion_type, :type, :parent_id 325 | end 326 | 327 | attr_reader :parent_id, :id 328 | 329 | def initialize(id, parent_id, collection_id, view_id) 330 | @id = id 331 | @parent_id = parent_id 332 | @collection_id = collection_id 333 | @view_id = view_id 334 | end 335 | end 336 | 337 | # class that represents a CollectionViewPage, inheriting all properties from CollectionView 338 | class CollectionViewPage < CollectionView 339 | @notion_type = "collection_view_page" 340 | @type = "collection_view_page" 341 | 342 | def type 343 | NotionAPI::CollectionViewRow.notion_type 344 | end 345 | 346 | class << self 347 | attr_reader :notion_type, :type, :parent_id 348 | end 349 | 350 | attr_reader :parent_id, :id 351 | end 352 | end 353 | -------------------------------------------------------------------------------- /lib/notion_api/notion_types/column_list_block.rb: -------------------------------------------------------------------------------- 1 | module NotionAPI 2 | 3 | # no use case for this yet. 4 | class ColumnListBlock < BlockTemplate 5 | @notion_type = "column_list" 6 | @type = "column_list" 7 | 8 | def type 9 | NotionAPI::ColumnListBlock.notion_type 10 | end 11 | 12 | class << self 13 | attr_reader :notion_type, :type 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/notion_api/notion_types/divider_block.rb: -------------------------------------------------------------------------------- 1 | module NotionAPI 2 | # divider block: --------- 3 | class DividerBlock < BlockTemplate 4 | @notion_type = "divider" 5 | @type = "divider" 6 | 7 | def type 8 | NotionAPI::DividerBlock.notion_type 9 | end 10 | 11 | class << self 12 | attr_reader :notion_type, :type 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/notion_api/notion_types/header_block.rb: -------------------------------------------------------------------------------- 1 | module NotionAPI 2 | 3 | # Header block: H1 4 | class HeaderBlock < BlockTemplate 5 | @notion_type = "header" 6 | @type = "header" 7 | 8 | def type 9 | NotionAPI::HeaderBlock.notion_type 10 | end 11 | 12 | class << self 13 | attr_reader :notion_type, :type 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/notion_api/notion_types/image_block.rb: -------------------------------------------------------------------------------- 1 | module NotionAPI 2 | 3 | # good for visual information 4 | class ImageBlock < BlockTemplate 5 | @notion_type = "image" 6 | @type = "image" 7 | 8 | def type 9 | NotionAPI::ImageBlock.notion_type 10 | end 11 | 12 | class << self 13 | attr_reader :notion_type, :type 14 | end 15 | 16 | private 17 | 18 | def self.create(block_id, new_block_id, block_title, target, position_command, request_ids, options) 19 | if !(options[:image]) 20 | raise ArgumentError, "Must specify image as an option. For example: .create(\"block type\", \"block title\", options: {image: \"https://image-domain.com\"})" 21 | else 22 | cookies = Core.options["cookies"] 23 | headers = Core.options["headers"] 24 | if options[:image].match(/^http:\/\/|^https:\/\//) 25 | create_hash = Utils::BlockComponents.create(new_block_id, self.notion_type) 26 | set_parent_alive_hash = Utils::BlockComponents.set_parent_to_alive(block_id, new_block_id) 27 | block_location_hash = Utils::BlockComponents.block_location_add(block_id, block_id, new_block_id, target, position_command) 28 | last_edited_time_parent_hash = Utils::BlockComponents.last_edited_time(block_id) 29 | last_edited_time_child_hash = Utils::BlockComponents.last_edited_time(block_id) 30 | title_hash = Utils::BlockComponents.title(new_block_id, block_title) 31 | source_url_hash = Utils::BlockComponents.source(new_block_id, options[:image]) 32 | display_source_url_hash = Utils::BlockComponents.display_source(new_block_id, options[:image]) 33 | 34 | operations = [ 35 | create_hash, 36 | set_parent_alive_hash, 37 | block_location_hash, 38 | last_edited_time_parent_hash, 39 | last_edited_time_child_hash, 40 | title_hash, 41 | source_url_hash, 42 | display_source_url_hash, 43 | ] 44 | 45 | request_url = URLS[:UPDATE_BLOCK] 46 | request_body = Utils::BlockComponents.build_payload(operations, request_ids) 47 | response = HTTParty.post( 48 | request_url, 49 | body: request_body.to_json, 50 | cookies: cookies, 51 | headers: headers, 52 | ) 53 | unless response.code == 200; raise "There was an issue completing your request. Here is the response from Notion: #{response.body}, and here is the payload that was sent: #{operations}. 54 | Please try again, and if issues persist open an issue in GitHub."; end 55 | 56 | self.new(new_block_id, block_title, block_id) 57 | else 58 | raise ArgumentError, "Currently, images can only be created through a public image URL." 59 | end 60 | end 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/notion_api/notion_types/latex_block.rb: -------------------------------------------------------------------------------- 1 | module NotionAPI 2 | 3 | # simiilar to code block but for mathematical functions. 4 | class LatexBlock < BlockTemplate 5 | @notion_type = "equation" 6 | @type = "equation" 7 | 8 | def type 9 | NotionAPI::LatexBlock.notion_type 10 | end 11 | 12 | class << self 13 | attr_reader :notion_type, :type 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/notion_api/notion_types/link_block.rb: -------------------------------------------------------------------------------- 1 | module NotionAPI 2 | 3 | # simiilar to code block but for mathematical functions. 4 | class LinkBlock < BlockTemplate 5 | @notion_type = "link_to_page" 6 | @type = "link_to_page" 7 | 8 | def type 9 | NotionAPI::LinkBlock.notion_type 10 | end 11 | 12 | class << self 13 | attr_reader :notion_type, :type 14 | end 15 | 16 | def self.create(block_id, new_block_id, block_title, target, position_command, request_ids, options) 17 | block_title = super.extract_id(block_title) 18 | 19 | cookies = Core.options["cookies"] 20 | headers = Core.options["headers"] 21 | 22 | create_block_hash = Utils::BlockComponents.create(new_block_id, self.notion_type) 23 | block_location_hash = Utils::BlockComponents.block_location_add(block_id, block_id, block_title, new_block_id, position_command) 24 | last_edited_time_hash = Utils::BlockComponents.last_edited_time(block_id) 25 | remove_item_hash = Utils::BlockComponents.block_location_remove( super.parent_id, new_block_id) 26 | 27 | operations = [ 28 | create_block_hash, 29 | block_location_hash, 30 | last_edited_time_hash, 31 | remove_item_hash 32 | ] 33 | 34 | request_url = URLS[:UPDATE_BLOCK] 35 | request_body = Utils::BlockComponents.build_payload(operations, request_ids) 36 | 37 | response = HTTParty.post( 38 | request_url, 39 | body: request_body.to_json, 40 | cookies: cookies, 41 | headers: headers, 42 | ) 43 | 44 | unless response.code == 200; raise "There was an issue completing your request. Here is the response from Notion: #{response.body}, and here is the payload that was sent: #{operations}. 45 | Please try again, and if issues persist open an issue in GitHub."; end 46 | 47 | self.new(new_block_id, block_title, block_id) 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/notion_api/notion_types/numbered_block.rb: -------------------------------------------------------------------------------- 1 | module NotionAPI 2 | # Numbered list Block: best for an ordered list 3 | class NumberedBlock < BlockTemplate 4 | @notion_type = "numbered_list" 5 | @type = "numbered_list" 6 | 7 | def type 8 | NotionAPI::NumberedBlock.notion_type 9 | end 10 | 11 | class << self 12 | attr_reader :notion_type, :type 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/notion_api/notion_types/page_block.rb: -------------------------------------------------------------------------------- 1 | module NotionAPI 2 | 3 | # Page Block, entrypoint for the application 4 | class PageBlock < BlockTemplate 5 | @notion_type = "page" 6 | @type = "page" 7 | 8 | def type 9 | NotionAPI::PageBlock.notion_type 10 | end 11 | 12 | class << self 13 | attr_reader :notion_type, :type 14 | end 15 | 16 | def get_block(url_or_id) 17 | # ! retrieve a Notion Block and return its instantiated class object. 18 | # ! url_or_id -> the block ID or URL : ``str`` 19 | get(url_or_id) 20 | end 21 | 22 | def get_collection(url_or_id) 23 | # ! retrieve a Notion Collection and return its instantiated class object. 24 | # ! url_or_id -> the block ID or URL : ``str`` 25 | clean_id = extract_id(url_or_id) 26 | 27 | request_body = { 28 | pageId: clean_id, 29 | chunkNumber: 0, 30 | limit: 100, 31 | verticalColumns: false, 32 | } 33 | jsonified_record_response = get_all_block_info(request_body) 34 | 35 | block_parent_id = extract_parent_id(clean_id, jsonified_record_response) 36 | block_collection_id = extract_collection_id(clean_id, jsonified_record_response) 37 | block_view_id = extract_view_ids(clean_id, jsonified_record_response).join 38 | block_title = extract_collection_title(clean_id, block_collection_id, jsonified_record_response) 39 | 40 | CollectionView.new(clean_id, block_title, block_parent_id, block_collection_id, block_view_id) 41 | end 42 | 43 | def create_collection(collection_type, collection_title, data) 44 | # ! create a Notion Collection View and return its instantiated class object. 45 | # ! _collection_type -> the type of collection to create : ``str`` 46 | # ! collection_title -> the title of the collection view : ``str`` 47 | # ! data -> JSON data to add to the table : ``str`` 48 | 49 | valid_types = %w[table board list timeline calendar gallery] 50 | unless valid_types.include?(collection_type); raise ArgumentError, "That collection type is not yet supported. Try: #{valid_types.join}."; end 51 | cookies = Core.options["cookies"] 52 | headers = Core.options["headers"] 53 | 54 | new_block_id = extract_id(SecureRandom.hex(16)) 55 | parent_id = extract_id(SecureRandom.hex(16)) 56 | collection_id = extract_id(SecureRandom.hex(16)) 57 | view_id = extract_id(SecureRandom.hex(16)) 58 | 59 | children = [] 60 | alive_blocks = [] 61 | data.each do |_row| 62 | child = extract_id(SecureRandom.hex(16)) 63 | children.push(child) 64 | alive_blocks.push(Utils::CollectionViewComponents.set_collection_blocks_alive(child, collection_id)) 65 | end 66 | 67 | request_id = extract_id(SecureRandom.hex(16)) 68 | transaction_id = extract_id(SecureRandom.hex(16)) 69 | space_id = extract_id(SecureRandom.hex(16)) 70 | 71 | request_ids = { 72 | request_id: request_id, 73 | transaction_id: transaction_id, 74 | space_id: space_id, 75 | } 76 | 77 | create_collection_view = Utils::CollectionViewComponents.create_collection_view(new_block_id, collection_id, view_id) 78 | configure_view = Utils::CollectionViewComponents.set_view_config(collection_type, new_block_id, view_id, children) 79 | 80 | # returns the JSON and some useful column mappings... 81 | column_data = Utils::CollectionViewComponents.set_collection_columns(collection_id, new_block_id, data) 82 | configure_columns_hash = column_data[0] 83 | column_mappings = column_data[1] 84 | set_parent_alive_hash = Utils::BlockComponents.set_parent_to_alive(@id, new_block_id) 85 | add_block_hash = Utils::BlockComponents.block_location_add(@id, @id, new_block_id, nil, "listAfter") 86 | new_block_edited_time = Utils::BlockComponents.last_edited_time(new_block_id) 87 | collection_title_hash = Utils::CollectionViewComponents.set_collection_title(collection_title, collection_id) 88 | 89 | operations = [ 90 | create_collection_view, 91 | configure_view, 92 | configure_columns_hash, 93 | set_parent_alive_hash, 94 | add_block_hash, 95 | new_block_edited_time, 96 | collection_title_hash, 97 | ] 98 | operations << alive_blocks 99 | all_ops = operations.flatten 100 | data.each_with_index do |row, i| 101 | child = children[i] 102 | row.keys.each_with_index do |col_name, j| 103 | child_component = Utils::CollectionViewComponents.insert_data(child, j.zero? ? "title" : col_name, row[col_name], column_mappings[j]) 104 | all_ops.push(child_component) 105 | end 106 | end 107 | 108 | request_url = URLS[:UPDATE_BLOCK] 109 | request_body = build_payload(all_ops, request_ids) 110 | response = HTTParty.post( 111 | request_url, 112 | body: request_body.to_json, 113 | cookies: cookies, 114 | headers: headers, 115 | ) 116 | 117 | unless response.code == 200; raise "There was an issue completing your request. Here is the response from Notion: #{response.body}, and here is the payload that was sent: #{operations}. 118 | Please try again, and if issues persist open an issue in GitHub."; end 119 | 120 | CollectionView.new(new_block_id, collection_title, parent_id, collection_id, view_id) 121 | end 122 | 123 | def create(block_type, block_title, target = nil, position = 'after', options: {}) 124 | page_block = super 125 | 126 | page_block.import_content(options[:content]) if options[:content] 127 | 128 | page_block 129 | end 130 | 131 | def import_content(content) 132 | file_name = "#{@title}.md" 133 | file_urls = build_upload_file_urls(file_name) 134 | signed_put_url = file_urls['signedPutUrl'] 135 | file_url = file_urls['url'] 136 | raise 'Error on getting temporary file url uploaded.' unless signed_put_url && file_url 137 | 138 | upload_content_on_file(signed_put_url, content) 139 | move_content_on_page(file_url, file_name) 140 | end 141 | 142 | def build_upload_file_urls(file_name) 143 | request_body = { 144 | bucket: 'temporary', 145 | name: file_name, 146 | contentType: 'text/markdown' 147 | } 148 | 149 | HTTParty.post( 150 | URLS[:GET_UPLOAD_FILE_URL], 151 | body: request_body.to_json, 152 | cookies: Core.options['cookies'], 153 | headers: Core.options['headers'] 154 | ) 155 | end 156 | 157 | def upload_content_on_file(signed_put_url, content) 158 | HTTParty.put( 159 | signed_put_url, 160 | body: content, 161 | cookies: Core.options['cookies'], 162 | headers: { 'Content-Type' => 'text/markdown' } 163 | ) 164 | end 165 | 166 | def move_content_on_page(file_url, file_name) 167 | request_body = { 168 | task: { 169 | eventName: 'importFile', 170 | request: { 171 | fileURL: file_url, 172 | fileName: file_name, 173 | importType: 'ReplaceBlock', 174 | pageId: @id 175 | } 176 | } 177 | } 178 | 179 | HTTParty.post( 180 | URLS[:ENQUEUE_TASK], 181 | body: request_body.to_json, 182 | cookies: Core.options['cookies'], 183 | headers: Core.options['headers'] 184 | ) 185 | end 186 | end 187 | end 188 | -------------------------------------------------------------------------------- /lib/notion_api/notion_types/quote_block.rb: -------------------------------------------------------------------------------- 1 | module NotionAPI 2 | 3 | # best for memorable information 4 | class QuoteBlock < BlockTemplate 5 | @notion_type = "quote" 6 | @type = "quote" 7 | 8 | def type 9 | NotionAPI::QuoteBlock.notion_type 10 | end 11 | 12 | class << self 13 | attr_reader :notion_type, :type 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/notion_api/notion_types/sub_header_block.rb: -------------------------------------------------------------------------------- 1 | module NotionAPI 2 | 3 | # SubHeader Block: H2 4 | class SubHeaderBlock < BlockTemplate 5 | @notion_type = "sub_header" 6 | @type = "sub_header" 7 | 8 | def type 9 | NotionAPI::SubHeaderBlock.notion_type 10 | end 11 | 12 | class << self 13 | attr_reader :notion_type, :type 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/notion_api/notion_types/sub_sub_header.rb: -------------------------------------------------------------------------------- 1 | module NotionAPI 2 | 3 | # Sub-Sub Header Block: H3 4 | class SubSubHeaderBlock < BlockTemplate 5 | @notion_type = "sub_sub_header" 6 | @type = "sub_sub_header" 7 | 8 | def type 9 | NotionAPI::SubSubHeaderBlock.notion_type 10 | end 11 | 12 | class << self 13 | attr_reader :notion_type, :type 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/notion_api/notion_types/table_of_contents_block.rb: -------------------------------------------------------------------------------- 1 | module NotionAPI 2 | # maps out the headers - sub-headers - sub-sub-headers on the page 3 | class TableOfContentsBlock < BlockTemplate 4 | @notion_type = "table_of_contents" 5 | @type = "table_of_contents" 6 | 7 | def type 8 | NotionAPI::TableOfContentsBlock.notion_type 9 | end 10 | 11 | class << self 12 | attr_reader :notion_type, :type 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/notion_api/notion_types/template.rb: -------------------------------------------------------------------------------- 1 | require_relative "../core" 2 | require "httparty" 3 | 4 | module NotionAPI 5 | # Base Template for all blocks. Inherits core methods from the Block class defined in block.rb 6 | class BlockTemplate < Core 7 | include Utils 8 | 9 | attr_reader :id, :title, :parent_id 10 | 11 | def initialize(id, title, parent_id) 12 | @id = id 13 | @title = title 14 | @parent_id = parent_id 15 | end 16 | 17 | def title=(new_title) 18 | # ! Change the title of a block. 19 | # ! new_title -> new title for the block : ``str`` 20 | request_id = extract_id(SecureRandom.hex(16)) 21 | transaction_id = extract_id(SecureRandom.hex(16)) 22 | space_id = extract_id(SecureRandom.hex(16)) 23 | update_title(new_title.to_s, request_id, transaction_id, space_id) 24 | @title = new_title 25 | end 26 | 27 | def convert(block_class_to_convert_to) 28 | # ! convert a block from its current type to another. 29 | # ! block_class_to_convert_to -> the type of block to convert to : ``cls`` 30 | if type == block_class_to_convert_to.notion_type 31 | # if converting to same type, skip and return self 32 | self 33 | else 34 | # setup cookies, headers, and grab/create static vars for request 35 | cookies = Core.options["cookies"] 36 | headers = Core.options["headers"] 37 | request_url = URLS[:UPDATE_BLOCK] 38 | 39 | # set random IDs for request 40 | request_id = extract_id(SecureRandom.hex(16)) 41 | transaction_id = extract_id(SecureRandom.hex(16)) 42 | space_id = extract_id(SecureRandom.hex(16)) 43 | request_ids = { 44 | request_id: request_id, 45 | transaction_id: transaction_id, 46 | space_id: space_id, 47 | } 48 | 49 | # build hash's that contain the operations to send to Notion 50 | convert_type_hash = Utils::BlockComponents.convert_type(@id, block_class_to_convert_to) 51 | last_edited_time_parent_hash = Utils::BlockComponents.last_edited_time(@parent_id) 52 | last_edited_time_child_hash = Utils::BlockComponents.last_edited_time(@id) 53 | 54 | operations = [ 55 | convert_type_hash, 56 | last_edited_time_parent_hash, 57 | last_edited_time_child_hash, 58 | ] 59 | 60 | request_body = build_payload(operations, request_ids) 61 | response = HTTParty.post( 62 | request_url, 63 | body: request_body.to_json, 64 | cookies: cookies, 65 | headers: headers, 66 | ) 67 | unless response.code == 200; raise "There was an issue completing your request. Here is the response from Notion: #{response.body}, and here is the payload that was sent: #{operations}. 68 | Please try again, and if issues persist open an issue in GitHub."; end 69 | 70 | block_class_to_convert_to.new(@id, @title, @parent_id) 71 | end 72 | end 73 | 74 | def duplicate(target_block = nil) 75 | # ! duplicate the block that this method is invoked upon. 76 | # ! target_block -> the block to place the duplicated block after. Can be any valid Block ID! : ``str`` 77 | cookies = Core.options["cookies"] 78 | headers = Core.options["headers"] 79 | request_url = URLS[:UPDATE_BLOCK] 80 | 81 | new_block_id = extract_id(SecureRandom.hex(16)) 82 | request_id = extract_id(SecureRandom.hex(16)) 83 | transaction_id = extract_id(SecureRandom.hex(16)) 84 | space_id = extract_id(SecureRandom.hex(16)) 85 | 86 | root_children = children_ids(@id) 87 | sub_children = [] 88 | root_children.each { |root_id| sub_children.push(children_ids(root_id)) } 89 | 90 | request_ids = { 91 | request_id: request_id, 92 | transaction_id: transaction_id, 93 | space_id: space_id, 94 | } 95 | body = { 96 | pageId: @id, 97 | chunkNumber: 0, 98 | limit: 100, 99 | verticalColumns: false, 100 | } 101 | 102 | user_notion_id = get_notion_id(body) 103 | 104 | block = target_block ? get(target_block) : self # allows dev to place block anywhere! 105 | 106 | props_and_formatting = get_block_props_and_format(@id, @title) 107 | props = props_and_formatting[:properties] 108 | formats = props_and_formatting[:format] 109 | duplicate_hash = Utils::BlockComponents.duplicate(type, @title, block.id, new_block_id, user_notion_id, root_children, props, formats) 110 | set_parent_alive_hash = Utils::BlockComponents.set_parent_to_alive(block.parent_id, new_block_id) 111 | block_location_hash = Utils::BlockComponents.block_location_add(block.parent_id, block.id, new_block_id, target_block, "listAfter") 112 | last_edited_time_parent_hash = Utils::BlockComponents.last_edited_time(block.parent_id) 113 | last_edited_time_child_hash = Utils::BlockComponents.last_edited_time(block.id) 114 | 115 | operations = [ 116 | duplicate_hash, 117 | set_parent_alive_hash, 118 | block_location_hash, 119 | last_edited_time_parent_hash, 120 | last_edited_time_child_hash, 121 | ] 122 | 123 | request_body = build_payload(operations, request_ids) 124 | response = HTTParty.post( 125 | request_url, 126 | body: request_body.to_json, 127 | cookies: cookies, 128 | headers: headers, 129 | ) 130 | unless response.code == 200; raise "There was an issue completing your request. Here is the response from Notion: #{response.body}, and here is the payload that was sent: #{operations}. 131 | Please try again, and if issues persist open an issue in GitHub."; end 132 | 133 | class_to_return = NotionAPI.const_get(Classes.select { |cls| NotionAPI.const_get(cls).notion_type == type }.join.to_s) 134 | class_to_return.new(new_block_id, @title, block.parent_id) 135 | end 136 | 137 | def move(target_block, position = "after") 138 | # ! move the block to a new location. 139 | # ! target_block -> the targetted block to move to. : ``str`` 140 | # ! position -> where the block should be listed, in positions relative to the target_block [before, after, top-child, last-child] 141 | positions_hash = { 142 | "after" => "listAfter", 143 | "before" => "listBefore", 144 | } 145 | 146 | unless positions_hash.keys.include?(position); raise ArgumentError, "Invalid position. You said: #{position}, valid options are: #{positions_hash.keys.join(", ")}"; end 147 | 148 | position_command = positions_hash[position] 149 | cookies = Core.options["cookies"] 150 | headers = Core.options["headers"] 151 | request_url = URLS[:UPDATE_BLOCK] 152 | 153 | request_id = extract_id(SecureRandom.hex(16)) 154 | transaction_id = extract_id(SecureRandom.hex(16)) 155 | space_id = extract_id(SecureRandom.hex(16)) 156 | 157 | request_ids = { 158 | request_id: request_id, 159 | transaction_id: transaction_id, 160 | space_id: space_id, 161 | } 162 | 163 | is_same_parent = (@parent_id == target_block.parent_id) 164 | set_block_dead_hash = Utils::BlockComponents.set_block_to_dead(@id) # kill the block this method is invoked on... 165 | block_location_remove_hash = Utils::BlockComponents.block_location_remove(@parent_id, @id) # remove the block this method is invoked on... 166 | parent_location_hash = Utils::BlockComponents.parent_location_add(is_same_parent ? @parent_id : target_block.parent_id, @id) # set parent location to alive 167 | block_location_add_hash = Utils::BlockComponents.block_location_add(is_same_parent ? @parent_id : target_block.parent_id, @id, target_block.id, position_command) 168 | last_edited_time_parent_hash = Utils::BlockComponents.last_edited_time(@parent_id) 169 | 170 | if is_same_parent 171 | last_edited_time_child_hash = Utils::BlockComponents.last_edited_time(@id) 172 | operations = [ 173 | set_block_dead_hash, 174 | block_location_remove_hash, 175 | parent_location_hash, 176 | block_location_add_hash, 177 | last_edited_time_parent_hash, 178 | last_edited_time_child_hash, 179 | ] 180 | else 181 | last_edited_time_new_parent_hash = Utils::BlockComponents.last_edited_time(target_block.parent_id) 182 | last_edited_time_child_hash = Utils::BlockComponents.last_edited_time(@id) 183 | @parent_id = target_block.parent_id 184 | operations = [ 185 | set_block_dead_hash, 186 | block_location_remove_hash, 187 | parent_location_hash, 188 | block_location_add_hash, 189 | last_edited_time_parent_hash, 190 | last_edited_time_new_parent_hash, 191 | last_edited_time_child_hash, 192 | ] 193 | end 194 | request_body = build_payload(operations, request_ids) 195 | response = HTTParty.post( 196 | request_url, 197 | body: request_body.to_json, 198 | cookies: cookies, 199 | headers: headers, 200 | ) 201 | unless response.code == 200; raise "There was an issue completing your request. Here is the response from Notion: #{response.body}, and here is the payload that was sent: #{operations}. 202 | Please try again, and if issues persist open an issue in GitHub."; end 203 | 204 | self 205 | end 206 | 207 | def create(block_type, block_title, target = nil, position = "after", options: {}) 208 | # ! create a new block 209 | # ! block_type -> the type of block to create : ``cls`` 210 | # ! block_title -> the title of the new block : ``str`` 211 | # ! target -> the block_id that the new block should be placed after. ``str`` 212 | # ! position -> 'after' or 'before' 213 | 214 | positions_hash = { 215 | "after" => "listAfter", 216 | "before" => "listBefore", 217 | } 218 | unless positions_hash.keys.include?(position); raise "Invalid position. You said: #{position}, valid options are: #{positions_hash.keys.join(", ")}"; end 219 | 220 | position_command = positions_hash[position] 221 | 222 | new_block_id = extract_id(SecureRandom.hex(16)) 223 | request_id = extract_id(SecureRandom.hex(16)) 224 | transaction_id = extract_id(SecureRandom.hex(16)) 225 | space_id = extract_id(SecureRandom.hex(16)) 226 | 227 | request_ids = { 228 | request_id: request_id, 229 | transaction_id: transaction_id, 230 | space_id: space_id, 231 | } 232 | 233 | block_type.create(@id, new_block_id, block_title, target, position_command, request_ids, options) 234 | end 235 | 236 | private 237 | 238 | def self.create(block_id, new_block_id, block_title, target, position_command, request_ids, options) 239 | blocks_with_emojis = [NotionAPI::PageBlock, NotionAPI::CalloutBlock] 240 | cookies = Core.options["cookies"] 241 | headers = Core.options["headers"] 242 | 243 | create_hash = Utils::BlockComponents.create(new_block_id, self.notion_type) 244 | set_parent_alive_hash = Utils::BlockComponents.set_parent_to_alive(block_id, new_block_id) 245 | block_location_hash = Utils::BlockComponents.block_location_add(block_id, block_id, new_block_id, target, position_command) 246 | last_edited_time_parent_hash = Utils::BlockComponents.last_edited_time(block_id) 247 | last_edited_time_child_hash = Utils::BlockComponents.last_edited_time(block_id) 248 | title_hash = Utils::BlockComponents.title(new_block_id, block_title) 249 | 250 | operations = [ 251 | create_hash, 252 | set_parent_alive_hash, 253 | block_location_hash, 254 | last_edited_time_parent_hash, 255 | last_edited_time_child_hash, 256 | title_hash, 257 | ] 258 | 259 | if blocks_with_emojis.include?(self) 260 | emoji_choices = ["😀", "😃", "😄", "😁", "😆", "😅", "🤣", "😂", "🙂", "🙃", "😉", "😊", "😇", "🥰", "😍", "😀", "😃"] 261 | emoji = options[:emoji] || emoji_choices[rand(0...emoji_choices.length)] 262 | emoji_icon_hash = Utils::BlockComponents.add_emoji_icon(new_block_id, emoji) 263 | operations.push(emoji_icon_hash) 264 | end 265 | 266 | request_url = URLS[:UPDATE_BLOCK] 267 | request_body = Utils::BlockComponents.build_payload(operations, request_ids) 268 | response = HTTParty.post( 269 | request_url, 270 | body: request_body.to_json, 271 | cookies: cookies, 272 | headers: headers, 273 | ) 274 | unless response.code == 200; raise "There was an issue completing your request. Here is the response from Notion: #{response.body}, and here is the payload that was sent: #{operations}. 275 | Please try again, and if issues persist open an issue in GitHub."; end 276 | 277 | self.new(new_block_id, block_title, block_id) 278 | end 279 | 280 | def get(url_or_id) 281 | # ! retrieve a Notion Block and return its instantiated class object. 282 | # ! url_or_id -> the block ID or URL : ``str`` 283 | clean_id = extract_id(url_or_id) 284 | 285 | request_body = { 286 | pageId: clean_id, 287 | chunkNumber: 0, 288 | limit: 100, 289 | verticalColumns: false, 290 | } 291 | jsonified_record_response = get_all_block_info(request_body) 292 | 293 | block_type = extract_type(clean_id, jsonified_record_response) 294 | block_parent_id = extract_parent_id(clean_id, jsonified_record_response) 295 | 296 | if block_type.nil? 297 | {} 298 | else 299 | block_class = NotionAPI.const_get(BLOCK_TYPES[block_type].to_s) 300 | if [NotionAPI::CollectionView, NotionAPI::CollectionViewPage].include?(block_class) 301 | block_collection_id = extract_collection_id(clean_id, jsonified_record_response) 302 | block_view_id = extract_view_ids(clean_id, jsonified_record_response) 303 | collection_title = extract_collection_title(clean_id, block_collection_id, jsonified_record_response) 304 | 305 | block = block_class.new(clean_id, collection_title, block_parent_id, block_collection_id, block_view_id.join) 306 | schema = extract_collection_schema(block_collection_id, block_view_id[0]) 307 | column_mappings = schema.keys 308 | column_names = column_mappings.map { |mapping| schema[mapping]["name"] } 309 | block.instance_variable_set(:@column_names, column_names) 310 | CollectionView.class_eval { attr_reader :column_names } 311 | 312 | block 313 | else 314 | block_title = extract_title(clean_id, jsonified_record_response) 315 | block_class.new(clean_id, block_title, block_parent_id) 316 | end 317 | end 318 | end 319 | 320 | def update_title(new_title, request_id, transaction_id, space_id) 321 | # ! Helper method for sending POST request to change title of block. 322 | # ! new_title -> new title for the block : ``str`` 323 | # ! request_id -> the unique ID for the request key. Generated using SecureRandom : ``str`` 324 | # ! transaction_id -> the unique ID for the transaction key. Generated using SecureRandom: ``str`` 325 | # ! transaction_id -> the unique ID for the space key. Generated using SecureRandom: ``str`` 326 | # setup cookies, headers, and grab/create static vars for request 327 | cookies = Core.options["cookies"] 328 | headers = Core.options["headers"] 329 | request_url = URLS[:UPDATE_BLOCK] 330 | 331 | # set unique IDs for request 332 | request_ids = { 333 | request_id: request_id, 334 | transaction_id: transaction_id, 335 | space_id: space_id, 336 | } 337 | 338 | # build and set operations to send to Notion 339 | title_hash = Utils::BlockComponents.title(@id, new_title) 340 | last_edited_time_child_hash = Utils::BlockComponents.last_edited_time(@id) 341 | operations = [ 342 | title_hash, 343 | last_edited_time_child_hash, 344 | ] 345 | 346 | request_body = build_payload(operations, request_ids) # defined in utils.rb 347 | 348 | response = HTTParty.post( 349 | request_url, 350 | body: request_body.to_json, 351 | cookies: cookies, 352 | headers: headers, 353 | ) 354 | response.body 355 | end 356 | end 357 | end 358 | -------------------------------------------------------------------------------- /lib/notion_api/notion_types/text_block.rb: -------------------------------------------------------------------------------- 1 | module NotionAPI 2 | 3 | # good for just about anything (-: 4 | class TextBlock < BlockTemplate 5 | @notion_type = "text" 6 | @type = "text" 7 | 8 | def type 9 | NotionAPI::TextBlock.notion_type 10 | end 11 | 12 | class << self 13 | attr_reader :notion_type, :type 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/notion_api/notion_types/todo_block.rb: -------------------------------------------------------------------------------- 1 | module NotionAPI 2 | 3 | # To-Do block: best for checklists and tracking to-dos. 4 | class TodoBlock < BlockTemplate 5 | @notion_type = "to_do" 6 | @type = "to_do" 7 | 8 | def type 9 | NotionAPI::TodoBlock.notion_type 10 | end 11 | 12 | class << self 13 | attr_reader :notion_type, :type 14 | end 15 | 16 | def checked=(checked_value) 17 | # ! change the checked property of the Todo Block. 18 | # ! checked_value -> boolean value used to determine whether the block should be checked [yes] or not [no] : ``str`` 19 | # set static variables for request 20 | cookies = Core.options["cookies"] 21 | headers = Core.options["headers"] 22 | request_url = URLS[:UPDATE_BLOCK] 23 | 24 | # set unique values for request 25 | request_id = extract_id(SecureRandom.hex(16)) 26 | transaction_id = extract_id(SecureRandom.hex(16)) 27 | space_id = extract_id(SecureRandom.hex(16)) 28 | request_ids = { 29 | request_id: request_id, 30 | transaction_id: transaction_id, 31 | space_id: space_id, 32 | } 33 | 34 | if %w[yes no].include?(checked_value.downcase) 35 | checked_hash = Utils::BlockComponents.checked_todo(@id, checked_value.downcase) 36 | last_edited_time_parent_hash = Utils::BlockComponents.last_edited_time(@parent_id) 37 | last_edited_time_child_hash = Utils::BlockComponents.last_edited_time(@id) 38 | 39 | operations = [ 40 | checked_hash, 41 | last_edited_time_parent_hash, 42 | last_edited_time_child_hash, 43 | ] 44 | request_body = build_payload(operations, request_ids) 45 | response = HTTParty.post( 46 | request_url, 47 | body: request_body.to_json, 48 | cookies: cookies, 49 | headers: headers, 50 | ) 51 | unless response.code == 200; raise "There was an issue completing your request. Here is the response from Notion: #{response.body}, and here is the payload that was sent: #{operations}. 52 | Please try again, and if issues persist open an issue in GitHub."; end 53 | 54 | true 55 | else 56 | false 57 | end 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /lib/notion_api/notion_types/toggle_block.rb: -------------------------------------------------------------------------------- 1 | module NotionAPI 2 | 3 | # Toggle block: best for storing children blocks 4 | class ToggleBlock < BlockTemplate 5 | @notion_type = "toggle" 6 | @type = "toggle" 7 | 8 | def type 9 | NotionAPI::ToggleBlock.notion_type 10 | end 11 | 12 | class << self 13 | attr_reader :notion_type, :type 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/notion_api/utils.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Utils 4 | # ! defines utility functions and static variables for this application. 5 | URLS = { 6 | GET_BLOCK: "https://www.notion.so/api/v3/loadPageChunk", 7 | UPDATE_BLOCK: "https://www.notion.so/api/v3/saveTransactions", 8 | GET_COLLECTION: "https://www.notion.so/api/v3/queryCollection", 9 | GET_UPLOAD_FILE_URL: 'https://www.notion.so/api/v3/getUploadFileUrl', 10 | ENQUEUE_TASK: 'https://www.notion.so/api/v3/enqueueTask' 11 | }.freeze 12 | 13 | class BlockComponents 14 | # ! Each function defined here builds one component that is included in each request sent to Notions backend. 15 | # ! Each request sent will contain multiple components. 16 | 17 | def self.build_payload(operations, request_ids) 18 | # ! properly formats the payload for Notions backend. 19 | # ! operations -> an array of hashes that define the operations to perform : ``Array[Hash]`` 20 | # ! request_ids -> the unique IDs for the request : ``str`` 21 | request_id = request_ids[:request_id] 22 | transaction_id = request_ids[:transaction_id] 23 | space_id = request_ids[:space_id] 24 | payload = { 25 | requestId: request_id, 26 | transactions: [ 27 | { 28 | id: transaction_id, 29 | shardId: 955_090, 30 | spaceId: space_id, 31 | operations: operations, 32 | }, 33 | ], 34 | } 35 | payload 36 | end 37 | def self.create(block_id, block_type) 38 | # ! payload for creating a block. 39 | # ! block_id -> id of the new block : ``str`` 40 | # ! block_type -> type of block to create : ``cls`` 41 | table = "block" 42 | path = [] 43 | command = "update" 44 | timestamp = DateTime.now.strftime("%Q") 45 | { 46 | id: block_id, 47 | table: table, 48 | path: path, 49 | command: command, 50 | args: { 51 | id: block_id, 52 | type: block_type, 53 | properties: {}, 54 | created_time: timestamp, 55 | last_edited_time: timestamp, 56 | }, 57 | } 58 | end 59 | 60 | def self.title(id, title) 61 | # ! payload for updating the title of a block 62 | # ! id -> the ID to update the title of : ``str`` 63 | table = "block" 64 | path = %w[properties title] 65 | command = "set" 66 | 67 | { 68 | id: id, 69 | table: table, 70 | path: path, 71 | command: command, 72 | args: [[title]], 73 | } 74 | end 75 | 76 | def self.last_edited_time(id) 77 | # ! payload for updating the last edited time 78 | # ! id -> either the block ID or parent ID : ``str`` 79 | timestamp = DateTime.now.strftime("%Q") 80 | table = "block" 81 | path = ["last_edited_time"] 82 | command = "set" 83 | 84 | { 85 | table: table, 86 | id: id, 87 | path: path, 88 | command: command, 89 | args: timestamp, 90 | } 91 | end 92 | 93 | def self.convert_type(id, block_class_to_convert_to) 94 | # ! payload for converting a block to a different type. 95 | # ! id -> id of the block to convert : ``str`` 96 | # ! block_class_to_convert_to -> type to convert to block to: ``NotionAPI::`` 97 | table = "block" 98 | path = [] 99 | command = "update" 100 | 101 | { 102 | id: id, 103 | table: table, 104 | path: path, 105 | command: command, 106 | args: { 107 | type: block_class_to_convert_to.notion_type, 108 | }, 109 | } 110 | end 111 | 112 | def self.set_parent_to_alive(block_parent_id, new_block_id) 113 | # ! payload for setting a blocks parent ID to 'alive' 114 | # ! block_parent_id -> the blocks parent ID : ``str`` 115 | # ! new_block_id -> the new block ID, who is a child of the parent : ``str`` 116 | table = "block" 117 | path = [] 118 | command = "update" 119 | parent_table = "block" 120 | alive = true 121 | { 122 | id: new_block_id, 123 | table: table, 124 | path: path, 125 | command: command, 126 | args: { 127 | parent_id: block_parent_id, 128 | parent_table: parent_table, 129 | alive: alive, 130 | }, 131 | } 132 | end 133 | 134 | def self.set_block_to_dead(block_id) 135 | # ! payload for setting a block to dead (alive == true) 136 | # ! block_id -> the block ID to 'kill' : ``str`` 137 | table = "block" 138 | path = [] 139 | command = "update" 140 | alive = false 141 | 142 | { 143 | id: block_id, 144 | table: table, 145 | path: path, 146 | command: command, 147 | args: { 148 | alive: alive, 149 | }, 150 | } 151 | end 152 | 153 | def self.duplicate(block_type, block_title, block_id, new_block_id, user_notion_id, contents, properties, formatting) 154 | # ! payload for duplicating a block. Most properties should be 155 | # ! inherited from the block class the method is invoked on. 156 | # ! block_type -> type of block that is being duplicated : ``cls`` 157 | # ! block_title -> title of block : ``str`` 158 | # ! block_id -> id of block: ``str`` 159 | # ! new_block_id -> id of new block : ``str`` 160 | # ! user_notion_id -> ID of notion user : ``str`` 161 | # ! contents -> The children of the block 162 | timestamp = DateTime.now.strftime("%Q") 163 | table = "block" 164 | path = [] 165 | command = "update" 166 | 167 | { 168 | id: new_block_id, 169 | table: table, 170 | path: path, 171 | command: command, 172 | args: { 173 | id: new_block_id, 174 | version: 10, 175 | type: block_type, 176 | properties: properties, 177 | format: formatting, 178 | content: contents, # root-level blocks 179 | created_time: timestamp, 180 | last_edited_time: timestamp, 181 | created_by_table: "notion_user", 182 | created_by_id: user_notion_id, 183 | last_edited_by_table: "notion_user", 184 | last_edited_by_id: user_notion_id, 185 | copied_from: block_id, 186 | }, 187 | } 188 | end 189 | 190 | def self.parent_location_add(block_parent_id, block_id) 191 | # ! payload for adding a parent 192 | # ! block_parent_id -> the parent id of the block : ``str`` 193 | # ! block_id -> the id of the block : ``str`` 194 | table = "block" 195 | path = [] 196 | command = "update" 197 | parent_table = "block" 198 | alive = true 199 | 200 | { 201 | id: block_id, 202 | table: table, 203 | path: path, 204 | command: command, 205 | args: { 206 | parent_id: block_parent_id, 207 | parent_table: parent_table, 208 | alive: alive, 209 | }, 210 | } 211 | end 212 | 213 | def self.block_location_add(block_parent_id, block_id, new_block_id = nil, target, command) 214 | # ! payload for duplicating a block. Most properties should be 215 | # ! inherited from the block class the method is invoked on. 216 | # ! block_parent_id -> id of parent block : ``str`` 217 | # ! block_id -> id of block: ``str`` 218 | # ! new_block_id -> id of the new block: ``str`` 219 | # ! target -> the ID of the target block : ``str`` 220 | # ! command -> the position of the block, before or after, in relation to the target : ``str`` 221 | table = "block" 222 | path = ["content"] 223 | 224 | args = if command == "listAfter" 225 | { 226 | after: target || block_id, 227 | id: new_block_id || block_id, 228 | } 229 | else 230 | { 231 | before: target || block_id, 232 | id: new_block_id || block_id, 233 | } 234 | end 235 | 236 | { 237 | table: table, 238 | id: block_parent_id, # ID of the parent for the new block. It should be the block that the method is invoked on. 239 | path: path, 240 | command: command, 241 | args: args, 242 | } 243 | end 244 | 245 | def self.row_location_add(last_row_id, block_id, view_id) 246 | # ! add a new row to a Collection View table 247 | # ! last_row_id -> ID of the last row in the CV : ``str`` 248 | # ! block_id -> ID of the blow : ``str`` 249 | # ! view_id -> ID of the View : ``str`` 250 | { 251 | "table": "collection_view", 252 | "id": view_id, 253 | "path": [ 254 | "page_sort", 255 | ], 256 | "command": "listAfter", 257 | "args": { 258 | "after": last_row_id, 259 | "id": block_id, 260 | }, 261 | } 262 | end 263 | 264 | def self.block_location_remove(block_parent_id, block_id) 265 | # ! removes a notion block 266 | # ! block_parent_id -> the parent ID of the block to remove : ``str`` 267 | # ! block_id -> the ID of the block to remove : ``str`` 268 | table = "block" 269 | path = ["content"] 270 | command = "listRemove" 271 | { 272 | table: table, 273 | id: block_parent_id, # ID of the parent for the new block. It should be the block that the method is invoked on. 274 | path: path, 275 | command: command, 276 | args: { 277 | id: block_id, 278 | }, 279 | } 280 | end 281 | 282 | def self.checked_todo(block_id, standardized_check_val) 283 | # ! payload for setting a "checked" value for TodoBlock. 284 | # ! block_id -> the ID of the block to remove : ``str`` 285 | # ! standardized_check_val -> tyes/no value, determines the checked property of the block : ``str`` 286 | table = "block" 287 | path = ["properties"] 288 | command = "update" 289 | { 290 | id: block_id, 291 | table: table, 292 | path: path, 293 | command: command, 294 | args: { 295 | checked: [[standardized_check_val]], 296 | }, 297 | } 298 | end 299 | 300 | def self.update_codeblock_language(block_id, coding_language) 301 | # ! update the language for a codeblock 302 | # ! block_id -> id of the code block 303 | # ! coding_language -> language to change the block to. 304 | table = "block" 305 | path = ["properties"] 306 | command = "update" 307 | 308 | { 309 | id: block_id, 310 | table: table, 311 | path: path, 312 | command: command, 313 | args: { 314 | language: [[coding_language]], 315 | }, 316 | } 317 | end 318 | def self.add_emoji_icon(block_id, icon) 319 | # ! add an emoji icon for either a page or callout block 320 | # ! block_id -> the ID of the block : ``str`` 321 | # ! icon -> the icon for the block. This is currently randomly chosen. : ``str`` 322 | { 323 | id: block_id, 324 | table: "block", 325 | path: ["format", "page_icon"], 326 | command: "set", "args": icon, 327 | } 328 | end 329 | 330 | def self.source(new_block_id, url) 331 | # ! set the source for the ImageBlock 332 | # ! new_block_id -> the ID of the new ImageBlock: ``str`` 333 | # ! url -> the URL for the image 334 | table = "block" 335 | path = ["properties"] 336 | command = "update" 337 | 338 | { 339 | id: new_block_id, 340 | table: table, 341 | path: path, 342 | command: command, 343 | args: { 344 | source: [[ 345 | url, 346 | ]], 347 | }, 348 | } 349 | end 350 | 351 | def self.display_source(new_block_id, block_url) 352 | # ! set the display source for the ImageBlock 353 | # ! new_block_id -> the ID of the new ImageBlock: ``str`` 354 | # ! block_url -> the URL of the ImageBlock: ``str`` 355 | { 356 | id: new_block_id, 357 | table: "block", 358 | path: [ 359 | "format", 360 | ], 361 | command: "update", 362 | args: { 363 | display_source: block_url, 364 | }, 365 | } 366 | end 367 | end 368 | 369 | class CollectionViewComponents 370 | def self.create_collection_view(new_block_id, collection_id, view_ids) 371 | # ! payload for creating a collection view 372 | # ! new_block_id -> id of the new block 373 | # ! collection_id -> ID of the collection. 374 | # ! view_ids -> id of the view 375 | table = "block" 376 | command = "update" 377 | path = [] 378 | type = "collection_view" 379 | properties = {} 380 | timestamp = DateTime.now.strftime("%Q") 381 | 382 | { 383 | id: new_block_id, 384 | table: table, 385 | path: path, 386 | command: command, 387 | args: { 388 | id: new_block_id, 389 | type: type, 390 | collection_id: collection_id, 391 | view_ids: [ 392 | view_ids, 393 | ], 394 | properties: properties, 395 | created_time: timestamp, 396 | last_edited_time: timestamp, 397 | }, 398 | } 399 | end 400 | 401 | def self.set_collection_blocks_alive(new_block_id, collection_id) 402 | # ! payload for setting the collection blocks to alive. 403 | # ! new_block_id -> id of the new block 404 | # ! collection_id -> ID of the collection. 405 | table = "block" 406 | path = [] 407 | command = "update" 408 | parent_table = "collection" 409 | alive = true 410 | type = "page" 411 | properties = {} 412 | timestamp = DateTime.now.strftime("%Q") 413 | 414 | { 415 | id: new_block_id, 416 | table: table, 417 | path: path, 418 | command: command, 419 | args: { 420 | id: new_block_id, 421 | type: type, 422 | parent_id: collection_id, 423 | parent_table: parent_table, 424 | alive: alive, 425 | properties: properties, 426 | created_time: timestamp, 427 | last_edited_time: timestamp, 428 | }, 429 | } 430 | end 431 | 432 | def self.set_view_config(collection_type, new_block_id, view_id, children_ids) 433 | # ! payload for setting the configurations of the view. 434 | # ! new_block_id -> id of the new block 435 | # ! view_id -> id of the view 436 | # ! children_ids -> IDs for the children of the collection. 437 | table = "collection_view" 438 | path = [] 439 | command = "update" 440 | version = 0 441 | name = "Default View" 442 | parent_table = "block" 443 | alive = true 444 | 445 | { 446 | id: view_id, 447 | table: table, 448 | path: path, 449 | command: command, 450 | args: { 451 | id: view_id, 452 | version: version, 453 | type: collection_type, 454 | name: name, 455 | page_sort: children_ids, 456 | parent_id: new_block_id, 457 | parent_table: parent_table, 458 | alive: alive, 459 | }, 460 | } 461 | end 462 | 463 | def self.set_collection_columns(collection_id, new_block_id, data) 464 | # ! payload for setting the columns of the table. 465 | # ! collection_id -> ID of the collection. 466 | # ! new_block_id -> id of the new block 467 | # ! data -> json data to insert into table. 468 | col_names = data[0].keys 469 | data_mappings = { Integer => "number", String => "text", Array => "text", Float => "number", Date => "date" } 470 | exceptions = [ArgumentError, TypeError] 471 | data_types = col_names.map do |name| 472 | # TODO: this is a little hacky... should probably think about a better way or add a requirement for user input to match a certain criteria. 473 | begin 474 | DateTime.parse(data[0][name]) ? data_mappings[Date] : nil 475 | rescue *exceptions 476 | data_mappings[data[0][name].class] 477 | end 478 | end 479 | 480 | schema_conf = {} 481 | col_names.each_with_index do |_name, i| 482 | if i.zero? 483 | schema_conf[:title] = { name: col_names[i], type: "title" } 484 | else 485 | schema_conf[col_names[i]] = { name: col_names[i], type: data_types[i] } 486 | end 487 | end 488 | return { 489 | id: collection_id, 490 | table: "collection", 491 | path: [], 492 | command: "update", 493 | args: { 494 | id: collection_id, 495 | schema: schema_conf, 496 | parent_id: new_block_id, 497 | parent_table: "block", 498 | alive: true, 499 | }, 500 | }, data_types 501 | end 502 | 503 | def self.set_collection_title(collection_title, collection_id) 504 | # ! payload for setting the title of the collection. 505 | # ! collection_title -> title of the collection. 506 | # ! collection_id -> ID of the collection. 507 | table = "collection" 508 | path = ["name"] 509 | command = "set" 510 | 511 | { 512 | id: collection_id, 513 | table: table, 514 | path: path, 515 | command: command, 516 | args: [[collection_title]], 517 | } 518 | end 519 | 520 | def self.insert_data(block_id, column, value, mapping) 521 | # ! payload for inserting data into the table. 522 | # ! block_id -> the ID of the block : ``str`` 523 | # ! column -> the name of the column to insert data into. 524 | # ! value -> the value to insert into the column. 525 | # ! mapping -> the column data type. 526 | simple_mappings = ["title", "text", "phone_number", "email", "url", "number", "checkbox", "select", "multi_select"] 527 | datetime_mappings = ["date"] 528 | media_mappings = ["file"] 529 | person_mappings = ["person"] 530 | page_mappings = ["relation"] 531 | 532 | table = "block" 533 | path = [ 534 | "properties", 535 | column, 536 | ] 537 | command = "set" 538 | 539 | if simple_mappings.include?(mapping) 540 | args = [[value]] 541 | elsif media_mappings.include?(mapping) 542 | args = [[value, [["a", value]]]] 543 | elsif datetime_mappings.include?(mapping) 544 | args = [["‣", [["d", { "type": "date", "start_date": value }]]]] 545 | elsif person_mappings.include?(mapping) 546 | args = [["‣", 547 | [["u", value]]]] 548 | elsif page_mappings.include?(mapping) 549 | args = [["‣", 550 | [["p", value]]]] 551 | else 552 | raise SchemaTypeError, "Invalid property type: #{mapping}" 553 | end 554 | 555 | { 556 | table: table, 557 | id: block_id, 558 | command: command, 559 | path: path, 560 | args: args, 561 | } 562 | end 563 | 564 | def self.add_new_option(column, value, collection_id) 565 | table = "collection" 566 | path = ["schema", column, "options"] 567 | command = "keyedObjectListAfter" 568 | colors = ["default", "gray", "brown", "orange", "yellow", "green", "blue", "purple", "pink", "red"] 569 | random_color = colors[rand(0...colors.length)] 570 | 571 | args = { 572 | "value": { 573 | "id": SecureRandom.hex(16), 574 | "value": value, 575 | "color": random_color, 576 | }, 577 | } 578 | 579 | { 580 | table: table, 581 | id: collection_id, 582 | command: command, 583 | path: path, 584 | args: args, 585 | } 586 | end 587 | 588 | def self.add_new_row(new_block_id) 589 | # ! payload for adding a new row to the table. 590 | # ! new_block_id -> the ID of the new row : ``str`` 591 | table = "block" 592 | path = [] 593 | command = "set" 594 | type = "page" 595 | 596 | { 597 | id: new_block_id, 598 | table: table, 599 | path: path, 600 | command: command, 601 | args: { 602 | type: type, 603 | id: new_block_id, 604 | version: 1, 605 | }, 606 | } 607 | end 608 | 609 | def self.query_collection_body(collection_id, view_id, options: {}) 610 | # ! payload for querying the table for data. 611 | # ! collection_id -> the collection ID : ``str`` 612 | # ! view_id -> the view ID : ``str`` 613 | # ! search_query -> the query for searching the table : ``str`` 614 | query = options[:query] || {} 615 | limit = options[:limit] || 100_000 616 | search_query = options[:search_query] || "" 617 | 618 | loader = { 619 | type: "table", 620 | limit: limit, 621 | searchQuery: search_query, 622 | loadContentCover: true, 623 | } 624 | 625 | { 626 | collectionId: collection_id, 627 | collectionViewId: view_id, 628 | query: query, 629 | loader: loader, 630 | } 631 | end 632 | 633 | def self.add_collection_property(collection_id, args) 634 | # ! payload for adding a column to the table. 635 | # ! collection_id -> the collection ID : ``str`` 636 | # ! args -> the definition of the column : ``str`` 637 | 638 | { 639 | id: collection_id, 640 | table: "collection", 641 | path: [], 642 | command: "update", 643 | args: args, 644 | } 645 | end 646 | 647 | def self.update_property_value(page_id, column_name, new_value, column_type) 648 | # ! update the specified column_name to new_value 649 | # ! page_id -> the ID of the page: ``str`` 650 | # ! column_name -> the name of the column ["property"] to update: ``str`` 651 | # ! new_value -> the new value to assign to that column ["property"]: ``str`` 652 | # ! column_type -> the type of the property: ``str`` 653 | table = "block" 654 | path = [ 655 | "properties", 656 | column_name, 657 | ] 658 | command = "set" 659 | 660 | if column_type == "relation" 661 | args = [["‣", [[ 662 | "p", new_value, 663 | ]]]] 664 | else 665 | args = [[ 666 | new_value, 667 | ]] 668 | end 669 | 670 | { 671 | id: page_id, 672 | table: table, 673 | path: path, 674 | command: command, 675 | args: args, 676 | } 677 | end 678 | end 679 | 680 | class SchemaTypeError < StandardError 681 | def initialize(msg = "Custom exception that is raised when an invalid property type is passed as a mapping.", exception_type = "schema_type") 682 | @exception_type = exception_type 683 | super(msg) 684 | end 685 | end 686 | 687 | def build_payload(operations, request_ids) 688 | # ! properly formats the payload for Notions backend. 689 | # ! operations -> an array of hashes that define the operations to perform : ``Array[Hash]`` 690 | # ! request_ids -> the unique IDs for the request : ``str`` 691 | request_id = request_ids[:request_id] 692 | transaction_id = request_ids[:transaction_id] 693 | space_id = request_ids[:space_id] 694 | payload = { 695 | requestId: request_id, 696 | transactions: [ 697 | { 698 | id: transaction_id, 699 | shardId: 955_090, 700 | spaceId: space_id, 701 | operations: operations, 702 | }, 703 | ], 704 | } 705 | payload 706 | end 707 | end 708 | -------------------------------------------------------------------------------- /lib/notion_api/version.rb: -------------------------------------------------------------------------------- 1 | module NotionAPI 2 | VERSION = '1.1.4' 3 | end -------------------------------------------------------------------------------- /notion.gemspec: -------------------------------------------------------------------------------- 1 | lib = File.expand_path('../lib', __FILE__) 2 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 3 | 4 | require "notion_api/version" 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "notion" 8 | spec.version = NotionAPI::VERSION 9 | spec.authors = ["Dan Murphy"] 10 | spec.email = ["danielmurph8@gmail.com"] 11 | spec.summary = %q[A lightweight gem that allows you to easily read, write, and update Notion data with Ruby] 12 | spec.description = <<~DESC 13 | The Notion API gem allows Ruby developers to programmatically access their Notion pages. 14 | They can add new blocks, move blocks to different locations, duplicate blocks, update 15 | properties of blocks, create and update children blocks, and create and update tables. 16 | DESC 17 | spec.license = "MIT" 18 | spec.homepage = "https://danmurphy1217.github.io/notion-ruby/" 19 | spec.metadata['allowed_push_host'] = 'https://rubygems.org' 20 | 21 | spec.files = Dir[ 22 | "lib/**/*.rb", 23 | ] 24 | 25 | spec.extra_rdoc_files = [ 26 | "LICENSE.md", 27 | "README.md", 28 | ] 29 | 30 | spec.required_ruby_version = ">= 2.5" 31 | 32 | spec.add_runtime_dependency('httparty', '~> 0.17') 33 | spec.add_runtime_dependency('json', '~> 2.2') 34 | 35 | spec.add_development_dependency("bundler") 36 | spec.add_development_dependency("rake", "~> 13.0.0") 37 | spec.add_development_dependency("rspec", "~> 3.9.0") 38 | spec.add_development_dependency("rubocop", "~> 1.4.0") 39 | end -------------------------------------------------------------------------------- /notion_api_block_spec.yaml: -------------------------------------------------------------------------------- 1 | # Notion API Objects 2 | 3 | name: BlockBase 4 | Extends: false 5 | Parameters: 6 | - object: "block" 7 | - id: string 8 | - type: string 9 | - created_time: string 10 | - last_edited_time: string 11 | - has_children: boolean 12 | 13 | name: ParagraphBlock 14 | Extends: true 15 | - name: BlockBase 16 | Parameters: 17 | - type: "paragraph" 18 | - Definition: 19 | - text: RichText[] 20 | - children?: Block[] 21 | 22 | name: HeadingOneBlock 23 | Extends: true 24 | - name: BlockBase 25 | Parameters: 26 | - type: "heading_1" 27 | - Definition: 28 | - text: RichText[] 29 | - has_children: false 30 | 31 | name: HeadingTwoBlock 32 | Extends: true 33 | - name: BlockBase 34 | Parameters: 35 | - type: "heading_2" 36 | - Definition: 37 | - text: RichText[] 38 | - has_children: false 39 | 40 | name: HeadingThreeBlock 41 | Extends: true 42 | - name: BlockBase 43 | Parameters: 44 | - type: "heading_3" 45 | - Definition: 46 | - text: RichText[] 47 | - has_children: false 48 | 49 | name: BulletedListItemBlock 50 | Extends: true 51 | - name: BaseBlock 52 | Parameters: 53 | - type: "bulleted_list_item" 54 | - Definition: 55 | - text: RichText[] 56 | - children?: Block[] 57 | 58 | name: NumberedListItemBlock 59 | Extends: true 60 | - name: BaseBlock 61 | Parameters: 62 | - type: "numbered_list_item" 63 | - Definition: 64 | - text: RichText[] 65 | - children?: Block[] 66 | 67 | name: ToDoBlock 68 | Extends: true 69 | - name: BaseBlock 70 | Parameters: 71 | - type: "to_do" 72 | - Definition: 73 | - text: RichText[] 74 | - checked: boolean 75 | - children?: Block[] 76 | 77 | name: ToggleBlock 78 | Extends: true 79 | - name: BaseBlock 80 | Parameters: 81 | - type: "toggle" 82 | - Definition: 83 | - text: RichText[] 84 | - children?: Block[] 85 | 86 | name: ChildPageBlock 87 | Extends: true 88 | - name: BaseBlock 89 | Parameters: 90 | - type: "child_page" 91 | - Definition: 92 | - title: "string" 93 | 94 | name: EmbedBlock 95 | Extends: true 96 | - name: BaseBlock 97 | Parameters: 98 | - type: "embed" 99 | - Definition: 100 | - url: "string" 101 | - caption? : RichText[] 102 | 103 | name: ExternalFileWithCaption 104 | Extends: true 105 | - name: ExternalFile 106 | Parameters: 107 | - caption?: RichText[] 108 | 109 | name: FileWithCaption 110 | Extends: true 111 | - name: File 112 | Parameters: 113 | - caption?: RichText[] 114 | 115 | name: ImageBlock 116 | Extends: true 117 | - name: BaseBlock 118 | Parameters: 119 | - type: "image" 120 | - image: ExternalFileWithCaption | FileWithCaption 121 | 122 | name: VideoBlock 123 | Extends: true 124 | - name: BaseBlock 125 | Parameters: 126 | - type: "video" 127 | - image: ExternalFileWithCaption | FileWithCaption 128 | 129 | name: FileBlock 130 | Extends: true 131 | - name: BaseBlock 132 | Parameters: 133 | - type: "file" 134 | - image: ExternalFileWithCaption | FileWithCaption 135 | 136 | name: PDFBlock 137 | Extends: true 138 | - name: BaseBlock 139 | Parameters: 140 | - type: "pdf" 141 | - image: ExternalFileWithCaption | FileWithCaption 142 | 143 | name: AudioBlock 144 | Extends: true 145 | - name: BaseBlock 146 | Parameters: 147 | - type: "audio" 148 | - image: ExternalFileWithCaption | FileWithCaption 149 | 150 | name: UnsupportedBlock 151 | Extends: true 152 | - name: BaseBlock 153 | Parameters: 154 | - type: "unsupported" 155 | -------------------------------------------------------------------------------- /spec/blocks_spec.rb: -------------------------------------------------------------------------------- 1 | require "notion_api" 2 | 3 | describe NotionAPI::BlockTemplate do 4 | context "testing the NotionAPI::BlockTemplate public class methods" do 5 | describe "#title=" do 6 | it "should update the title of the block the method is invoked on." do 7 | @block = $Block_spec_page.get_block($Block_spec_title_id) 8 | old_title = @block.title 9 | new_title = SecureRandom.hex(n = 16) 10 | expect(old_title).not_to eq(new_title) 11 | expect(@block.title).to eq(old_title) 12 | @block.title = new_title # update title 13 | expect(@block.title).to eq(new_title) 14 | expect(@block.title).not_to eq(old_title) 15 | end 16 | end 17 | describe "#convert" do 18 | it "should convert the block to a different type and return the new block." do 19 | @block = $Block_spec_page.get_block($Block_spec_convert_id) 20 | filtered_classes = Classes.select { |cls| ![:CollectionView, :CollectionViewRow, :CollectionViewPage, :ImageBlock, :LinkBlock].include?(cls)} 21 | 22 | number_of_classes = filtered_classes.length 23 | class_for_conversion = filtered_classes[rand(0...number_of_classes)] 24 | notion_class_object = NotionAPI.const_get(class_for_conversion.to_s) 25 | @converted_block = @block.convert(notion_class_object) 26 | 27 | expect(@converted_block.type).to eq(notion_class_object.notion_type) 28 | expect(@converted_block.title).to eq(@block.title) 29 | expect(@converted_block.id).to eq(@block.id) 30 | expect(@converted_block.parent_id).to eq(@block.parent_id) 31 | end 32 | end 33 | describe "#duplicate_no_target" do 34 | it "should duplicate the block the method is invoked on." do 35 | @block = $Block_spec_page.get_block($Block_spec_duplicate_id_one) 36 | @duplicated_block = @block.duplicate 37 | 38 | expect(@duplicated_block.type).to eq(@block.type) 39 | expect(@duplicated_block.title).to eq(@block.title) 40 | expect(@duplicated_block.id).not_to eq(@block.id) 41 | expect(@duplicated_block.parent_id).to eq(@block.parent_id) 42 | end 43 | end 44 | describe "#duplicate_with_target" do 45 | it "should duplicate the block the method is invoked on in the specified location." do 46 | @block = $Block_spec_page.get_block($Block_spec_duplicate_id_one) 47 | @target = $Block_spec_sub_page.get_block($Block_spec_duplicate_id_target) 48 | @duplicated_block = @block.duplicate(@target.id) 49 | 50 | expect(@duplicated_block.type).to eq(@block.type) 51 | expect(@duplicated_block.title).to eq(@block.title) 52 | expect(@duplicated_block.id).not_to eq(@block.id) 53 | expect(@duplicated_block.parent_id).not_to eq(@block.parent_id) # ! should not have same parent 54 | end 55 | end 56 | describe "#move" do 57 | it "should move the block the method is invoked on to a new location after or before the target_block." do 58 | @new_block = $Block_spec_move_page.create(NotionAPI::TextBlock, 'I\'ll be moved...') 59 | initial_parent_id = @new_block.parent_id 60 | initial_title = @new_block.title 61 | initial_id = @new_block.id 62 | initial_type = @new_block.type 63 | @target_block = $Block_spec_move_page.get_block($Block_spec_move_id_target) # moves block to new page 64 | 65 | @new_block.move(@target_block) # move to new page... 66 | expect(@new_block.parent_id).not_to eq(initial_parent_id) 67 | expect(@new_block.title).to eq(initial_title) 68 | expect(@new_block.id).to eq(initial_id) 69 | expect(@new_block.type).to eq(initial_type) 70 | end 71 | end 72 | describe "#get_collection" do 73 | it "should return a collection block." do 74 | @collection = $Block_spec_get_page.get_collection($Block_spec_get_collection_id) 75 | expect(@collection.title).to eq("Copy of Test Car Data") 76 | 77 | i = 0 78 | $Vehicle_data_csv.split("\n").each {|r| i += 1} 79 | 80 | @rows = @collection.rows 81 | 82 | expect(@collection.row_ids.length).to eq(45).and(eq(i - 1)) 83 | expect(@rows.length).to eq(45).and(eq(i - 1)) 84 | 85 | # ! row parent should == collection parent should == page ID 86 | expect(@rows[0].parent_id).to eq(@collection.parent_id) 87 | expect(@rows[0].parent_id).to eq($Block_spec_get_page.id) 88 | end 89 | end 90 | describe "#create_collection" do 91 | it "should create a collection view table and return it." do 92 | title = "Emoji Data: #{DateTime.now.strftime('%Q')}" 93 | @collection = $Block_spec_create_page.create_collection("table", title, $Json) 94 | expect(@collection.title).to eq(title) 95 | expect(@collection.row_ids.length).to eq($Json.length) 96 | @rows = @collection.rows 97 | # ! ensure correct order of mapping... 98 | @rows.each_with_index do |row, i| 99 | expect(@collection.row(@rows[i].id).emoji).to eq($Json[i]["emoji"]) 100 | end 101 | end 102 | end 103 | describe "#add_row" do 104 | before(:context) { 105 | @collection = $Block_spec_add_page.get_collection($Block_spec_add_row_id) 106 | @col_one = "#{SecureRandom.hex(16)}" 107 | @col_two = "https://www.electronjs.org/" 108 | @col_three = "#{DateTime.now.strftime("%Q")}" 109 | @col_six = "danielmurph8@gmail.com" 110 | @col_seven = "3028934649" 111 | 112 | @json_data = { "Col One" => @col_one, "Col Two" => @col_two, "Col Three" => @col_three, "Col Four" => @col_three, "Col Five" => @col_two, "Col Six" => @col_six, "Col Seven" => @col_seven } 113 | @new_row = @collection.add_row(@json_data) 114 | @new_row_info = @collection.row(@new_row.id) 115 | } 116 | subject { @new_row_info } 117 | 118 | it "should have a col_one method that equals json_data['col one']." do 119 | expect(@new_row_info.col_one).to eq(@json_data["Col One"]) 120 | end 121 | it "should have a col_one method that equals json_data['col two']." do 122 | expect(@new_row_info.col_two).to eq(@json_data["Col Two"]) 123 | end 124 | it "should have a col_one method that equals json_data['col three']." do 125 | expect(@new_row_info.col_three).to eq(@json_data["Col Three"]) 126 | end 127 | it "should have a col_one method that equals json_data['col four']." do 128 | expect(@new_row_info.col_four).to eq(@json_data["Col Four"]) 129 | end 130 | it "should have a col_one method that equals json_data['col five']." do 131 | expect(@new_row_info.col_five).to eq(@json_data["Col Five"]) 132 | end 133 | it "should have a col_one method that equals json_data['col six']." do 134 | expect(@new_row_info.col_six).to eq(@json_data["Col Six"]) 135 | end 136 | end 137 | context "testing the Notion::BlockTemplate private class methods" do 138 | describe "#get" do 139 | it "should return a block instance." do 140 | $Blocks = $Block_spec_get_page.send('get', $Block_spec_get_id) 141 | expect($Blocks).to be_an_instance_of(NotionAPI::TodoBlock) 142 | expect($Blocks.id).to eq($Block_spec_get_id) 143 | expect($Blocks.parent_id).to eq($Block_spec_get_page.id) 144 | expect($Blocks.title).to eq("TODO Block") 145 | end 146 | end 147 | end 148 | end 149 | context "cleaning the property names of a Notion Collection View." do 150 | it "FULL_PAGE: should parse the column name, replace non-alphanumeric characters, and then return the cleaned column name" do 151 | test_prop_hash = {"1234"=> "Col Name (one) (two) 1234?!>"} 152 | expect($Full_page_cv.clean_property_names(test_prop_hash, "1234")).to eq(:col_name_one_two_1234) 153 | end 154 | it "INLINE: should parse the column name, replace non-alphanumeric characters, and thenr eturn the cleaned column name" do 155 | inline_cv = $Block_spec_page.get_collection("3224c94f-e660-4092-9dba-d26b69b68d40") 156 | test_prop_hash = {"1234"=> "Col Name (#$^&^^*one) (@!#$!two) 1!@#2$%@3!@#@?4!>"} 157 | expect(inline_cv.clean_property_names(test_prop_hash, "1234")).to eq(:col_name_one_two_1234) 158 | end 159 | end 160 | end 161 | 162 | -------------------------------------------------------------------------------- /spec/client_spec.rb: -------------------------------------------------------------------------------- 1 | require "notion_api" 2 | 3 | describe NotionAPI::Client do 4 | context "testing the NotionAPI::Client class" do 5 | describe "#new" do 6 | it "should create a new Client instance with token_v2 and active_user_header attributes." do 7 | @client = NotionAPI::Client.new(ENV["token_v2"]) 8 | expect(@client).to be_an_instance_of(NotionAPI::Client) 9 | expect(@client.token_v2).to eq(ENV["token_v2"]) 10 | expect(@client.active_user_header).to be_nil 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/core_spec.rb: -------------------------------------------------------------------------------- 1 | require "notion_api" 2 | 3 | describe NotionAPI::Core do 4 | context "testing the NotionAPI::Core public class methods" do 5 | describe "#get_page_errant" do 6 | it "should error due to incorrect URL/ID input." do 7 | expect { $Client.get_page($Core_spec_invalid_url_one) }.to raise_error(ArgumentError) 8 | expect { $Client.get_page($Core_spec_invalid_url_two) }.to raise_error(ArgumentError) 9 | end 10 | end 11 | 12 | describe "#get_page" do 13 | it "should return a PageBlock." do 14 | expect { $Client.get_page($Core_spec_page_id_no_dashes) }.not_to raise_error 15 | expect($Core_spec_page.title).to eq("CORE.RB TESTS") 16 | expect($Core_spec_page.type).to eq("page") 17 | expect($Core_spec_page.id).to eq($Core_spec_page_id) 18 | expect($Core_spec_page.parent_id.gsub("-", "")).to eq($Core_spec_page_parent_no_dashes) 19 | expect($Core_spec_page.id.gsub("-", "")).to eq($Core_spec_page_id_no_dashes) 20 | end 21 | end 22 | 23 | describe "#children" do 24 | it "should return an array of block classes relating to the children of the block this method is invoked on." do 25 | @children = $Core_spec_page.children 26 | expect(@children[0].title).to eq("first block for testing purposes...") 27 | expect(@children[0].type).to eq("bulleted_list") 28 | expect(@children[0].id).to eq($Test_core_block_id_one) 29 | expect(@children[0].parent_id).to eq($Core_spec_page.id) 30 | end 31 | end 32 | 33 | describe "#children_ids" do 34 | it "should return an array of children IDs relating to the children on the page." do 35 | @children = $Core_spec_page.children 36 | @children_ids = $Core_spec_page.children_ids 37 | expect(@children[0].type).to eq($Core_spec_page.get_block(@children_ids[0]).type) 38 | expect(@children[0].title).to eq($Core_spec_page.get_block(@children_ids[0]).title) 39 | 40 | expect(@children[-1].type).to eq($Core_spec_page.get_block(@children_ids[-1]).type) 41 | expect(@children[-1].title).to eq($Core_spec_page.get_block(@children_ids[-1]).title) 42 | expect(@children_ids.length).to eq(@children.length) 43 | end 44 | end 45 | end 46 | 47 | context "testing the NotionAPI::Core private methods" do 48 | describe "#get_notion_id" do 49 | it "should return the User Notion ID sent from Notion in the response headers." do 50 | expect($Client.send("get_notion_id", $Body)).to eq(ENV["user_notion_id"]) 51 | end 52 | end 53 | 54 | describe "#get_last_page_block_id" do 55 | it "should return the User Notion ID sent from Notion in the response headers." do 56 | @children_ids = $Core_spec_page.children_ids 57 | expect($Client.send("get_last_page_block_id", $Core_spec_page_id_no_dashes)).to eq(@children_ids[-1]) 58 | end 59 | end 60 | 61 | describe "#get_all_block_info" do 62 | it "should return all record information pertaining to a Notion Block." do 63 | expect($Jsonified_core_page.keys).to eq(["block", "space"]).or(eq(["block", "space", "collection_view", "collection"])) # if collection view block... 64 | end 65 | end 66 | 67 | describe "#extract_title" do 68 | it "should extract the title of a block and return it" do 69 | expect($Client.send("extract_title", $Core_spec_page_id, $Jsonified_core_page)).to eq("CORE.RB TESTS") 70 | expect($Client.send("extract_title", $Test_core_block_id_one, $Jsonified_core_page)).to eq("first block for testing purposes...") 71 | expect($Client.send("extract_title", $Test_core_block_id_two, $Jsonified_core_page)).to eq("second block for testing purposes...") 72 | end 73 | end 74 | describe "#extract_collection_title" do 75 | it "should extract the title of a collection and return it" do 76 | @collection_one_id = $Core_spec_page.get_collection($Test_collection_block_id_one).collection_id 77 | @collection_two_id = $Core_spec_page.get_collection($Test_collection_block_id_two).collection_id 78 | expect($Client.send("extract_collection_title", $Test_collection_block_id_one, @collection_one_id, $Jsonified_core_page)).to eq("Test Emoji Data") 79 | expect($Client.send("extract_collection_title", $Test_collection_block_id_two, @collection_two_id, $Jsonified_core_page)).to eq("Test Car Data") 80 | end 81 | end 82 | describe "#extract_type" do 83 | it "should extract the type of a block and return it" do 84 | expect($Client.send("extract_type", $Core_spec_page_id, $Jsonified_core_page)).to eq("page") 85 | expect($Client.send("extract_type", $Test_core_block_id_one, $Jsonified_core_page)).to eq("bulleted_list") 86 | expect($Client.send("extract_type", $Test_core_block_id_two, $Jsonified_core_page)).to eq("text") 87 | end 88 | end 89 | describe "#extract_parent_id" do 90 | it "should return the parent id of the object the method is invoked on." do 91 | expect($Client.send("extract_parent_id", $Core_spec_page_id, $Jsonified_core_page).gsub("-", "")).to eq($Core_spec_page_parent_no_dashes) 92 | expect($Client.send("extract_parent_id", $Test_core_block_id_one, $Jsonified_response_block_one)).to eq($Core_spec_page_id) 93 | expect($Client.send("extract_parent_id", $Test_core_block_id_two, $Jsonified_response_block_two)).to eq($Core_spec_page_id) 94 | end 95 | end 96 | describe "#extract_collection_id" do 97 | it "should return the collection id of the Collection View object the method is invoked on." do 98 | expect($Client.send("extract_collection_id", $Test_collection_block_id_one, $Jsonified_core_page)).to eq($Test_collection_id_one) 99 | expect($Client.send("extract_collection_id", $Test_collection_block_id_two, $Jsonified_core_page)).to eq($Test_collection_id_two) 100 | end 101 | end 102 | describe "#extract_view_ids" do 103 | it "should return the view ids of the Collection View object the method is invoked on." do 104 | expect($Client.send("extract_view_ids", $Test_collection_block_id_one, $Jsonified_core_page)).to be_an_instance_of(Array) 105 | expect($Client.send("extract_view_ids", $Test_collection_block_id_two, $Jsonified_core_page)).to be_an_instance_of(Array) 106 | end 107 | end 108 | describe "#extract_id" do 109 | it "should return the cleaned ID of the URL or ID passed." do 110 | expect($Client.send("extract_id", $Core_spec_page_id)).to eq($Core_spec_page_id) 111 | expect($Client.send("extract_id", $Core_spec_page_id).gsub("-", "")).to eq($Core_spec_page_id_no_dashes) 112 | expect($Client.send("extract_id", $Core_spec_url)).to eq($Core_spec_page_id) 113 | end 114 | end 115 | end 116 | end 117 | -------------------------------------------------------------------------------- /spec/fixtures/emoji_data.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "emoji": "😀", 4 | "description": "grinning face", 5 | "category": "Smileys & Emotion", 6 | "aliases": ["grinning"], 7 | "tags": ["smile", "happy"], 8 | "unicode_version": "6.1", 9 | "ios_version": "6.0" 10 | }, 11 | { 12 | "emoji": "😃", 13 | "description": "grinning face with big eyes", 14 | "category": "Smileys & Emotion", 15 | "aliases": ["smiley"], 16 | "tags": ["happy", "joy", "haha"], 17 | "unicode_version": "6.0", 18 | "ios_version": "6.0" 19 | }, 20 | { 21 | "emoji": "😄", 22 | "description": "grinning face with smiling eyes", 23 | "category": "Smileys & Emotion", 24 | "aliases": ["smile"], 25 | "tags": ["happy", "joy", "laugh", "pleased"], 26 | "unicode_version": "6.0", 27 | "ios_version": "6.0" 28 | }, 29 | { 30 | "emoji": "😁", 31 | "description": "beaming face with smiling eyes", 32 | "category": "Smileys & Emotion", 33 | "aliases": ["grin"], 34 | "tags": [], 35 | "unicode_version": "6.0", 36 | "ios_version": "6.0" 37 | }, 38 | { 39 | "emoji": "😆", 40 | "description": "grinning squinting face", 41 | "category": "Smileys & Emotion", 42 | "aliases": ["laughing", "satisfied"], 43 | "tags": ["happy", "haha"], 44 | "unicode_version": "6.0", 45 | "ios_version": "6.0" 46 | }, 47 | { 48 | "emoji": "😅", 49 | "description": "grinning face with sweat", 50 | "category": "Smileys & Emotion", 51 | "aliases": ["sweat_smile"], 52 | "tags": ["hot"], 53 | "unicode_version": "6.0", 54 | "ios_version": "6.0" 55 | }, 56 | { 57 | "emoji": "🤣", 58 | "description": "rolling on the floor laughing", 59 | "category": "Smileys & Emotion", 60 | "aliases": ["rofl"], 61 | "tags": ["lol", "laughing"], 62 | "unicode_version": "9.0", 63 | "ios_version": "10.2" 64 | }, 65 | { 66 | "emoji": "😂", 67 | "description": "face with tears of joy", 68 | "category": "Smileys & Emotion", 69 | "aliases": ["joy"], 70 | "tags": ["tears"], 71 | "unicode_version": "6.0", 72 | "ios_version": "6.0" 73 | }, 74 | { 75 | "emoji": "🙂", 76 | "description": "slightly smiling face", 77 | "category": "Smileys & Emotion", 78 | "aliases": ["slightly_smiling_face"], 79 | "tags": [], 80 | "unicode_version": "7.0", 81 | "ios_version": "9.1" 82 | }, 83 | { 84 | "emoji": "🙃", 85 | "description": "upside-down face", 86 | "category": "Smileys & Emotion", 87 | "aliases": ["upside_down_face"], 88 | "tags": [], 89 | "unicode_version": "8.0", 90 | "ios_version": "9.1" 91 | }, 92 | { 93 | "emoji": "😉", 94 | "description": "winking face", 95 | "category": "Smileys & Emotion", 96 | "aliases": ["wink"], 97 | "tags": ["flirt"], 98 | "unicode_version": "6.0", 99 | "ios_version": "6.0" 100 | }, 101 | { 102 | "emoji": "😊", 103 | "description": "smiling face with smiling eyes", 104 | "category": "Smileys & Emotion", 105 | "aliases": ["blush"], 106 | "tags": ["proud"], 107 | "unicode_version": "6.0", 108 | "ios_version": "6.0" 109 | }, 110 | { 111 | "emoji": "😇", 112 | "description": "smiling face with halo", 113 | "category": "Smileys & Emotion", 114 | "aliases": ["innocent"], 115 | "tags": ["angel"], 116 | "unicode_version": "6.0", 117 | "ios_version": "6.0" 118 | }, 119 | { 120 | "emoji": "🥰", 121 | "description": "smiling face with hearts", 122 | "category": "Smileys & Emotion", 123 | "aliases": ["smiling_face_with_three_hearts"], 124 | "tags": ["love"], 125 | "unicode_version": "11.0", 126 | "ios_version": "12.1" 127 | }, 128 | { 129 | "emoji": "😍", 130 | "description": "smiling face with heart-eyes", 131 | "category": "Smileys & Emotion", 132 | "aliases": ["heart_eyes"], 133 | "tags": ["love", "crush"], 134 | "unicode_version": "6.0", 135 | "ios_version": "6.0" 136 | }, 137 | { 138 | "emoji": "😀", 139 | "description": "grinning face", 140 | "category": "Smileys & Emotion", 141 | "aliases": ["grinning"], 142 | "tags": ["smile", "happy"], 143 | "unicode_version": "6.1", 144 | "ios_version": "6.0" 145 | }, 146 | { 147 | "emoji": "😃", 148 | "description": "grinning face with big eyes", 149 | "category": "Smileys & Emotion", 150 | "aliases": ["smiley"], 151 | "tags": ["happy", "joy", "haha"], 152 | "unicode_version": "6.0", 153 | "ios_version": "6.0" 154 | } 155 | ] 156 | -------------------------------------------------------------------------------- /spec/fixtures/notion_block_one_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "block": { 3 | "1f5ae85f-f89f-4779-9fa4-b30c3b229cdb": { 4 | "role": "editor", 5 | "value": { 6 | "id": "1f5ae85f-f89f-4779-9fa4-b30c3b229cdb", 7 | "version": 56, 8 | "type": "bulleted_list", 9 | "properties": { 10 | "title": [["first block for testing purposes..."]] 11 | }, 12 | "created_time": 1606008667652, 13 | "last_edited_time": 1606165980000, 14 | "parent_id": "9c50a7b3-9ad7-4f2b-aa08-b3c95f1f19e7", 15 | "parent_table": "block", 16 | "alive": true, 17 | "copied_from": "6982f801-5da8-e44c-362a-b9cc634e78bb", 18 | "created_by_table": "notion_user", 19 | "created_by_id": "0c5f02f3-495d-4b73-b1c5-9f6fe03a8c26", 20 | "last_edited_by_table": "notion_user", 21 | "last_edited_by_id": "0c5f02f3-495d-4b73-b1c5-9f6fe03a8c26", 22 | "shard_id": 955090, 23 | "space_id": "f687f7de-7f4c-4a86-b109-941a8dae92d2" 24 | } 25 | }, 26 | "9c50a7b3-9ad7-4f2b-aa08-b3c95f1f19e7": { 27 | "role": "editor", 28 | "value": { 29 | "id": "9c50a7b3-9ad7-4f2b-aa08-b3c95f1f19e7", 30 | "version": 34, 31 | "type": "page", 32 | "properties": { 33 | "title": [["CORE.RB TESTS"]] 34 | }, 35 | "content": [ 36 | "1f5ae85f-f89f-4779-9fa4-b30c3b229cdb", 37 | "d7407d2c-adbb-497c-b99f-9f9d426d2b18", 38 | "34d03794-ecdd-e42d-bb76-7e4aa77b6503", 39 | "f1664a99-165b-49cc-811c-84f37655908a" 40 | ], 41 | "format": { 42 | "page_icon": "🍾" 43 | }, 44 | "created_time": 1606148662319, 45 | "last_edited_time": 1606165980000, 46 | "parent_id": "66447bc8-17f0-44bc-81ed-3cf4802e9b00", 47 | "parent_table": "block", 48 | "alive": true, 49 | "created_by_table": "notion_user", 50 | "created_by_id": "0c5f02f3-495d-4b73-b1c5-9f6fe03a8c26", 51 | "last_edited_by_table": "notion_user", 52 | "last_edited_by_id": "0c5f02f3-495d-4b73-b1c5-9f6fe03a8c26", 53 | "shard_id": 955090, 54 | "space_id": "f687f7de-7f4c-4a86-b109-941a8dae92d2" 55 | } 56 | }, 57 | "66447bc8-17f0-44bc-81ed-3cf4802e9b00": { 58 | "role": "editor", 59 | "value": { 60 | "id": "66447bc8-17f0-44bc-81ed-3cf4802e9b00", 61 | "version": 540, 62 | "type": "page", 63 | "properties": { 64 | "title": [["Notion API Testing"]] 65 | }, 66 | "content": [ 67 | "ee0a6531-44cd-439f-a68c-1bdccbebfc8a", 68 | "f13da22b-9012-4c49-ac41-6b7f97bd519e", 69 | "9c50a7b3-9ad7-4f2b-aa08-b3c95f1f19e7", 70 | "b94758d4-e7df-4af8-863f-b90c11b35fff", 71 | "7c1d1c19-77ee-45cd-bc3d-82fad4bd1a78", 72 | "2cbbe0bf-34cd-409b-9162-64284b33e526", 73 | "9edc0f82-1ec2-4dff-9167-6b3e2dd0f730", 74 | "ba87b719-9207-49a7-b646-de042b266ba8", 75 | "1348703c-ad64-e138-20a7-58b13018af86", 76 | "f5ed36eb-e5c3-4c52-bda8-60be03315332", 77 | "88d108d2-e54b-1f04-4ea0-c4e2ed6c4a12", 78 | "e6e66202-8211-40a8-bffc-8215b8d07dad", 79 | "f9b96cda-6c2f-431d-906d-8c0cb892afda", 80 | "bf9892b8-8c39-47a5-9d03-2dfab5e69179", 81 | "a1702f9b-b092-4703-8f08-90339c6d9176", 82 | "0f9b43d6-448b-2a6d-e027-1a419a98cad2", 83 | "268f3c0d-c201-e5c4-3c79-426791848150", 84 | "dde32522-a7a4-ba1d-844a-d9dbfa5c9994", 85 | "54b58ffe-8258-898e-99fb-6e7e260840a3", 86 | "8ab39c03-b097-4cbf-8dc3-c37b667b371c", 87 | "1353c9ab-2509-a0ff-cbce-2cba1dd2a2e0", 88 | "b2230a72-9472-8b30-1682-cb92b53e71d8", 89 | "592743ff-9904-c9f4-d490-1d558ae918e5", 90 | "20bfcad0-8fe4-452d-a4f5-ff69ac75ad0f", 91 | "58614350-f17e-47b6-81d2-da757754eef3", 92 | "b0ce97b7-ad38-b9b9-3806-62ec8d098a0c", 93 | "90075461-b7ce-6aa4-314e-556875c69dae", 94 | "db49c942-a957-2498-e3dc-e01e7636d950", 95 | "b4e8d4ad-0ac6-f13e-e6dc-6690c7c29fea", 96 | "057f090b-8e46-4215-8fef-c1224efeddd9", 97 | "4a71d78a-37bf-ab11-9f37-b4f9a7642036", 98 | "5fcfa44c-7f60-b5bf-e2ab-c2e6304a4581", 99 | "24ee3b27-d7b4-4cfb-b837-912deccca5a4", 100 | "5e022e72-994f-40f3-8d1d-cda7f1b8b107", 101 | "9e0578f3-d4d7-4ed9-bda3-f050a6eb6dca", 102 | "5e8c4c40-a444-210d-8475-22386864089c", 103 | "3cdb51fd-25fe-29d6-edc1-7503c9b1e768", 104 | "f76fa467-cdb6-40e4-87ae-8dac19a75efc", 105 | "79bc0290-18ec-420b-ae1b-57a8f3744fcb", 106 | "9a07d006-9295-4a2c-be35-9b6bf78ee7fe", 107 | "c9db948d-91c5-0048-3a0a-7674f4e874a0", 108 | "e637befa-128f-6950-c9c0-8e71fc159c5d", 109 | "ef74f834-7185-e9e8-5bdd-5f8bece38623", 110 | "1dca3f8a-8d85-3a35-5d4c-34cfd7a066e9", 111 | "b2852fb7-a8f5-2e69-6769-c9d2a5878cf4", 112 | "054ab04e-694f-462d-30d5-1068038eb379", 113 | "1cff7eb6-ea6e-4293-7cd3-25cf79f6c7f0", 114 | "2c9b2885-ad4d-4f1f-a077-d3048c79232c", 115 | "f86323b9-8b79-74a7-f268-b1a25ff4a892" 116 | ], 117 | "permissions": [ 118 | { 119 | "role": "editor", 120 | "type": "user_permission", 121 | "user_id": "0c5f02f3-495d-4b73-b1c5-9f6fe03a8c26" 122 | } 123 | ], 124 | "created_time": 1606008660000, 125 | "last_edited_time": 1606164600000, 126 | "parent_id": "f687f7de-7f4c-4a86-b109-941a8dae92d2", 127 | "parent_table": "space", 128 | "alive": true, 129 | "created_by_table": "notion_user", 130 | "created_by_id": "0c5f02f3-495d-4b73-b1c5-9f6fe03a8c26", 131 | "last_edited_by_table": "notion_user", 132 | "last_edited_by_id": "0c5f02f3-495d-4b73-b1c5-9f6fe03a8c26", 133 | "shard_id": 955090, 134 | "space_id": "f687f7de-7f4c-4a86-b109-941a8dae92d2" 135 | } 136 | } 137 | }, 138 | "space": { 139 | "f687f7de-7f4c-4a86-b109-941a8dae92d2": { 140 | "role": "editor", 141 | "value": { 142 | "id": "f687f7de-7f4c-4a86-b109-941a8dae92d2", 143 | "version": 185, 144 | "name": "Dan's Workspace", 145 | "domain": "danmurphy", 146 | "permissions": [ 147 | { 148 | "role": "editor", 149 | "type": "user_permission", 150 | "user_id": "0c5f02f3-495d-4b73-b1c5-9f6fe03a8c26" 151 | } 152 | ], 153 | "beta_enabled": false, 154 | "pages": [ 155 | "66447bc8-17f0-44bc-81ed-3cf4802e9b00", 156 | "9268e71e-aaa8-4c54-a865-bd6e3d5472ce", 157 | "c7e1114a-f161-4b94-a7e7-704fd13d5545", 158 | "71728e4a-fc6d-41a8-abef-e6c00ac781fc", 159 | "6b5485c5-94b8-4e0b-a86f-abb455394087", 160 | "c335c2a9-7cb0-4847-9db8-a7f96e64db33", 161 | "861cdea1-ea57-47b9-a529-4c52569b8be8", 162 | "be0cda9c-054b-47e4-8937-bdc4ca2badc6", 163 | "7457ecd2-ba7e-43f1-a81b-f324586e0b38", 164 | "31fab05b-ded9-42ed-b4aa-0b8da0aa61bc", 165 | "cdc37dcc-d283-42a0-9cd9-863d36a7ae80", 166 | "abc7df36-0879-4db9-a3a2-cc780fc7ae01", 167 | "67af938e-4199-4bcd-b3b4-060d062271dd", 168 | "c040f0b8-d845-414a-aa52-3d7309b72d32", 169 | "7f5d96a9-a197-42ab-b5b8-7800a2c99840", 170 | "e3401dbc-5a89-4ab1-80ca-d1e0df475497", 171 | "00394898-7a6a-423f-a2ee-9be44b7300cc", 172 | "61981b4c-cc1c-4f67-a7d5-3ff6df03ad43", 173 | "67e187b1-d9fe-42fe-8ae3-e9532496b636" 174 | ], 175 | "disable_public_access": false, 176 | "disable_guests": false, 177 | "disable_move_to_space": false, 178 | "disable_export": false, 179 | "created_time": 1588292657021, 180 | "last_edited_time": 1606008660000, 181 | "created_by_table": "notion_user", 182 | "created_by_id": "0c5f02f3-495d-4b73-b1c5-9f6fe03a8c26", 183 | "last_edited_by_table": "notion_user", 184 | "last_edited_by_id": "0c5f02f3-495d-4b73-b1c5-9f6fe03a8c26", 185 | "shard_id": 955090, 186 | "plan_type": "personal", 187 | "invite_link_code": "df2e9c349d7e736dffbd69b5e84ef4860b4d8fe8", 188 | "invite_link_enabled": true 189 | } 190 | } 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /spec/fixtures/notion_block_two_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "block": { 3 | "d7407d2c-adbb-497c-b99f-9f9d426d2b18": { 4 | "role": "editor", 5 | "value": { 6 | "id": "d7407d2c-adbb-497c-b99f-9f9d426d2b18", 7 | "version": 46, 8 | "type": "text", 9 | "properties": { 10 | "title": [["second block for testing purposes..."]] 11 | }, 12 | "created_time": 1606161600000, 13 | "last_edited_time": 1606164420000, 14 | "parent_id": "9c50a7b3-9ad7-4f2b-aa08-b3c95f1f19e7", 15 | "parent_table": "block", 16 | "alive": true, 17 | "created_by_table": "notion_user", 18 | "created_by_id": "0c5f02f3-495d-4b73-b1c5-9f6fe03a8c26", 19 | "last_edited_by_table": "notion_user", 20 | "last_edited_by_id": "0c5f02f3-495d-4b73-b1c5-9f6fe03a8c26", 21 | "shard_id": 955090, 22 | "space_id": "f687f7de-7f4c-4a86-b109-941a8dae92d2" 23 | } 24 | }, 25 | "9c50a7b3-9ad7-4f2b-aa08-b3c95f1f19e7": { 26 | "role": "editor", 27 | "value": { 28 | "id": "9c50a7b3-9ad7-4f2b-aa08-b3c95f1f19e7", 29 | "version": 34, 30 | "type": "page", 31 | "properties": { 32 | "title": [["CORE.RB TESTS"]] 33 | }, 34 | "content": [ 35 | "1f5ae85f-f89f-4779-9fa4-b30c3b229cdb", 36 | "d7407d2c-adbb-497c-b99f-9f9d426d2b18", 37 | "34d03794-ecdd-e42d-bb76-7e4aa77b6503", 38 | "f1664a99-165b-49cc-811c-84f37655908a" 39 | ], 40 | "format": { 41 | "page_icon": "🍾" 42 | }, 43 | "created_time": 1606148662319, 44 | "last_edited_time": 1606165980000, 45 | "parent_id": "66447bc8-17f0-44bc-81ed-3cf4802e9b00", 46 | "parent_table": "block", 47 | "alive": true, 48 | "created_by_table": "notion_user", 49 | "created_by_id": "0c5f02f3-495d-4b73-b1c5-9f6fe03a8c26", 50 | "last_edited_by_table": "notion_user", 51 | "last_edited_by_id": "0c5f02f3-495d-4b73-b1c5-9f6fe03a8c26", 52 | "shard_id": 955090, 53 | "space_id": "f687f7de-7f4c-4a86-b109-941a8dae92d2" 54 | } 55 | }, 56 | "66447bc8-17f0-44bc-81ed-3cf4802e9b00": { 57 | "role": "editor", 58 | "value": { 59 | "id": "66447bc8-17f0-44bc-81ed-3cf4802e9b00", 60 | "version": 540, 61 | "type": "page", 62 | "properties": { 63 | "title": [["Notion API Testing"]] 64 | }, 65 | "content": [ 66 | "ee0a6531-44cd-439f-a68c-1bdccbebfc8a", 67 | "f13da22b-9012-4c49-ac41-6b7f97bd519e", 68 | "9c50a7b3-9ad7-4f2b-aa08-b3c95f1f19e7", 69 | "b94758d4-e7df-4af8-863f-b90c11b35fff", 70 | "7c1d1c19-77ee-45cd-bc3d-82fad4bd1a78", 71 | "2cbbe0bf-34cd-409b-9162-64284b33e526", 72 | "9edc0f82-1ec2-4dff-9167-6b3e2dd0f730", 73 | "ba87b719-9207-49a7-b646-de042b266ba8", 74 | "1348703c-ad64-e138-20a7-58b13018af86", 75 | "f5ed36eb-e5c3-4c52-bda8-60be03315332", 76 | "88d108d2-e54b-1f04-4ea0-c4e2ed6c4a12", 77 | "e6e66202-8211-40a8-bffc-8215b8d07dad", 78 | "f9b96cda-6c2f-431d-906d-8c0cb892afda", 79 | "bf9892b8-8c39-47a5-9d03-2dfab5e69179", 80 | "a1702f9b-b092-4703-8f08-90339c6d9176", 81 | "0f9b43d6-448b-2a6d-e027-1a419a98cad2", 82 | "268f3c0d-c201-e5c4-3c79-426791848150", 83 | "dde32522-a7a4-ba1d-844a-d9dbfa5c9994", 84 | "54b58ffe-8258-898e-99fb-6e7e260840a3", 85 | "8ab39c03-b097-4cbf-8dc3-c37b667b371c", 86 | "1353c9ab-2509-a0ff-cbce-2cba1dd2a2e0", 87 | "b2230a72-9472-8b30-1682-cb92b53e71d8", 88 | "592743ff-9904-c9f4-d490-1d558ae918e5", 89 | "20bfcad0-8fe4-452d-a4f5-ff69ac75ad0f", 90 | "58614350-f17e-47b6-81d2-da757754eef3", 91 | "b0ce97b7-ad38-b9b9-3806-62ec8d098a0c", 92 | "90075461-b7ce-6aa4-314e-556875c69dae", 93 | "db49c942-a957-2498-e3dc-e01e7636d950", 94 | "b4e8d4ad-0ac6-f13e-e6dc-6690c7c29fea", 95 | "057f090b-8e46-4215-8fef-c1224efeddd9", 96 | "4a71d78a-37bf-ab11-9f37-b4f9a7642036", 97 | "5fcfa44c-7f60-b5bf-e2ab-c2e6304a4581", 98 | "24ee3b27-d7b4-4cfb-b837-912deccca5a4", 99 | "5e022e72-994f-40f3-8d1d-cda7f1b8b107", 100 | "9e0578f3-d4d7-4ed9-bda3-f050a6eb6dca", 101 | "5e8c4c40-a444-210d-8475-22386864089c", 102 | "3cdb51fd-25fe-29d6-edc1-7503c9b1e768", 103 | "f76fa467-cdb6-40e4-87ae-8dac19a75efc", 104 | "79bc0290-18ec-420b-ae1b-57a8f3744fcb", 105 | "9a07d006-9295-4a2c-be35-9b6bf78ee7fe", 106 | "c9db948d-91c5-0048-3a0a-7674f4e874a0", 107 | "e637befa-128f-6950-c9c0-8e71fc159c5d", 108 | "ef74f834-7185-e9e8-5bdd-5f8bece38623", 109 | "1dca3f8a-8d85-3a35-5d4c-34cfd7a066e9", 110 | "b2852fb7-a8f5-2e69-6769-c9d2a5878cf4", 111 | "054ab04e-694f-462d-30d5-1068038eb379", 112 | "1cff7eb6-ea6e-4293-7cd3-25cf79f6c7f0", 113 | "2c9b2885-ad4d-4f1f-a077-d3048c79232c", 114 | "f86323b9-8b79-74a7-f268-b1a25ff4a892" 115 | ], 116 | "permissions": [ 117 | { 118 | "role": "editor", 119 | "type": "user_permission", 120 | "user_id": "0c5f02f3-495d-4b73-b1c5-9f6fe03a8c26" 121 | } 122 | ], 123 | "created_time": 1606008660000, 124 | "last_edited_time": 1606164600000, 125 | "parent_id": "f687f7de-7f4c-4a86-b109-941a8dae92d2", 126 | "parent_table": "space", 127 | "alive": true, 128 | "created_by_table": "notion_user", 129 | "created_by_id": "0c5f02f3-495d-4b73-b1c5-9f6fe03a8c26", 130 | "last_edited_by_table": "notion_user", 131 | "last_edited_by_id": "0c5f02f3-495d-4b73-b1c5-9f6fe03a8c26", 132 | "shard_id": 955090, 133 | "space_id": "f687f7de-7f4c-4a86-b109-941a8dae92d2" 134 | } 135 | } 136 | }, 137 | "space": { 138 | "f687f7de-7f4c-4a86-b109-941a8dae92d2": { 139 | "role": "editor", 140 | "value": { 141 | "id": "f687f7de-7f4c-4a86-b109-941a8dae92d2", 142 | "version": 185, 143 | "name": "Dan's Workspace", 144 | "domain": "danmurphy", 145 | "permissions": [ 146 | { 147 | "role": "editor", 148 | "type": "user_permission", 149 | "user_id": "0c5f02f3-495d-4b73-b1c5-9f6fe03a8c26" 150 | } 151 | ], 152 | "beta_enabled": false, 153 | "pages": [ 154 | "66447bc8-17f0-44bc-81ed-3cf4802e9b00", 155 | "9268e71e-aaa8-4c54-a865-bd6e3d5472ce", 156 | "c7e1114a-f161-4b94-a7e7-704fd13d5545", 157 | "71728e4a-fc6d-41a8-abef-e6c00ac781fc", 158 | "6b5485c5-94b8-4e0b-a86f-abb455394087", 159 | "c335c2a9-7cb0-4847-9db8-a7f96e64db33", 160 | "861cdea1-ea57-47b9-a529-4c52569b8be8", 161 | "be0cda9c-054b-47e4-8937-bdc4ca2badc6", 162 | "7457ecd2-ba7e-43f1-a81b-f324586e0b38", 163 | "31fab05b-ded9-42ed-b4aa-0b8da0aa61bc", 164 | "cdc37dcc-d283-42a0-9cd9-863d36a7ae80", 165 | "abc7df36-0879-4db9-a3a2-cc780fc7ae01", 166 | "67af938e-4199-4bcd-b3b4-060d062271dd", 167 | "c040f0b8-d845-414a-aa52-3d7309b72d32", 168 | "7f5d96a9-a197-42ab-b5b8-7800a2c99840", 169 | "e3401dbc-5a89-4ab1-80ca-d1e0df475497", 170 | "00394898-7a6a-423f-a2ee-9be44b7300cc", 171 | "61981b4c-cc1c-4f67-a7d5-3ff6df03ad43", 172 | "67e187b1-d9fe-42fe-8ae3-e9532496b636" 173 | ], 174 | "disable_public_access": false, 175 | "disable_guests": false, 176 | "disable_move_to_space": false, 177 | "disable_export": false, 178 | "created_time": 1588292657021, 179 | "last_edited_time": 1606008660000, 180 | "created_by_table": "notion_user", 181 | "created_by_id": "0c5f02f3-495d-4b73-b1c5-9f6fe03a8c26", 182 | "last_edited_by_table": "notion_user", 183 | "last_edited_by_id": "0c5f02f3-495d-4b73-b1c5-9f6fe03a8c26", 184 | "shard_id": 955090, 185 | "plan_type": "personal", 186 | "invite_link_code": "df2e9c349d7e736dffbd69b5e84ef4860b4d8fe8", 187 | "invite_link_enabled": true 188 | } 189 | } 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /spec/fixtures/notion_collection_view_one_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "block": { 3 | "f1664a99-165b-49cc-811c-84f37655908a": { 4 | "role": "editor", 5 | "value": { 6 | "id": "f1664a99-165b-49cc-811c-84f37655908a", 7 | "version": 29, 8 | "type": "collection_view", 9 | "view_ids": ["7e223541-82c0-4b0b-bf41-8017f673bf6e"], 10 | "collection_id": "a83a6cc0-ce7a-a14e-4483-4bfea6756922", 11 | "created_time": 1606013154935, 12 | "last_edited_time": 1606164600000, 13 | "parent_id": "9c50a7b3-9ad7-4f2b-aa08-b3c95f1f19e7", 14 | "parent_table": "block", 15 | "alive": true, 16 | "created_by_table": "notion_user", 17 | "created_by_id": "0c5f02f3-495d-4b73-b1c5-9f6fe03a8c26", 18 | "last_edited_by_table": "notion_user", 19 | "last_edited_by_id": "0c5f02f3-495d-4b73-b1c5-9f6fe03a8c26", 20 | "shard_id": 955090, 21 | "space_id": "f687f7de-7f4c-4a86-b109-941a8dae92d2" 22 | } 23 | }, 24 | "9c50a7b3-9ad7-4f2b-aa08-b3c95f1f19e7": { 25 | "role": "editor", 26 | "value": { 27 | "id": "9c50a7b3-9ad7-4f2b-aa08-b3c95f1f19e7", 28 | "version": 34, 29 | "type": "page", 30 | "properties": { 31 | "title": [["CORE.RB TESTS"]] 32 | }, 33 | "content": [ 34 | "1f5ae85f-f89f-4779-9fa4-b30c3b229cdb", 35 | "d7407d2c-adbb-497c-b99f-9f9d426d2b18", 36 | "34d03794-ecdd-e42d-bb76-7e4aa77b6503", 37 | "f1664a99-165b-49cc-811c-84f37655908a" 38 | ], 39 | "format": { 40 | "page_icon": "🍾" 41 | }, 42 | "created_time": 1606148662319, 43 | "last_edited_time": 1606165980000, 44 | "parent_id": "66447bc8-17f0-44bc-81ed-3cf4802e9b00", 45 | "parent_table": "block", 46 | "alive": true, 47 | "created_by_table": "notion_user", 48 | "created_by_id": "0c5f02f3-495d-4b73-b1c5-9f6fe03a8c26", 49 | "last_edited_by_table": "notion_user", 50 | "last_edited_by_id": "0c5f02f3-495d-4b73-b1c5-9f6fe03a8c26", 51 | "shard_id": 955090, 52 | "space_id": "f687f7de-7f4c-4a86-b109-941a8dae92d2" 53 | } 54 | }, 55 | "66447bc8-17f0-44bc-81ed-3cf4802e9b00": { 56 | "role": "editor", 57 | "value": { 58 | "id": "66447bc8-17f0-44bc-81ed-3cf4802e9b00", 59 | "version": 540, 60 | "type": "page", 61 | "properties": { 62 | "title": [["Notion API Testing"]] 63 | }, 64 | "content": [ 65 | "ee0a6531-44cd-439f-a68c-1bdccbebfc8a", 66 | "f13da22b-9012-4c49-ac41-6b7f97bd519e", 67 | "9c50a7b3-9ad7-4f2b-aa08-b3c95f1f19e7", 68 | "b94758d4-e7df-4af8-863f-b90c11b35fff", 69 | "7c1d1c19-77ee-45cd-bc3d-82fad4bd1a78", 70 | "2cbbe0bf-34cd-409b-9162-64284b33e526", 71 | "9edc0f82-1ec2-4dff-9167-6b3e2dd0f730", 72 | "ba87b719-9207-49a7-b646-de042b266ba8", 73 | "1348703c-ad64-e138-20a7-58b13018af86", 74 | "f5ed36eb-e5c3-4c52-bda8-60be03315332", 75 | "88d108d2-e54b-1f04-4ea0-c4e2ed6c4a12", 76 | "e6e66202-8211-40a8-bffc-8215b8d07dad", 77 | "f9b96cda-6c2f-431d-906d-8c0cb892afda", 78 | "bf9892b8-8c39-47a5-9d03-2dfab5e69179", 79 | "a1702f9b-b092-4703-8f08-90339c6d9176", 80 | "0f9b43d6-448b-2a6d-e027-1a419a98cad2", 81 | "268f3c0d-c201-e5c4-3c79-426791848150", 82 | "dde32522-a7a4-ba1d-844a-d9dbfa5c9994", 83 | "54b58ffe-8258-898e-99fb-6e7e260840a3", 84 | "8ab39c03-b097-4cbf-8dc3-c37b667b371c", 85 | "1353c9ab-2509-a0ff-cbce-2cba1dd2a2e0", 86 | "b2230a72-9472-8b30-1682-cb92b53e71d8", 87 | "592743ff-9904-c9f4-d490-1d558ae918e5", 88 | "20bfcad0-8fe4-452d-a4f5-ff69ac75ad0f", 89 | "58614350-f17e-47b6-81d2-da757754eef3", 90 | "b0ce97b7-ad38-b9b9-3806-62ec8d098a0c", 91 | "90075461-b7ce-6aa4-314e-556875c69dae", 92 | "db49c942-a957-2498-e3dc-e01e7636d950", 93 | "b4e8d4ad-0ac6-f13e-e6dc-6690c7c29fea", 94 | "057f090b-8e46-4215-8fef-c1224efeddd9", 95 | "4a71d78a-37bf-ab11-9f37-b4f9a7642036", 96 | "5fcfa44c-7f60-b5bf-e2ab-c2e6304a4581", 97 | "24ee3b27-d7b4-4cfb-b837-912deccca5a4", 98 | "5e022e72-994f-40f3-8d1d-cda7f1b8b107", 99 | "9e0578f3-d4d7-4ed9-bda3-f050a6eb6dca", 100 | "5e8c4c40-a444-210d-8475-22386864089c", 101 | "3cdb51fd-25fe-29d6-edc1-7503c9b1e768", 102 | "f76fa467-cdb6-40e4-87ae-8dac19a75efc", 103 | "79bc0290-18ec-420b-ae1b-57a8f3744fcb", 104 | "9a07d006-9295-4a2c-be35-9b6bf78ee7fe", 105 | "c9db948d-91c5-0048-3a0a-7674f4e874a0", 106 | "e637befa-128f-6950-c9c0-8e71fc159c5d", 107 | "ef74f834-7185-e9e8-5bdd-5f8bece38623", 108 | "1dca3f8a-8d85-3a35-5d4c-34cfd7a066e9", 109 | "b2852fb7-a8f5-2e69-6769-c9d2a5878cf4", 110 | "054ab04e-694f-462d-30d5-1068038eb379", 111 | "1cff7eb6-ea6e-4293-7cd3-25cf79f6c7f0", 112 | "2c9b2885-ad4d-4f1f-a077-d3048c79232c", 113 | "f86323b9-8b79-74a7-f268-b1a25ff4a892" 114 | ], 115 | "permissions": [ 116 | { 117 | "role": "editor", 118 | "type": "user_permission", 119 | "user_id": "0c5f02f3-495d-4b73-b1c5-9f6fe03a8c26" 120 | } 121 | ], 122 | "created_time": 1606008660000, 123 | "last_edited_time": 1606164600000, 124 | "parent_id": "f687f7de-7f4c-4a86-b109-941a8dae92d2", 125 | "parent_table": "space", 126 | "alive": true, 127 | "created_by_table": "notion_user", 128 | "created_by_id": "0c5f02f3-495d-4b73-b1c5-9f6fe03a8c26", 129 | "last_edited_by_table": "notion_user", 130 | "last_edited_by_id": "0c5f02f3-495d-4b73-b1c5-9f6fe03a8c26", 131 | "shard_id": 955090, 132 | "space_id": "f687f7de-7f4c-4a86-b109-941a8dae92d2" 133 | } 134 | } 135 | }, 136 | "collection_view": { 137 | "7e223541-82c0-4b0b-bf41-8017f673bf6e": { 138 | "role": "editor", 139 | "value": { 140 | "id": "7e223541-82c0-4b0b-bf41-8017f673bf6e", 141 | "version": 54, 142 | "type": "table", 143 | "name": "Default View", 144 | "parent_id": "f1664a99-165b-49cc-811c-84f37655908a", 145 | "parent_table": "block", 146 | "alive": true, 147 | "page_sort": [ 148 | "cdcc3283-953f-451c-de89-3b57978a26ce", 149 | "04edc78f-be65-3602-72c0-66ff73716824", 150 | "b39a1898-42a9-a457-2d14-e68bff9381c2", 151 | "5b08d25a-66ec-c13e-bd18-8f2f32642978", 152 | "ba280c0c-7f8c-f3b4-799b-043fd4c79518", 153 | "b51f3e0d-01ff-5593-b9c3-9f61e89bf913", 154 | "a42ee8e5-44d5-b50f-859f-0de2bf61e90f", 155 | "7b19a0e1-1c16-7e1c-3a04-23f5e210cfea", 156 | "eb32d0a3-b19b-555c-e92f-c5854b51d73b", 157 | "1ee7ebe2-6c41-0d40-7c5a-bfabcc8a17f9", 158 | "4276cf67-2ef4-0d23-39f9-fa8d669eb12d", 159 | "6cc88b53-b4a1-e77a-70f1-f247878e1708", 160 | "9067d7f6-0008-6cf0-5d0f-037596b2cbf5", 161 | "7ece7d5e-5d6d-4d10-a4ca-c64cac5f0b1d", 162 | "99931edd-347c-1fd1-cd72-f18d693b6e90", 163 | "bf0afb6e-49fc-dfd7-4476-8525ac25df07", 164 | "a0777f8c-8e61-4db1-f61f-df322ee6e489", 165 | "f138e30c-9f4b-4647-f24c-3846b9adb631", 166 | "5baf2e0c-25f6-42f9-028c-4b4e7ac6594b", 167 | "3253fa3f-27ef-4e4e-deaf-eeafe4928091", 168 | "6348e88c-526e-bbd2-ff06-ca6a254fc22b", 169 | "527c234a-283f-2830-2117-f87ec3041785", 170 | "f2a3f9e0-e58e-e3d6-f8db-332199aa1cd2", 171 | "932bc28c-0482-20ac-73b5-212fcbcc841f", 172 | "9470737e-6413-622b-66ba-da36030fe94f", 173 | "83ed4e66-7340-8cad-9db5-d74e6d2776b4", 174 | "51e2443b-cfc6-e174-7122-46e1ca44ade7", 175 | "0863bd40-f80b-cf6b-42d7-89562d105e79", 176 | "77772c8b-5c3e-0f40-7840-49ee1a926555", 177 | "5d6bd569-5c5f-69fb-c551-008675a698cc", 178 | "2e8edc01-c303-5d3b-86d4-4228271019f9", 179 | "b3175f00-f8bb-404b-bdeb-210f0628c5da", 180 | "9457c48e-5847-d85d-7cd6-087879c6d836", 181 | "f8692479-df7c-a86a-35bc-e04cd67b0889", 182 | "e139e2c8-87e4-561a-df03-7b2e5378c41c", 183 | "308911e5-c67a-fc44-efd5-6443249da0bc", 184 | "a8fbaad1-5294-c2bc-cbe3-0a75a4e45388", 185 | "df44b9c0-af2d-7dd3-c380-ae3e9b0c24ab", 186 | "0fda1947-98b7-8300-9684-587f2591cf03", 187 | "89387072-0656-4a05-a397-090f0c84fec3", 188 | "a820ba85-8919-18da-e92e-5baf69a6ae90", 189 | "ae9cba8a-1b49-5aa2-55eb-23a5e5e04ac9", 190 | "f586b50a-2b52-c753-8313-37f5946564fd", 191 | "b861c429-0bc2-e784-458d-7b07d8df8d6e", 192 | "2042e60e-7e13-5635-af2c-ed4cb46e1fc1", 193 | "e86be770-7ce5-04a8-18c6-6a2ade9657be", 194 | "69ee98d9-e3b9-0273-7891-697a33d9ad13", 195 | "feeafbe7-58e8-86c3-5878-e48994e08265", 196 | "4c2abedc-e489-016c-2614-59b446efd02e", 197 | "dd8eacca-1072-0b14-b916-a5eac05e94cf", 198 | "364dc828-93d6-d1ca-7585-1753173bdbcb", 199 | "7fe28870-8c96-3d0f-7fb8-bdb1b5eafbcb", 200 | "659f8cb8-5b05-140e-241c-954dbff9a2ed", 201 | "dfff4cc9-acea-60f3-45f8-d1d146efd7e2", 202 | "e4664a4f-875d-7b2e-419a-ca0911ff9277", 203 | "fc0e5b35-00eb-97ef-3082-e19da4132c13", 204 | "48cac4ba-9f7b-2735-8d78-7335560a04b4", 205 | "b0c53582-f07b-ac59-b86f-1efc249568c9", 206 | "e252c004-3af5-f946-bdb0-3353b1055106", 207 | "3af0c813-cb1c-2fe9-b7ef-3b7521c5816d", 208 | "6da81a22-08ae-b062-17a8-0b05321041e3", 209 | "01fecf51-b58c-ae8b-0a10-0a16f6900577", 210 | "522c686a-38a3-b835-b6b5-4b1d73fbce67", 211 | "e2b724bd-3511-7143-15cb-6c1867811bbf", 212 | "7140b167-76af-9304-d369-8e00b3f8d371", 213 | "c820a616-5d57-9b08-03ec-7b950cf7c6cb", 214 | "adc329f4-7686-d6a3-6e5e-8b578d9c989c", 215 | "57816d55-2d16-b321-84db-47f6590d4520", 216 | "3277edf3-2350-bef1-e09d-42c112b7ca73", 217 | "06e04f08-3cf1-b763-cf50-8d6e739c6809", 218 | "f570944d-0a52-6540-400b-44c140dd5288", 219 | "ae0a9f68-3951-a5a9-3b24-60d9089313e4", 220 | "5ee726ca-4559-e309-af88-9f25e21af767", 221 | "f4c5d878-f2bf-cfa1-12eb-c764a1ab9cc8", 222 | "dcadbc5f-e550-49c3-97da-61262dedb8bc", 223 | "27ed5f67-f1d3-35b6-cd32-e36c0963bd9e", 224 | "cb90cafc-9aad-1e38-984c-fd82c45ba407", 225 | "880cc07d-b4d8-7e3f-6c8b-d7f34c54ba06", 226 | "17e99e7e-ef7f-1242-d5a7-8155f0d0432c", 227 | "231f8b52-defb-26ce-9625-3117df3caad3", 228 | "598182d5-7599-b508-793d-e1ed8fdeac5a", 229 | "e3e4b1d3-fa91-6ac7-dadf-e42cae3561fa", 230 | "d4e5a620-1193-990a-0b00-ee31eb271f5e", 231 | "1990e983-69a2-12e6-93eb-0419efc648fb", 232 | "2ff76189-4ef9-c143-055a-04dc8d4a0843", 233 | "5fc7d179-f02a-4f23-76d0-deaf59cdcf60", 234 | "41e0f1bc-8f73-f300-f780-7177b7ead2ed", 235 | "dcfa4235-bd49-62f5-4c7c-b1a276925c67", 236 | "06d4789f-a228-888f-ba97-7a89e0363452", 237 | "ff63c63b-6955-6e8f-ff44-655cc31dcc4d", 238 | "b38ff285-bb5d-c8fb-374f-c747a293e2e4", 239 | "b20e9e29-de26-a2f3-ab0f-13f008e17172", 240 | "49600084-629d-4fd6-bb1d-feda26564d8d", 241 | "18f2004d-d8b7-4188-b950-2622dbc70892" 242 | ], 243 | "shard_id": 955090, 244 | "space_id": "f687f7de-7f4c-4a86-b109-941a8dae92d2" 245 | } 246 | } 247 | }, 248 | "collection": { 249 | "a83a6cc0-ce7a-a14e-4483-4bfea6756922": { 250 | "role": "editor", 251 | "value": { 252 | "id": "a83a6cc0-ce7a-a14e-4483-4bfea6756922", 253 | "version": 51, 254 | "name": [["Test Emoji Data"]], 255 | "schema": { 256 | "tags": { 257 | "name": "tags", 258 | "type": "text" 259 | }, 260 | "title": { 261 | "name": "emoji", 262 | "type": "title" 263 | }, 264 | "aliases": { 265 | "name": "aliases", 266 | "type": "text" 267 | }, 268 | "category": { 269 | "name": "category", 270 | "type": "text" 271 | }, 272 | "description": { 273 | "name": "description", 274 | "type": "text" 275 | }, 276 | "ios_version": { 277 | "name": "ios_version", 278 | "type": "text" 279 | }, 280 | "unicode_version": { 281 | "name": "unicode_version", 282 | "type": "text" 283 | } 284 | }, 285 | "format": { 286 | "collection_page_properties": [ 287 | { 288 | "visible": false, 289 | "property": "aliases" 290 | }, 291 | { 292 | "visible": false, 293 | "property": "category" 294 | }, 295 | { 296 | "visible": false, 297 | "property": "description" 298 | }, 299 | { 300 | "visible": false, 301 | "property": "ios_version" 302 | }, 303 | { 304 | "visible": false, 305 | "property": "tags" 306 | }, 307 | { 308 | "visible": false, 309 | "property": "unicode_version" 310 | }, 311 | { 312 | "visible": true, 313 | "property": "Foz=" 314 | } 315 | ] 316 | }, 317 | "parent_id": "f1664a99-165b-49cc-811c-84f37655908a", 318 | "parent_table": "block", 319 | "alive": true, 320 | "migrated": true 321 | } 322 | } 323 | }, 324 | "space": { 325 | "f687f7de-7f4c-4a86-b109-941a8dae92d2": { 326 | "role": "editor", 327 | "value": { 328 | "id": "f687f7de-7f4c-4a86-b109-941a8dae92d2", 329 | "version": 185, 330 | "name": "Dan's Workspace", 331 | "domain": "danmurphy", 332 | "permissions": [ 333 | { 334 | "role": "editor", 335 | "type": "user_permission", 336 | "user_id": "0c5f02f3-495d-4b73-b1c5-9f6fe03a8c26" 337 | } 338 | ], 339 | "beta_enabled": false, 340 | "pages": [ 341 | "66447bc8-17f0-44bc-81ed-3cf4802e9b00", 342 | "9268e71e-aaa8-4c54-a865-bd6e3d5472ce", 343 | "c7e1114a-f161-4b94-a7e7-704fd13d5545", 344 | "71728e4a-fc6d-41a8-abef-e6c00ac781fc", 345 | "6b5485c5-94b8-4e0b-a86f-abb455394087", 346 | "c335c2a9-7cb0-4847-9db8-a7f96e64db33", 347 | "861cdea1-ea57-47b9-a529-4c52569b8be8", 348 | "be0cda9c-054b-47e4-8937-bdc4ca2badc6", 349 | "7457ecd2-ba7e-43f1-a81b-f324586e0b38", 350 | "31fab05b-ded9-42ed-b4aa-0b8da0aa61bc", 351 | "cdc37dcc-d283-42a0-9cd9-863d36a7ae80", 352 | "abc7df36-0879-4db9-a3a2-cc780fc7ae01", 353 | "67af938e-4199-4bcd-b3b4-060d062271dd", 354 | "c040f0b8-d845-414a-aa52-3d7309b72d32", 355 | "7f5d96a9-a197-42ab-b5b8-7800a2c99840", 356 | "e3401dbc-5a89-4ab1-80ca-d1e0df475497", 357 | "00394898-7a6a-423f-a2ee-9be44b7300cc", 358 | "61981b4c-cc1c-4f67-a7d5-3ff6df03ad43", 359 | "67e187b1-d9fe-42fe-8ae3-e9532496b636" 360 | ], 361 | "disable_public_access": false, 362 | "disable_guests": false, 363 | "disable_move_to_space": false, 364 | "disable_export": false, 365 | "created_time": 1588292657021, 366 | "last_edited_time": 1606008660000, 367 | "created_by_table": "notion_user", 368 | "created_by_id": "0c5f02f3-495d-4b73-b1c5-9f6fe03a8c26", 369 | "last_edited_by_table": "notion_user", 370 | "last_edited_by_id": "0c5f02f3-495d-4b73-b1c5-9f6fe03a8c26", 371 | "shard_id": 955090, 372 | "plan_type": "personal", 373 | "invite_link_code": "df2e9c349d7e736dffbd69b5e84ef4860b4d8fe8", 374 | "invite_link_enabled": true 375 | } 376 | } 377 | } 378 | } 379 | -------------------------------------------------------------------------------- /spec/fixtures/notion_collection_view_two_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "block": { 3 | "34d03794-ecdd-e42d-bb76-7e4aa77b6503": { 4 | "role": "editor", 5 | "value": { 6 | "id": "34d03794-ecdd-e42d-bb76-7e4aa77b6503", 7 | "version": 24, 8 | "type": "collection_view", 9 | "view_ids": ["5fdb08da-0732-49dc-d0c3-2e31fccca73a"], 10 | "collection_id": "5ea0fa7c-00cd-4ee0-1915-8b5c423f8f3a", 11 | "created_time": 1606014865990, 12 | "last_edited_time": 1606164540000, 13 | "parent_id": "9c50a7b3-9ad7-4f2b-aa08-b3c95f1f19e7", 14 | "parent_table": "block", 15 | "alive": true, 16 | "created_by_table": "notion_user", 17 | "created_by_id": "0c5f02f3-495d-4b73-b1c5-9f6fe03a8c26", 18 | "last_edited_by_table": "notion_user", 19 | "last_edited_by_id": "0c5f02f3-495d-4b73-b1c5-9f6fe03a8c26", 20 | "shard_id": 955090, 21 | "space_id": "f687f7de-7f4c-4a86-b109-941a8dae92d2" 22 | } 23 | }, 24 | "9c50a7b3-9ad7-4f2b-aa08-b3c95f1f19e7": { 25 | "role": "editor", 26 | "value": { 27 | "id": "9c50a7b3-9ad7-4f2b-aa08-b3c95f1f19e7", 28 | "version": 34, 29 | "type": "page", 30 | "properties": { 31 | "title": [["CORE.RB TESTS"]] 32 | }, 33 | "content": [ 34 | "1f5ae85f-f89f-4779-9fa4-b30c3b229cdb", 35 | "d7407d2c-adbb-497c-b99f-9f9d426d2b18", 36 | "34d03794-ecdd-e42d-bb76-7e4aa77b6503", 37 | "f1664a99-165b-49cc-811c-84f37655908a" 38 | ], 39 | "format": { 40 | "page_icon": "🍾" 41 | }, 42 | "created_time": 1606148662319, 43 | "last_edited_time": 1606165980000, 44 | "parent_id": "66447bc8-17f0-44bc-81ed-3cf4802e9b00", 45 | "parent_table": "block", 46 | "alive": true, 47 | "created_by_table": "notion_user", 48 | "created_by_id": "0c5f02f3-495d-4b73-b1c5-9f6fe03a8c26", 49 | "last_edited_by_table": "notion_user", 50 | "last_edited_by_id": "0c5f02f3-495d-4b73-b1c5-9f6fe03a8c26", 51 | "shard_id": 955090, 52 | "space_id": "f687f7de-7f4c-4a86-b109-941a8dae92d2" 53 | } 54 | }, 55 | "66447bc8-17f0-44bc-81ed-3cf4802e9b00": { 56 | "role": "editor", 57 | "value": { 58 | "id": "66447bc8-17f0-44bc-81ed-3cf4802e9b00", 59 | "version": 540, 60 | "type": "page", 61 | "properties": { 62 | "title": [["Notion API Testing"]] 63 | }, 64 | "content": [ 65 | "ee0a6531-44cd-439f-a68c-1bdccbebfc8a", 66 | "f13da22b-9012-4c49-ac41-6b7f97bd519e", 67 | "9c50a7b3-9ad7-4f2b-aa08-b3c95f1f19e7", 68 | "b94758d4-e7df-4af8-863f-b90c11b35fff", 69 | "7c1d1c19-77ee-45cd-bc3d-82fad4bd1a78", 70 | "2cbbe0bf-34cd-409b-9162-64284b33e526", 71 | "9edc0f82-1ec2-4dff-9167-6b3e2dd0f730", 72 | "ba87b719-9207-49a7-b646-de042b266ba8", 73 | "1348703c-ad64-e138-20a7-58b13018af86", 74 | "f5ed36eb-e5c3-4c52-bda8-60be03315332", 75 | "88d108d2-e54b-1f04-4ea0-c4e2ed6c4a12", 76 | "e6e66202-8211-40a8-bffc-8215b8d07dad", 77 | "f9b96cda-6c2f-431d-906d-8c0cb892afda", 78 | "bf9892b8-8c39-47a5-9d03-2dfab5e69179", 79 | "a1702f9b-b092-4703-8f08-90339c6d9176", 80 | "0f9b43d6-448b-2a6d-e027-1a419a98cad2", 81 | "268f3c0d-c201-e5c4-3c79-426791848150", 82 | "dde32522-a7a4-ba1d-844a-d9dbfa5c9994", 83 | "54b58ffe-8258-898e-99fb-6e7e260840a3", 84 | "8ab39c03-b097-4cbf-8dc3-c37b667b371c", 85 | "1353c9ab-2509-a0ff-cbce-2cba1dd2a2e0", 86 | "b2230a72-9472-8b30-1682-cb92b53e71d8", 87 | "592743ff-9904-c9f4-d490-1d558ae918e5", 88 | "20bfcad0-8fe4-452d-a4f5-ff69ac75ad0f", 89 | "58614350-f17e-47b6-81d2-da757754eef3", 90 | "b0ce97b7-ad38-b9b9-3806-62ec8d098a0c", 91 | "90075461-b7ce-6aa4-314e-556875c69dae", 92 | "db49c942-a957-2498-e3dc-e01e7636d950", 93 | "b4e8d4ad-0ac6-f13e-e6dc-6690c7c29fea", 94 | "057f090b-8e46-4215-8fef-c1224efeddd9", 95 | "4a71d78a-37bf-ab11-9f37-b4f9a7642036", 96 | "5fcfa44c-7f60-b5bf-e2ab-c2e6304a4581", 97 | "24ee3b27-d7b4-4cfb-b837-912deccca5a4", 98 | "5e022e72-994f-40f3-8d1d-cda7f1b8b107", 99 | "9e0578f3-d4d7-4ed9-bda3-f050a6eb6dca", 100 | "5e8c4c40-a444-210d-8475-22386864089c", 101 | "3cdb51fd-25fe-29d6-edc1-7503c9b1e768", 102 | "f76fa467-cdb6-40e4-87ae-8dac19a75efc", 103 | "79bc0290-18ec-420b-ae1b-57a8f3744fcb", 104 | "9a07d006-9295-4a2c-be35-9b6bf78ee7fe", 105 | "c9db948d-91c5-0048-3a0a-7674f4e874a0", 106 | "e637befa-128f-6950-c9c0-8e71fc159c5d", 107 | "ef74f834-7185-e9e8-5bdd-5f8bece38623", 108 | "1dca3f8a-8d85-3a35-5d4c-34cfd7a066e9", 109 | "b2852fb7-a8f5-2e69-6769-c9d2a5878cf4", 110 | "054ab04e-694f-462d-30d5-1068038eb379", 111 | "1cff7eb6-ea6e-4293-7cd3-25cf79f6c7f0", 112 | "2c9b2885-ad4d-4f1f-a077-d3048c79232c", 113 | "f86323b9-8b79-74a7-f268-b1a25ff4a892" 114 | ], 115 | "permissions": [ 116 | { 117 | "role": "editor", 118 | "type": "user_permission", 119 | "user_id": "0c5f02f3-495d-4b73-b1c5-9f6fe03a8c26" 120 | } 121 | ], 122 | "created_time": 1606008660000, 123 | "last_edited_time": 1606164600000, 124 | "parent_id": "f687f7de-7f4c-4a86-b109-941a8dae92d2", 125 | "parent_table": "space", 126 | "alive": true, 127 | "created_by_table": "notion_user", 128 | "created_by_id": "0c5f02f3-495d-4b73-b1c5-9f6fe03a8c26", 129 | "last_edited_by_table": "notion_user", 130 | "last_edited_by_id": "0c5f02f3-495d-4b73-b1c5-9f6fe03a8c26", 131 | "shard_id": 955090, 132 | "space_id": "f687f7de-7f4c-4a86-b109-941a8dae92d2" 133 | } 134 | } 135 | }, 136 | "collection_view": { 137 | "5fdb08da-0732-49dc-d0c3-2e31fccca73a": { 138 | "role": "editor", 139 | "value": { 140 | "id": "5fdb08da-0732-49dc-d0c3-2e31fccca73a", 141 | "version": 1, 142 | "type": "table", 143 | "name": "Default View", 144 | "parent_id": "34d03794-ecdd-e42d-bb76-7e4aa77b6503", 145 | "parent_table": "block", 146 | "alive": true, 147 | "page_sort": [ 148 | "785f4e24-e489-a316-50cf-b0b100c6673a", 149 | "78642d95-da23-744c-b084-46d039927bba", 150 | "96dff83c-6961-894c-39c2-c2c8bfcbfa90", 151 | "87ae8ae7-5518-fbe1-748e-eb690c707fac", 152 | "5a03dfa9-b85e-e07b-1a73-1a0a79f19f2f", 153 | "522eeef6-1e94-9309-8504-333fac06e3c4", 154 | "367ac33f-2124-9275-ca3a-88e492c774cf", 155 | "5a50bdd4-69c5-0708-5093-b135676e83c1", 156 | "edd4717c-bd35-aec9-9417-a13dfd874f00", 157 | "467b0d40-c711-9655-7bb3-10e66e627bbb", 158 | "47d7e0f8-dbaa-ed0b-45de-001c585e5f2c", 159 | "db07c63f-bb5b-0a2a-f502-62cf99be47ae", 160 | "a9856b1d-dff2-0368-9c99-da133783d051", 161 | "51dd712e-4653-98e6-d6c5-077b5a993f82", 162 | "7f565ea2-3154-39de-43d0-cbf2955370d0", 163 | "384310e4-a267-84b1-f63b-28673dd8dcbf", 164 | "063ef5e5-b7a7-3c67-1624-1bbc08498f95", 165 | "d0532c20-4b47-67ab-1a5b-033bcf00574c", 166 | "1a06a97a-f01a-d127-51b4-cf29ebefba47", 167 | "6534aad0-5ea4-53b9-a06c-57ecfb91e869", 168 | "ff9b8b89-1fed-f955-4afa-5a071198b0ee", 169 | "721fe76a-9e3c-d348-8324-994c95d77b2e", 170 | "6b0cb603-5e10-075d-f0d3-e4985b954b3e", 171 | "6e73629b-4fce-ed3d-6366-a64df4d7dc79", 172 | "20f4e316-4d44-981f-7089-c3b7deabce87", 173 | "f4dee37f-1ac2-d8d8-1c28-839dd3e69ae6", 174 | "f1c7077f-44a9-113d-a156-90ab6880c3e2", 175 | "44d0c5d7-44dc-cd9f-ba8e-2a3d7e6bafff", 176 | "42d3339e-13dc-52a6-3a5a-baa62b5ca5ae", 177 | "e3861041-5f73-f80d-3fb5-4b7aa6f9fabd", 178 | "ad55eadc-c83f-4251-3925-a74d6b37b1fd", 179 | "c7e75fc9-2f49-b89c-cbb9-9b01fd1c5bf7", 180 | "46f9932b-b16a-50da-9a60-cf26e28b67fe", 181 | "d1cb029a-87f4-4e66-8f1d-b47fcb1870fe", 182 | "834517f7-2f2e-fb4d-c28e-c1aa40fc767e", 183 | "24a16e13-0156-846c-f522-e35105a0448c", 184 | "ff8654bd-227b-7f55-a4cd-39c756996287", 185 | "9f8c4659-9d20-d3c4-bd35-543e08d5f203", 186 | "c04b24f1-07f1-609f-e677-e931a5a13f01", 187 | "5286db7b-884b-49c6-b33a-e4e8a922b4cf", 188 | "38c28379-65ad-4b13-2f73-2ad602eec5d5", 189 | "d8bbb625-32f1-fa3b-f34f-55970f36707c", 190 | "1e6b48a3-5f75-8656-404c-72013e34178d", 191 | "358e4533-54ef-2e04-e1f1-d6a336922596", 192 | "fbf44f93-52ee-0e88-262a-94982ffb3fb2" 193 | ], 194 | "shard_id": 955090, 195 | "space_id": "f687f7de-7f4c-4a86-b109-941a8dae92d2" 196 | } 197 | } 198 | }, 199 | "collection": { 200 | "5ea0fa7c-00cd-4ee0-1915-8b5c423f8f3a": { 201 | "role": "editor", 202 | "value": { 203 | "id": "5ea0fa7c-00cd-4ee0-1915-8b5c423f8f3a", 204 | "version": 4, 205 | "name": [["Test Car Data"]], 206 | "schema": { 207 | "age": { 208 | "name": "age", 209 | "type": "text" 210 | }, 211 | "vin": { 212 | "name": "vin", 213 | "type": "text" 214 | }, 215 | "body": { 216 | "name": "body", 217 | "type": "text" 218 | }, 219 | "fuel": { 220 | "name": "fuel", 221 | "type": "text" 222 | }, 223 | "make": { 224 | "name": "make", 225 | "type": "text" 226 | }, 227 | "msrp": { 228 | "name": "msrp", 229 | "type": "text" 230 | }, 231 | "year": { 232 | "name": "year", 233 | "type": "text" 234 | }, 235 | "model": { 236 | "name": "model", 237 | "type": "text" 238 | }, 239 | "price": { 240 | "name": "price", 241 | "type": "text" 242 | }, 243 | "stock": { 244 | "name": "stock", 245 | "type": "text" 246 | }, 247 | "title": { 248 | "name": "dealerid", 249 | "type": "title" 250 | }, 251 | "colour": { 252 | "name": "colour", 253 | "type": "text" 254 | }, 255 | "engine": { 256 | "name": "engine", 257 | "type": "text" 258 | }, 259 | "photos": { 260 | "name": "photos", 261 | "type": "text" 262 | }, 263 | "series": { 264 | "name": "series", 265 | "type": "text" 266 | }, 267 | "newused": { 268 | "name": "newused", 269 | "type": "text" 270 | }, 271 | "city_mpg": { 272 | "name": "city_mpg", 273 | "type": "text" 274 | }, 275 | "features": { 276 | "name": "features", 277 | "type": "text" 278 | }, 279 | "odometer": { 280 | "name": "odometer", 281 | "type": "text" 282 | }, 283 | "certified": { 284 | "name": "certified", 285 | "type": "text" 286 | }, 287 | "door_count": { 288 | "name": "door_count", 289 | "type": "text" 290 | }, 291 | "model_code": { 292 | "name": "model_code", 293 | "type": "text" 294 | }, 295 | "dealer_city": { 296 | "name": "dealer_city", 297 | "type": "text" 298 | }, 299 | "dealer_name": { 300 | "name": "dealer_name", 301 | "type": "text" 302 | }, 303 | "description": { 304 | "name": "description", 305 | "type": "text" 306 | }, 307 | "highway_mpg": { 308 | "name": "highway_mpg", 309 | "type": "text" 310 | }, 311 | "photo_count": { 312 | "name": "photo_count", 313 | "type": "text" 314 | }, 315 | "transmission": { 316 | "name": "transmission", 317 | "type": "text" 318 | }, 319 | "dealer_region": { 320 | "name": "dealer_region", 321 | "type": "text" 322 | }, 323 | "series_detail": { 324 | "name": "series_detail", 325 | "type": "text" 326 | }, 327 | "dealer_address": { 328 | "name": "dealer_address", 329 | "type": "text" 330 | }, 331 | "interior_color": { 332 | "name": "interior_color", 333 | "type": "text" 334 | }, 335 | "inventory_date": { 336 | "name": "inventory_date", 337 | "type": "text" 338 | }, 339 | "drivetrain_desc": { 340 | "name": "drivetrain_desc", 341 | "type": "text" 342 | }, 343 | "dealer_postal_code": { 344 | "name": "dealer_postal_code", 345 | "type": "text" 346 | }, 347 | "engine_cylinder_ct": { 348 | "name": "engine_cylinder_ct", 349 | "type": "text" 350 | }, 351 | "engine_displacement": { 352 | "name": "engine_displacement", 353 | "type": "text" 354 | }, 355 | "photos_last_modified_date": { 356 | "name": "photos_last_modified_date", 357 | "type": "text" 358 | } 359 | }, 360 | "parent_id": "34d03794-ecdd-e42d-bb76-7e4aa77b6503", 361 | "parent_table": "block", 362 | "alive": true, 363 | "migrated": true 364 | } 365 | } 366 | }, 367 | "space": { 368 | "f687f7de-7f4c-4a86-b109-941a8dae92d2": { 369 | "role": "editor", 370 | "value": { 371 | "id": "f687f7de-7f4c-4a86-b109-941a8dae92d2", 372 | "version": 185, 373 | "name": "Dan's Workspace", 374 | "domain": "danmurphy", 375 | "permissions": [ 376 | { 377 | "role": "editor", 378 | "type": "user_permission", 379 | "user_id": "0c5f02f3-495d-4b73-b1c5-9f6fe03a8c26" 380 | } 381 | ], 382 | "beta_enabled": false, 383 | "pages": [ 384 | "66447bc8-17f0-44bc-81ed-3cf4802e9b00", 385 | "9268e71e-aaa8-4c54-a865-bd6e3d5472ce", 386 | "c7e1114a-f161-4b94-a7e7-704fd13d5545", 387 | "71728e4a-fc6d-41a8-abef-e6c00ac781fc", 388 | "6b5485c5-94b8-4e0b-a86f-abb455394087", 389 | "c335c2a9-7cb0-4847-9db8-a7f96e64db33", 390 | "861cdea1-ea57-47b9-a529-4c52569b8be8", 391 | "be0cda9c-054b-47e4-8937-bdc4ca2badc6", 392 | "7457ecd2-ba7e-43f1-a81b-f324586e0b38", 393 | "31fab05b-ded9-42ed-b4aa-0b8da0aa61bc", 394 | "cdc37dcc-d283-42a0-9cd9-863d36a7ae80", 395 | "abc7df36-0879-4db9-a3a2-cc780fc7ae01", 396 | "67af938e-4199-4bcd-b3b4-060d062271dd", 397 | "c040f0b8-d845-414a-aa52-3d7309b72d32", 398 | "7f5d96a9-a197-42ab-b5b8-7800a2c99840", 399 | "e3401dbc-5a89-4ab1-80ca-d1e0df475497", 400 | "00394898-7a6a-423f-a2ee-9be44b7300cc", 401 | "61981b4c-cc1c-4f67-a7d5-3ff6df03ad43", 402 | "67e187b1-d9fe-42fe-8ae3-e9532496b636" 403 | ], 404 | "disable_public_access": false, 405 | "disable_guests": false, 406 | "disable_move_to_space": false, 407 | "disable_export": false, 408 | "created_time": 1588292657021, 409 | "last_edited_time": 1606008660000, 410 | "created_by_table": "notion_user", 411 | "created_by_id": "0c5f02f3-495d-4b73-b1c5-9f6fe03a8c26", 412 | "last_edited_by_table": "notion_user", 413 | "last_edited_by_id": "0c5f02f3-495d-4b73-b1c5-9f6fe03a8c26", 414 | "shard_id": 955090, 415 | "plan_type": "personal", 416 | "invite_link_code": "df2e9c349d7e736dffbd69b5e84ef4860b4d8fe8", 417 | "invite_link_enabled": true 418 | } 419 | } 420 | } 421 | } 422 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require "notion_api" 2 | require_relative "spec_variables" 3 | 4 | # TODO: missing testing concepts: 5 | # 1. tables and other CVs with missing data (i.e. how do the methods perform when there is a null cell) 6 | # 2. tables and other CVs with entirely blank rows (i.e. how do the methods perform when there is an entirely null cell) 7 | # 3. CV page IDs different from regular Page IDs, so that should be fixed. 8 | 9 | RSpec.configure do |conf| 10 | conf.before(:example) do 11 | 12 | conf.include Helpers 13 | end 14 | end -------------------------------------------------------------------------------- /spec/spec_variables.rb: -------------------------------------------------------------------------------- 1 | module Helpers 2 | #! CONSTANTS 3 | $Client = NotionAPI::Client.new(ENV["token_v2"]) 4 | 5 | #! CORE_SPEC.RB HELPERS 6 | $Core_spec_url = "https://www.notion.so/danmurphy/CORE-RB-TESTS-9c50a7b39ad74f2baa08b3c95f1f19e7" 7 | # errant data... 8 | $Core_spec_invalid_url_one = "https://www.notion.so/danmurphy/CORE-RB-TESTS-9c50a7b39ad74f2baa08b3c95f1f19e71" # one too many chars 9 | $Core_spec_invalid_url_two = "9c50a7b39ad74f2baa08b3c95f1f19e" # one too few chars 10 | # ids of various types... 11 | $Core_spec_page_id_no_dashes = "9c50a7b39ad74f2baa08b3c95f1f19e7" 12 | $Core_spec_page_id = "9c50a7b3-9ad7-4f2b-aa08-b3c95f1f19e7" 13 | $Core_spec_page_parent_no_dashes = "66447bc817f044bc81ed3cf4802e9b00" 14 | # PageBlock instance... 15 | $Core_spec_page = $Client.get_page($Core_spec_url) 16 | # json data for the page... 17 | $Jsonified_core_page = JSON.parse(File.read("./spec/fixtures/notion_page_response.json")) 18 | $Jsonified_response_block_one = JSON.parse(File.read("./spec/fixtures/notion_block_one_response.json")) 19 | $Jsonified_response_block_two = JSON.parse(File.read("./spec/fixtures/notion_block_two_response.json")) 20 | $Jsonified_response_collection_one = JSON.parse(File.read("./spec/fixtures/notion_collection_view_one_response.json")) 21 | $Jsonified_response_collection_one = JSON.parse(File.read("./spec/fixtures/notion_collection_view_two_response.json")) 22 | # test collection_view block IDs... 23 | $Test_collection_block_id_one = "f1664a99-165b-49cc-811c-84f37655908a" 24 | $Test_collection_block_id_two = "34d03794-ecdd-e42d-bb76-7e4aa77b6503" 25 | # test collection_view collection IDs... 26 | $Test_collection_id_one = "a83a6cc0-ce7a-a14e-4483-4bfea6756922" 27 | $Test_collection_id_two = "5ea0fa7c-00cd-4ee0-1915-8b5c423f8f3a" 28 | # test block IDs 29 | $Test_core_block_id_one = "1f5ae85f-f89f-4779-9fa4-b30c3b229cdb" 30 | $Test_core_block_id_two = "d7407d2c-adbb-497c-b99f-9f9d426d2b18" 31 | $Body = { :pageId => $Core_spec_page_id, :chunkNumber => 0, :limit => 100, :verticalColumns => false } 32 | 33 | 34 | #! BLOCKS_SPEC.RB HELPERS 35 | $Block_spec_url = "https://www.notion.so/danmurphy/BLOCKS-RB-TESTS-b94758d4e7df4af8863fb90c11b35fff" 36 | $Block_spec_sub_url = "https://www.notion.so/danmurphy/sub-blocks-37f78a617d8b47988869368a18bcb791" 37 | $Block_spec_page_url = "https://www.notion.so/danmurphy/Page-for-move-dc6febdbefac4fbc8eed70232e20454e" 38 | $Block_spec_get_url = "https://www.notion.so/danmurphy/Page-for-gets-b8d4eb9b27634044863b544ff2d542e9" 39 | $Block_spec_create_url = "https://www.notion.so/danmurphy/Page-for-creates-4518d1fce157451f937827605dd1fbe4" 40 | $Block_spec_add_url = "https://www.notion.so/danmurphy/Page-for-add-rows-15fd6f2e99014f869ecc5b059a69da79" 41 | $Block_spec_page = $Client.get_page($Block_spec_url) 42 | $Block_spec_sub_page = $Client.get_page($Block_spec_sub_url) 43 | $Block_spec_move_page = $Client.get_page($Block_spec_page_url) 44 | $Block_spec_get_page = $Client.get_page($Block_spec_get_url) 45 | $Block_spec_create_page = $Client.get_page($Block_spec_create_url) 46 | $Block_spec_add_page = $Client.get_page($Block_spec_add_url) 47 | 48 | $Block_spec_title_id = "5e5f1ec2-8af4-429e-bcb2-bac6d6f49798" 49 | $Block_spec_convert_id = "60526527-da05-4b58-99f0-6e1d18d60dd4" 50 | $Block_spec_duplicate_id_one = "b240e279-df40-4d36-b3b1-dbc419abbcd8" 51 | $Block_spec_duplicate_id_target = "b61f8da8-7bf4-4149-891a-5a043d83fc46" 52 | $Block_spec_move_id_target = "45cd1ef1-7130-427e-99e2-f49ba558222a" 53 | $Block_spec_get_id = "d2580628-2e93-4a0c-a23a-1efb116b4a99" 54 | $Block_spec_get_collection_id = "3224c94f-e660-4092-9dba-d26b69b68d40" 55 | $Block_spec_add_row_id = "eaa144ab-d91b-4640-b13a-a0ba1c8dd450" 56 | $Vehicle_data_csv = File.read("./spec/fixtures/vauto_inventory.csv") 57 | $Full_page_cv_url = "https://www.notion.so/danmurphy/3a8c0397841748b08da76f188b7fb381?v=a68811a8c18b433b9787b17dcd83527e" 58 | $Full_page_cv = $Client.get_page($Full_page_cv_url) 59 | $Json = JSON.parse(File.read("./spec/fixtures/emoji_data.json")) 60 | end -------------------------------------------------------------------------------- /spec/utils_spec.rb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danmurphy1217/notion-ruby/4698b79ab6359dc63be1e44743cf7cc6fdedd05d/spec/utils_spec.rb --------------------------------------------------------------------------------