Building on the previous two Notion tutorials, you have learned how to create a Notion integration token and retrieve, update, and delete Notion blocks via PowerShell and the Notion API.
Even more powerful are Notion’s Databases. Relational databases help to organize data logically and provide opportunities for analysis. With Notion, store pages within Databases with additional properties. In this tutorial, learn how to create a page, database, and database item. Build upon the Notion PowerShell module to continue expanding functionality.
Prerequisites
To follow along in this tutorial, you only need a Notion account and PowerShell; here, PowerShell v7.3.7 is in use.
The Power of Notion Databases
Though you may think of pages as standalone, within Notion, they are also integral to Databases. Therefore, combining the Page operations with those of Databases helps to understand the linkage. In Notion, Databases may have properties of many different types, such as Selects, Checkboxes, or Numbers.
Creating a Page
First, you need a place to put the database. To demonstrate this, create a new page using the /pages
endpoint. The resulting id
will be the GUID used as the parent_id
of the new Database.
$APIKey = 'secret_sK5Qir0UAgkLF1iauP5iMsq1vq3YW5S9o5GvF7M8PjK'
$APIURI = 'https://api.notion.com/v1'
$APIVersion = '2022-06-28'
$GUID = 'a2b3646d-e941-4df4-874d-56153139b618'
$Params = @{
"Headers" = @{
"Authorization" = "Bearer {0}" -F $APIKey
"Content-type" = "application/json"
"Notion-Version" = "{0}" -F $APIVersion
}
"Method" = 'POST'
"URI" = ("{0}/pages" -F $APIURI, [GUID]::new($GUID))
"Body" = @{
"parent" = @{
"type" = 'page_id'
"page_id" = $GUID
}
"properties" = @{
"title" = @(
@{
"text" = @{"content" = "Test" }
}
)
}
"children" = @()
} | ConvertTo-JSON -Depth 100
}
$Result = Invoke-RestMethod @Params
Creating the Database
With the page newly created, you will use the page_id
parent with the /databases
endpoint to create a new Database with a single required Title property. There is a required title
property that gives the Database its unique name as well.
$Params = @{
"Headers" = @{
"Authorization" = "Bearer {0}" -F $APIKey
"Content-type" = "application/json"
"Notion-Version" = "{0}" -F $APIVersion
}
"Method" = 'POST'
"URI" = ("{0}/databases" -F $APIURI)
"Body" = @{
"parent" = @{
"type" = "page_id"
"page_id" = $Result.id
}
"title" = @(
@{
"type" = "text"
"text" = @{
"content" = "Test"
"link" = $null
}
}
)
"properties" = @{
"Name" = @{
"type" = "title"
"title" = @{}
}
"Checkbox" = @{
"type" = "checkbox"
"checkbox" = @{}
}
"Number" = @{
"type" = "number"
"number" = @{
"format" = "number_with_commas"
}
}
}
} | ConvertTo-Json -Depth 100
}
$Result = Invoke-RestMethod @Params
Creating a Page in the Database
Finally, to store data within the Database, you must create a new item: a full Notion page. As the page item exists in the database, it has the added benefit of any additional properties. In the example above, you created a checkbox and number property.
$Params = @{
"Headers" = @{
"Authorization" = "Bearer {0}" -F $APIKey
"Content-type" = "application/json"
"Notion-Version" = "{0}" -F $APIVersion
}
"Method" = 'POST'
"URI" = ("{0}/pages" -F $APIURI)
"Body" = @{
"parent" = @{
"type" = "database_id"
"database_id" = $Result.id
}
"properties" = @{
"Name" = @{
"title" = @(
@{
"text" = @{"content" = "This is a Test Task"}
}
)
}
"Checkbox" = @{
"checkbox" = $True
}
"Number" = @{
"number" = 1000
}
}
} | ConvertTo-Json -Depth 100
}
$Result = Invoke-RestMethod @Params
Adding to the Module
To round out the module, three new functions will be created. These functions will be:
New-NotionPage
New-NotionDatabase
You may wonder why there isn’t a New-NotionDatabase
. As technically you are calling the same endpoint, /pages
, for both Databases and Pages, you can extend your New-NotionPage
function to cover both use cases.
Introduced in this function is the concept of parameter sets. Parameter sets offer a way to separate parameters into logical groupings and only allow those that belong together. Defined below is the DefaultParameterSetName
within the CmdletBinding
declaration. This gives a default for the function.
It’s necessary to define all the parameter sets each parameter belongs in. If a parameter must be in both, then multiple declarations must be used.
New-NotionPage
Function New-NotionPage {
[CmdletBinding(SupportsShouldProcess = $True, DefaultParameterSetName = 'Page')]
Param(
[String]$APIKey,
[String]$APIVersion,
[ValidateScript( { [System.URI]::IsWellFormedUriString( $_ ,[System.UriKind]::Absolute ) } )][String]$APIURI,
[Parameter(ValueFromPipelineByPropertyName = $True)]
[Alias("ID")]
[ValidateScript( { Try { If ( [GUID]::Parse( $_ ) ) { $True } } Catch { $False } } )][String]$GUID,
[Parameter(Mandatory, ParameterSetName = 'Page')]
$Title,
$Content,
[Parameter(ParameterSetName = 'Database')]
$Properties,
[Parameter(ParameterSetName = 'Database')]
[Switch]$Database
)
Process {
$Body = @{
"parent" = $Null
"properties" = $Null
}
If ($Database) {
$Body.properties = $Properties
$Body.parent = @{
"type" = 'database_id'
"database_id" = [GUID]::New($GUID)
}
} Else {
$Body.parent = @{
"type" = 'page_id'
"page_id" = [GUID]::New($GUID)
}
$Body.properties = @{
"title" = @(
@{
"text" = @{ "content" = $Title }
}
)
}
}
If ($Content) {
$Body.Add("children", $content)
}
Write-Verbose ($Body | Out-String)
$Params = @{
"Headers" = @{
"Authorization" = "Bearer {0}" -F $APIKey
"Content-type" = "application/json"
"Notion-Version" = "{0}" -F $APIVersion
}
"Method" = 'POST'
"URI" = ("{0}/pages" -F $APIURI)
"Body" = $Body | ConvertTo-JSON -Depth 100
}
Write-Verbose "[Process] Params: $($Params | Out-String)"
If ($PSCmdlet.ShouldProcess($GUID,"Adding Page")) {
Try {
$Result = Invoke-RestMethod @Params -ErrorAction 'Stop'
} Catch {
$Message = ($Error[0].ErrorDetails.Message | ConvertFrom-JSON).message
Write-Error "Command Failed to Run: $Message"
}
If ($Result) {
$Result
}
}
}
}
New-NotionDatabase
Function New-NotionDatabase {
[CmdletBinding(SupportsShouldProcess = $True)]
Param(
[String]$APIKey,
[String]$APIVersion,
[ValidateScript( { [System.URI]::IsWellFormedUriString( $_ ,[System.UriKind]::Absolute ) } )][String]$APIURI,
[Parameter(ValueFromPipelineByPropertyName = $True)]
[Alias("ID")]
[ValidateScript( { Try { If ( [GUID]::Parse( $_ ) ) { $True } } Catch { $False } } )][String]$GUID,
[Parameter(Mandatory)]
$Title,
$Properties
)
Process {
$Params = @{
"Headers" = @{
"Authorization" = "Bearer {0}" -F $APIKey
"Content-type" = "application/json"
"Notion-Version" = "{0}" -F $APIVersion
}
"Method" = 'POST'
"URI" = ("{0}/databases" -F $APIURI)
"Body" = @{
"parent" = @{
"type" = 'page_id'
"page_id" = [GUID]::New($GUID)
}
"title" = @(
@{
"type" = "text"
"text" = @{
"content" = $Title
"link" = $Null
}
}
)
"properties" = $Properties
} | ConvertTo-JSON -Depth 100
}
Write-Verbose "[Process] Params: $($Params | Out-String)"
If ($PSCmdlet.ShouldProcess($GUID,"Adding Database")) {
Try {
$Result = Invoke-RestMethod @Params -ErrorAction 'Stop'
} Catch {
$Message = ($Error[0].ErrorDetails.Message | ConvertFrom-JSON).message
Write-Error "Command Failed to Run: $Message"
}
If ($Result) {
$Result
}
}
}
}
When run together, you can see the results are the same as if you were to run the individual API calls, but in a more user-friendly nomenclature. By passing in the resulting objects from the previous calls you can simplify creation of child pages and objects.
$Parent = 'a2b3646de9414df4874d56153139b618'
$Result = New-NotionPage -GUID $Parent -Title "Database Page"
$Database = New-NotionDatabase -GUID $Result.id -Title "Test Database" -Properties @{
"Name" = @{
"type" = "title"
"title" = @{}
}
"Checkbox" = @{
"type" = "checkbox"
"checkbox" = @{}
}
"Number" = @{
"type" = "number"
"number" = @{
"format" = "number_with_commas"
}
}
}
$Item = New-NotionPage -Database -GUID $Database.id -Properties @{
"Name" = @{
"title" = @(
@{
"text" = @{"content" = "This is a Test Task"}
}
)
}
"Checkbox" = @{
"checkbox" = $True
}
"Number" = @{
"number" = 1000
}
}
Next Steps
The next step is to add a few supporting functions to the module to create stronger typing of available block types and additional usability. The following few articles will wrap up the function and finish creating a usable and full-featured module!