makenotion / notion-sdk-js

Official Notion JavaScript Client
https://developers.notion.com/docs
MIT License
4.94k stars 589 forks source link

Question about new typings #219

Closed JClackett closed 2 years ago

JClackett commented 3 years ago

I see that the individual block types have been removed and replaced with a large generic union on the responses of some queries.

for example (have removed some properties for clarity):

ListBlockChildrenResponse["results"] = Array<
           {
              type: "paragraph";
              ...stuff
             } | { 
              type: "heading_1";
             ...stuff

... and so on

So a Block I guess can now be represented by:

export type Block = ListBlockChildrenResponse["results"][0]

The same goes for the Database type

export type Database = QueryDatabaseResponse["results"][0]

But when I have a database that has properties on it like "Name" or "Slug", how do I correctly extend the properties type so that its possible to access it without TS complaining, before I could do something like:

const name = (database.properties.Name as TitlePropertyValue).title[0].plain_text

But now that the TitlePropertyValue type doesn't exist, not really sure to handle the custom properties?

Any ideas?

Thanks

JClackett commented 3 years ago

Also, I used to have a React component that could render out the RichTextBlock like below:

import * as React from "react"
import { Text, Link, StyleProps } from "@chakra-ui/react"
import type { Annotations, RichText } from "@notionhq/client/build/src/api-types"

interface RichTextBlockProps {
  text: RichText[]
}

export function RichTextBlock(props: RichTextBlockProps) {
  const textDecorationAttr = React.useCallback((annotations: Annotations): Omit<StyleProps, "apply"> => {
    return {
      fontWeight: annotations.bold ? 600 : undefined,
      textDecor: `${annotations.underline ? "underline " : ""}${
        annotations.strikethrough ? "line-through" : ""
      }`,
      fontStyle: annotations.italic ? "italic" : undefined,
    }
  }, [])

  if (props.text.length === 0) return <br />
  return (
    <>
      {props.text.map((text, i) => {
        if (text.href) {
          return (
            <Link
              key={i}
              href={text.href}
              isExternal
              color="gray.600"
              _hover={{ color: "gray.900" }}
              {...textDecorationAttr(text.annotations)}
              textDecor="underline"
            >
              {text.plain_text}
            </Link>
          )
        } else {
          return (
            <Text as="span" key={i} {...textDecorationAttr(text.annotations)}>
              {text.plain_text}
            </Text>
          )
        }
      })}
    </>
  )
}

Which was super nice as I could just import the "RichText" type, now it seems I have to manually copy it from your types file:

type RichText = {
  type: "text"
  text: {
    content: string
    link: {
      url: string
    } | null
  }
  annotations: {
    bold: boolean
    italic: boolean
    strikethrough: boolean
    underline: boolean
    code: boolean
    color:
      | "default"
      | "gray"
      | "brown"
      | "orange"
      | "yellow"
      | "green"
      | "blue"
      | "purple"
      | "pink"
      | "red"
      | "gray_background"
      | "brown_background"
      | "orange_background"
      | "yellow_background"
      | "green_background"
      | "blue_background"
      | "purple_background"
      | "pink_background"
      | "red_background"
  }
  plain_text: string
  href: string | null
}
nartc commented 3 years ago

You can extract more to grt what you need. Check this out to see how I do it: https://github.com/nartc/notion-stuff/blob/6e22fbffafb528e41cc31258c34b5234048ebb25/libs/v4-types/src/lib/types.ts#L6

peetjvv commented 3 years ago

I'm also finding the removal/change of these types frustrating. Was really nice to just be able to import them directly.

ghost commented 3 years ago

This made the client much much harder to work with when using typescript. Also the type hints that you get from visual code etc is more or less unreadable :/

alfdahlman commented 3 years ago

Also finding this frustrating :(

bitabs commented 3 years ago

I was actually trying to build their types from scratch for them and raise a PR, but its extremely long

JClackett commented 3 years ago

I'm assuming they auto generated them?

rhart92 commented 3 years ago

Apologies for the inconvenience of the new types. In an effort to provide a more accurate and up to date set of types we started auto-generating the types directly from our codebase but in the process we have made the types less ergonomic to work with. In the meantime, using the strategy suggested by @nartc should work to get access to specific block types as shown below:

import { GetBlockResponse } from './api-endpoints'

type HeadingOneBlockResponse = Extract<GetBlockResponse, { type: "heading_1"}>
type ImageBlockResponse = Extract<GetBlockResponse, { type: "image"}>

Note: The types will be slightly different for each endpoint request/responses. For example all responses will have type as required since it's alway returned, but it's optional for all requests. Another example difference is when appending blocks, you're able to add children, but when updating blocks, you can't.

toolness commented 3 years ago

Thanks @rhart92! I think this workaround is nice.

However, I wanted to note that a major problem I'm having with the latest typings is that they appear to be so large that my VSCode refuses to allow me to introspect them in the way that I'm used to.

For example, here's a snippet of code that I'd like to explore:

import notion from "@notionhq/client";

const client = new notion.Client({});

const stuff = await client.databases.query({
    database_id: "fake"
});

console.log(stuff);

Version 0.3.3

Using VSCode with version 0.3.3 of @notionhq/client, hovering my mouse over stuff reveals that it is of type DatabasesQueryResponse:

image

Furthermore, if I right-click on the variable and choose "Go to Type Definition", I am taken to the data type's definition in api-endpoints.d.ts, which leads me to more types which I can easily explore by right-clicking on them and choosing "Go to Definition". This is a great learning tool for me and it's how I explore many libraries.

It also looks like version 0.3.3 has reasonably-sized typings:

$ du -c -h node_modules/@notionhq/client/build/src/*.d.ts

8.0K    node_modules/@notionhq/client/build/src/Client.d.ts
 16K    node_modules/@notionhq/client/build/src/api-endpoints.d.ts
 28K    node_modules/@notionhq/client/build/src/api-types.d.ts
4.0K    node_modules/@notionhq/client/build/src/errors.d.ts
4.0K    node_modules/@notionhq/client/build/src/fetch-types.d.ts
4.0K    node_modules/@notionhq/client/build/src/helpers.d.ts
4.0K    node_modules/@notionhq/client/build/src/index.d.ts
4.0K    node_modules/@notionhq/client/build/src/logging.d.ts
4.0K    node_modules/@notionhq/client/build/src/type-utils.d.ts
 76K    total

Version 0.4.0

Now let's fast-forward to version 0.4.0 of the same library. Now right-clicking on stuff and choosing "Go to Type Definition" still works, but it takes a few seconds for VSCode to show me the file.

I think this is because the typings are much larger in 0.4.0:

$ du -c -h node_modules/@notionhq/client/build/src/*.d.ts

8.0K    node_modules/@notionhq/client/build/src/Client.d.ts
868K    node_modules/@notionhq/client/build/src/api-endpoints.d.ts
4.0K    node_modules/@notionhq/client/build/src/errors.d.ts
4.0K    node_modules/@notionhq/client/build/src/fetch-types.d.ts
4.0K    node_modules/@notionhq/client/build/src/helpers.d.ts
4.0K    node_modules/@notionhq/client/build/src/index.d.ts
4.0K    node_modules/@notionhq/client/build/src/logging.d.ts
4.0K    node_modules/@notionhq/client/build/src/type-utils.d.ts
900K    total

Version 0.4.6

Now let's go to the very latest version of the library, 0.4.6. At this point, hovering my mouse over stuff shows the tooltip (loading...) const stuff: any for several seconds before it resolves to its actual type. But what's really unfortunate is that right-clicking on the symbol and choosing "Go to Type Definition" doesn't work at all:

image

I suspect this is because the typings have now become even larger:

$ du -c -h node_modules/@notionhq/client/build/src/*.d.ts

8.0K    node_modules/@notionhq/client/build/src/Client.d.ts
6.6M    node_modules/@notionhq/client/build/src/api-endpoints.d.ts
4.0K    node_modules/@notionhq/client/build/src/errors.d.ts
4.0K    node_modules/@notionhq/client/build/src/fetch-types.d.ts
4.0K    node_modules/@notionhq/client/build/src/helpers.d.ts
4.0K    node_modules/@notionhq/client/build/src/index.d.ts
4.0K    node_modules/@notionhq/client/build/src/logging.d.ts
4.0K    node_modules/@notionhq/client/build/src/type-utils.d.ts
6.6M    total

My guess is that TypeScript is seeing that api-endpoints.d.ts is 6.6 megabytes and simply refusing to take me there, but I'm not sure. I'm also guessing that these huge typings have an effect on the performance of the TypeScript compiler, both in its speed and memory use, but I haven't done any work to verify that.

In any case, it would be great if the typings could be nice and small again, as it would likely bring back the great developer ergonomics that version 0.3.3 had.

toolness commented 2 years ago

Good news--it looks like the latest release, 0.4.8, has slimmed down the api-endpoints.d.ts file to 2.2 megabytes (in particular, an EmojiRequest type is now declared once and reused in many places, instead of being inlined everywhere). This makes my VSCode show me type definitions once more, which is great! Though further slimming of the SDK would be quite appreciated for the reasons mentioned in my previous comment.

loicortola commented 2 years ago

Hi @rhart92 ,

thanks for the work on the client. I see some comments about the fact that the code-generation trade-offs kind of kills DX in Typescript, just wanted to know if there was a roadmap to improve that in the future? (To implement better composition and more modularity rather than just exhaustive structure)?

Thank you for your clarifications :)

Niceplace commented 2 years ago

@rhart92 First of all, I want to say that I absolutely LOVE being able to use the notion API, let alone having an SDK for it. Thank you for your (and your colleague's) hard work on this, I'm sure it's no easy task !

I want to explain my understanding of my current problem with a simple example first and then explain my use case. I tried to see how I can apply your workaround with Extract<> but I'm not good enough with Typescript yet to understand how to do that with an Array of Union types. I searched as much as I could but I'm struggling here.

I'm using the SDK @ version 0.4.12 with typescript 4.5.4

Typescript Example

I seem to notice a fundamental problem with the way some types seem to be generated, especially with how Union types are designed within Typescript. It took me a while to get to the root of the problem and I learned something new so there's that :D

From the Handbook

TypeScript will only allow an operation if it is valid for every member of the union. For example, if you have the union string | number, you can’t use methods that are only available on string:

This constraint is better explained with the example below

type GetAllPeople = () => { results: Array<{ name: string, age: number} | { age: number}>}
type GetAllPeopleReturnType = ReturnType<GetAllPeople>['results']

const someArray: GetAllPeople = [{ name: 'Simon', age: 30 }, [{ age: 22}]
 // Will autocomplete and TS is happy
const allAges = someArray.map(element => element.age)
// Will not autocomplete and TS will complain because the property 'name' is not applicable 
//    to every member of Array<{ name: string, age: number} | { age: number}>
const allNames = someArray.map(element => element.name) 

Use Case with QueryDatabaseResponse

I am using the QueryDatabaseResponse type is exported in auto-generated types in: @notionhq/client/build/src/api-endpoints I build a little helper function to help me handle pagination automatically and I only need to feed it the parameters for the query function.

The return value's type is QueryDatabaseResponse['results'] which I am extracting from the return type of databases.query -> Awaited<ReturnType<typeof query>>['results'].

/**
 * Query all pages and return all records from a Notion database object
 *  Will log a warning if database has no records
 * @param parameters To specify database id, control sorting, filtering and pagination, directly using Notion's sdk types
 * @returns A list of all the results from the database or an empty array
 */
export const queryAll = async (
  parameters: QueryDatabaseParameters,
) => {
  const params: typeof parameters = { ...parameters };
  // getNotionClient() returns an authenticated instance of the notion SDK 
  const query = getNotionClient().databases.query;
  const allResults: Awaited<ReturnType<typeof query>>['results'] = [];

  let hasNextPage = true;
  while (hasNextPage) {
    const currentPage = await query(params);
    allResults.push(...currentPage.results);
    hasNextPage = currentPage.has_more;
    params.start_cursor = currentPage.next_cursor || undefined; // next_cursor is string | null while params.start_cursor is string | undefined -.-
  }
  if (!allResults.length) {
    logger.warn(`No results found in database ${params.database_id}`);
  }
  return allResults;
};

Unfortunately, it seems that the type of allResults is stuck to { id: string; object: 'page'}[] while the generated type for a database response (included in an expandable toggle right below) seem to indicate that the results property can be a lot of things. I'm unsure how to pick the right one, I would rather have the SDK infer it correctly based on the responses' contents but I don't know if that's even doable given the complexity and possible permutations of objects we can have in a Database.

Thoughts ? :/

CLICK TO EXPAND -Generated type definition for QueryDatabaseResponse ```typescript export declare type QueryDatabaseResponse = { type: "rollup"; rollup: { type: "number"; number: number | null; function: "count" | "count_values" | "empty" | "not_empty" | "unique" | "show_unique" | "percent_empty" | "percent_not_empty" | "sum" | "average" | "median" | "min" | "max" | "range" | "earliest_date" | "latest_date" | "date_range" | "checked" | "unchecked" | "percent_checked" | "percent_unchecked" | "show_original"; } | { type: "date"; date: { start: string; end: string | null; time_zone: TimeZoneRequest | null; } | null; function: "count" | "count_values" | "empty" | "not_empty" | "unique" | "show_unique" | "percent_empty" | "percent_not_empty" | "sum" | "average" | "median" | "min" | "max" | "range" | "earliest_date" | "latest_date" | "date_range" | "checked" | "unchecked" | "percent_checked" | "percent_unchecked" | "show_original"; } | { type: "array"; array: Array<{ type: "title"; title: Array<{ type: "text"; text: { content: string; link: { url: TextRequest; } | null; }; annotations: { bold: boolean; italic: boolean; strikethrough: boolean; underline: boolean; code: boolean; color: "default" | "gray" | "brown" | "orange" | "yellow" | "green" | "blue" | "purple" | "pink" | "red" | "gray_background" | "brown_background" | "orange_background" | "yellow_background" | "green_background" | "blue_background" | "purple_background" | "pink_background" | "red_background"; }; plain_text: string; href: string | null; } | { type: "mention"; mention: { type: "user"; user: { id: IdRequest; object: "user"; } | { type: "person"; person: { email?: string; }; name: string | null; avatar_url: string | null; id: IdRequest; object: "user"; } | { type: "bot"; bot: EmptyObject | { owner: { type: "user"; user: { type: "person"; person: { email: string; }; name: string | null; avatar_url: string | null; id: IdRequest; object: "user"; } | { id: IdRequest; object: "user"; }; } | { type: "workspace"; workspace: true; }; }; name: string | null; avatar_url: string | null; id: IdRequest; object: "user"; }; } | { type: "date"; date: { start: string; end: string | null; time_zone: TimeZoneRequest | null; }; } | { type: "link_preview"; link_preview: { url: TextRequest; }; } | { type: "page"; page: { id: IdRequest; }; } | { type: "database"; database: { id: IdRequest; }; }; annotations: { bold: boolean; italic: boolean; strikethrough: boolean; underline: boolean; code: boolean; color: "default" | "gray" | "brown" | "orange" | "yellow" | "green" | "blue" | "purple" | "pink" | "red" | "gray_background" | "brown_background" | "orange_background" | "yellow_background" | "green_background" | "blue_background" | "purple_background" | "pink_background" | "red_background"; }; plain_text: string; href: string | null; } | { type: "equation"; equation: { expression: TextRequest; }; annotations: { bold: boolean; italic: boolean; strikethrough: boolean; underline: boolean; code: boolean; color: "default" | "gray" | "brown" | "orange" | "yellow" | "green" | "blue" | "purple" | "pink" | "red" | "gray_background" | "brown_background" | "orange_background" | "yellow_background" | "green_background" | "blue_background" | "purple_background" | "pink_background" | "red_background"; }; plain_text: string; href: string | null; }>; } | { type: "rich_text"; rich_text: Array<{ type: "text"; text: { content: string; link: { url: TextRequest; } | null; }; annotations: { bold: boolean; italic: boolean; strikethrough: boolean; underline: boolean; code: boolean; color: "default" | "gray" | "brown" | "orange" | "yellow" | "green" | "blue" | "purple" | "pink" | "red" | "gray_background" | "brown_background" | "orange_background" | "yellow_background" | "green_background" | "blue_background" | "purple_background" | "pink_background" | "red_background"; }; plain_text: string; href: string | null; } | { type: "mention"; mention: { type: "user"; user: { id: IdRequest; object: "user"; } | { type: "person"; person: { email?: string; }; name: string | null; avatar_url: string | null; id: IdRequest; object: "user"; } | { type: "bot"; bot: EmptyObject | { owner: { type: "user"; user: { type: "person"; person: { email: string; }; name: string | null; avatar_url: string | null; id: IdRequest; object: "user"; } | { id: IdRequest; object: "user"; }; } | { type: "workspace"; workspace: true; }; }; name: string | null; avatar_url: string | null; id: IdRequest; object: "user"; }; } | { type: "date"; date: { start: string; end: string | null; time_zone: TimeZoneRequest | null; }; } | { type: "link_preview"; link_preview: { url: TextRequest; }; } | { type: "page"; page: { id: IdRequest; }; } | { type: "database"; database: { id: IdRequest; }; }; annotations: { bold: boolean; italic: boolean; strikethrough: boolean; underline: boolean; code: boolean; color: "default" | "gray" | "brown" | "orange" | "yellow" | "green" | "blue" | "purple" | "pink" | "red" | "gray_background" | "brown_background" | "orange_background" | "yellow_background" | "green_background" | "blue_background" | "purple_background" | "pink_background" | "red_background"; }; plain_text: string; href: string | null; } | { type: "equation"; equation: { expression: TextRequest; }; annotations: { bold: boolean; italic: boolean; strikethrough: boolean; underline: boolean; code: boolean; color: "default" | "gray" | "brown" | "orange" | "yellow" | "green" | "blue" | "purple" | "pink" | "red" | "gray_background" | "brown_background" | "orange_background" | "yellow_background" | "green_background" | "blue_background" | "purple_background" | "pink_background" | "red_background"; }; plain_text: string; href: string | null; }>; } | { type: "number"; number: number | null; } | { type: "url"; url: string | null; } | { type: "select"; select: { id: StringRequest; name: StringRequest; color: "default" | "gray" | "brown" | "orange" | "yellow" | "green" | "blue" | "purple" | "pink" | "red"; } | null; } | { type: "multi_select"; multi_select: Array<{ id: StringRequest; name: StringRequest; color: "default" | "gray" | "brown" | "orange" | "yellow" | "green" | "blue" | "purple" | "pink" | "red"; }>; } | { type: "people"; people: Array<{ id: IdRequest; object: "user"; } | { type: "person"; person: { email?: string; }; name: string | null; avatar_url: string | null; id: IdRequest; object: "user"; } | { type: "bot"; bot: EmptyObject | { owner: { type: "user"; user: { type: "person"; person: { email: string; }; name: string | null; avatar_url: string | null; id: IdRequest; object: "user"; } | { id: IdRequest; object: "user"; }; } | { type: "workspace"; workspace: true; }; }; name: string | null; avatar_url: string | null; id: IdRequest; object: "user"; }>; } | { type: "email"; email: string | null; } | { type: "phone_number"; phone_number: string | null; } | { type: "date"; date: { start: string; end: string | null; time_zone: TimeZoneRequest | null; } | null; } | { type: "files"; files: Array<{ file: { url: string; expiry_time: string; }; name: StringRequest; type?: "file"; } | { external: { url: TextRequest; }; name: StringRequest; type?: "external"; }>; } | { type: "checkbox"; checkbox: boolean; } | { type: "formula"; formula: { type: "string"; string: string | null; } | { type: "date"; date: { start: string; end: string | null; time_zone: TimeZoneRequest | null; } | null; } | { type: "number"; number: number | null; } | { type: "boolean"; boolean: boolean | null; }; } | { type: "relation"; relation: Array<{ id: string; }>; } | { type: "created_time"; created_time: string; } | { type: "created_by"; created_by: { id: IdRequest; object: "user"; } | { type: "person"; person: { email?: string; }; name: string | null; avatar_url: string | null; id: IdRequest; object: "user"; } | { type: "bot"; bot: EmptyObject | { owner: { type: "user"; user: { type: "person"; person: { email: string; }; name: string | null; avatar_url: string | null; id: IdRequest; object: "user"; } | { id: IdRequest; object: "user"; }; } | { type: "workspace"; workspace: true; }; }; name: string | null; avatar_url: string | null; id: IdRequest; object: "user"; }; } | { type: "last_edited_time"; last_edited_time: string; } | { type: "last_edited_by"; last_edited_by: { id: IdRequest; object: "user"; } | { type: "person"; person: { email?: string; }; name: string | null; avatar_url: string | null; id: IdRequest; object: "user"; } | { type: "bot"; bot: EmptyObject | { owner: { type: "user"; user: { type: "person"; person: { email: string; }; name: string | null; avatar_url: string | null; id: IdRequest; object: "user"; } | { id: IdRequest; object: "user"; }; } | { type: "workspace"; workspace: true; }; }; name: string | null; avatar_url: string | null; id: IdRequest; object: "user"; }; }>; function: "count" | "count_values" | "empty" | "not_empty" | "unique" | "show_unique" | "percent_empty" | "percent_not_empty" | "sum" | "average" | "median" | "min" | "max" | "range" | "earliest_date" | "latest_date" | "date_range" | "checked" | "unchecked" | "percent_checked" | "percent_unchecked" | "show_original"; } | { type: "unsupported"; unsupported: EmptyObject; function: "count" | "count_values" | "empty" | "not_empty" | "unique" | "show_unique" | "percent_empty" | "percent_not_empty" | "sum" | "average" | "median" | "min" | "max" | "range" | "earliest_date" | "latest_date" | "date_range" | "checked" | "unchecked" | "percent_checked" | "percent_unchecked" | "show_original"; }; object: "list"; results: Array<{ parent: { type: "database_id"; database_id: IdRequest; } | { type: "page_id"; page_id: IdRequest; } | { type: "workspace"; workspace: true; }; properties: Record; id: string; } | { type: "rich_text"; rich_text: Array<{ type: "text"; text: { content: string; link: { url: TextRequest; } | null; }; annotations: { bold: boolean; italic: boolean; strikethrough: boolean; underline: boolean; code: boolean; color: "default" | "gray" | "brown" | "orange" | "yellow" | "green" | "blue" | "purple" | "pink" | "red" | "gray_background" | "brown_background" | "orange_background" | "yellow_background" | "green_background" | "blue_background" | "purple_background" | "pink_background" | "red_background"; }; plain_text: string; href: string | null; } | { type: "mention"; mention: { type: "user"; user: { id: IdRequest; object: "user"; } | { type: "person"; person: { email?: string; }; name: string | null; avatar_url: string | null; id: IdRequest; object: "user"; } | { type: "bot"; bot: EmptyObject | { owner: { type: "user"; user: { type: "person"; person: { email: string; }; name: string | null; avatar_url: string | null; id: IdRequest; object: "user"; } | { id: IdRequest; object: "user"; }; } | { type: "workspace"; workspace: true; }; }; name: string | null; avatar_url: string | null; id: IdRequest; object: "user"; }; } | { type: "date"; date: { start: string; end: string | null; time_zone: TimeZoneRequest | null; }; } | { type: "link_preview"; link_preview: { url: TextRequest; }; } | { type: "page"; page: { id: IdRequest; }; } | { type: "database"; database: { id: IdRequest; }; }; annotations: { bold: boolean; italic: boolean; strikethrough: boolean; underline: boolean; code: boolean; color: "default" | "gray" | "brown" | "orange" | "yellow" | "green" | "blue" | "purple" | "pink" | "red" | "gray_background" | "brown_background" | "orange_background" | "yellow_background" | "green_background" | "blue_background" | "purple_background" | "pink_background" | "red_background"; }; plain_text: string; href: string | null; } | { type: "equation"; equation: { expression: TextRequest; }; annotations: { bold: boolean; italic: boolean; strikethrough: boolean; underline: boolean; code: boolean; color: "default" | "gray" | "brown" | "orange" | "yellow" | "green" | "blue" | "purple" | "pink" | "red" | "gray_background" | "brown_background" | "orange_background" | "yellow_background" | "green_background" | "blue_background" | "purple_background" | "pink_background" | "red_background"; }; plain_text: string; href: string | null; }>; id: string; } | { type: "number"; number: number | null; id: string; } | { type: "url"; url: string | null; id: string; } | { type: "select"; select: { id: StringRequest; name: StringRequest; color: "default" | "gray" | "brown" | "orange" | "yellow" | "green" | "blue" | "purple" | "pink" | "red"; } | null; id: string; } | { type: "multi_select"; multi_select: Array<{ id: StringRequest; name: StringRequest; color: "default" | "gray" | "brown" | "orange" | "yellow" | "green" | "blue" | "purple" | "pink" | "red"; }>; id: string; } | { type: "people"; people: Array<{ id: IdRequest; object: "user"; } | { type: "person"; person: { email?: string; }; name: string | null; avatar_url: string | null; id: IdRequest; object: "user"; } | { type: "bot"; bot: EmptyObject | { owner: { type: "user"; user: { type: "person"; person: { email: string; }; name: string | null; avatar_url: string | null; id: IdRequest; object: "user"; } | { id: IdRequest; object: "user"; }; } | { type: "workspace"; workspace: true; }; }; name: string | null; avatar_url: string | null; id: IdRequest; object: "user"; }>; id: string; } | { type: "email"; email: string | null; id: string; } | { type: "phone_number"; phone_number: string | null; id: string; } | { type: "date"; date: { start: string; end: string | null; time_zone: TimeZoneRequest | null; } | null; id: string; } | { type: "files"; files: Array<{ file: { url: string; expiry_time: string; }; name: StringRequest; type?: "file"; } | { external: { url: TextRequest; }; name: StringRequest; type?: "external"; }>; id: string; } | { type: "checkbox"; checkbox: boolean; id: string; } | { type: "formula"; formula: { type: "string"; string: string | null; } | { type: "date"; date: { start: string; end: string | null; time_zone: TimeZoneRequest | null; } | null; } | { type: "number"; number: number | null; } | { type: "boolean"; boolean: boolean | null; }; id: string; } | { type: "relation"; relation: Array<{ id: string; }>; id: string; } | { type: "created_time"; created_time: string; id: string; } | { type: "created_by"; created_by: { id: IdRequest; object: "user"; } | { type: "person"; person: { email?: string; }; name: string | null; avatar_url: string | null; id: IdRequest; object: "user"; } | { type: "bot"; bot: EmptyObject | { owner: { type: "user"; user: { type: "person"; person: { email: string; }; name: string | null; avatar_url: string | null; id: IdRequest; object: "user"; } | { id: IdRequest; object: "user"; }; } | { type: "workspace"; workspace: true; }; }; name: string | null; avatar_url: string | null; id: IdRequest; object: "user"; }; id: string; } | { type: "last_edited_time"; last_edited_time: string; id: string; } | { type: "last_edited_by"; last_edited_by: { id: IdRequest; object: "user"; } | { type: "person"; person: { email?: string; }; name: string | null; avatar_url: string | null; id: IdRequest; object: "user"; } | { type: "bot"; bot: EmptyObject | { owner: { type: "user"; user: { type: "person"; person: { email: string; }; name: string | null; avatar_url: string | null; id: IdRequest; object: "user"; } | { id: IdRequest; object: "user"; }; } | { type: "workspace"; workspace: true; }; }; name: string | null; avatar_url: string | null; id: IdRequest; object: "user"; }; id: string; } | { type: "rollup"; rollup: { type: "number"; number: number | null; function: "count" | "count_values" | "empty" | "not_empty" | "unique" | "show_unique" | "percent_empty" | "percent_not_empty" | "sum" | "average" | "median" | "min" | "max" | "range" | "earliest_date" | "latest_date" | "date_range" | "checked" | "unchecked" | "percent_checked" | "percent_unchecked" | "show_original"; } | { type: "date"; date: { start: string; end: string | null; time_zone: TimeZoneRequest | null; } | null; function: "count" | "count_values" | "empty" | "not_empty" | "unique" | "show_unique" | "percent_empty" | "percent_not_empty" | "sum" | "average" | "median" | "min" | "max" | "range" | "earliest_date" | "latest_date" | "date_range" | "checked" | "unchecked" | "percent_checked" | "percent_unchecked" | "show_original"; } | { type: "array"; array: Array<{ type: "title"; title: Array<{ type: "text"; text: { content: string; link: { url: TextRequest; } | null; }; annotations: { bold: boolean; italic: boolean; strikethrough: boolean; underline: boolean; code: boolean; color: "default" | "gray" | "brown" | "orange" | "yellow" | "green" | "blue" | "purple" | "pink" | "red" | "gray_background" | "brown_background" | "orange_background" | "yellow_background" | "green_background" | "blue_background" | "purple_background" | "pink_background" | "red_background"; }; plain_text: string; href: string | null; } | { type: "mention"; mention: { type: "user"; user: { id: IdRequest; object: "user"; } | { type: "person"; person: { email?: string; }; name: string | null; avatar_url: string | null; id: IdRequest; object: "user"; } | { type: "bot"; bot: EmptyObject | { owner: { type: "user"; user: { type: "person"; person: { email: string; }; name: string | null; avatar_url: string | null; id: IdRequest; object: "user"; } | { id: IdRequest; object: "user"; }; } | { type: "workspace"; workspace: true; }; }; name: string | null; avatar_url: string | null; id: IdRequest; object: "user"; }; } | { type: "date"; date: { start: string; end: string | null; time_zone: TimeZoneRequest | null; }; } | { type: "link_preview"; link_preview: { url: TextRequest; }; } | { type: "page"; page: { id: IdRequest; }; } | { type: "database"; database: { id: IdRequest; }; }; annotations: { bold: boolean; italic: boolean; strikethrough: boolean; underline: boolean; code: boolean; color: "default" | "gray" | "brown" | "orange" | "yellow" | "green" | "blue" | "purple" | "pink" | "red" | "gray_background" | "brown_background" | "orange_background" | "yellow_background" | "green_background" | "blue_background" | "purple_background" | "pink_background" | "red_background"; }; plain_text: string; href: string | null; } | { type: "equation"; equation: { expression: TextRequest; }; annotations: { bold: boolean; italic: boolean; strikethrough: boolean; underline: boolean; code: boolean; color: "default" | "gray" | "brown" | "orange" | "yellow" | "green" | "blue" | "purple" | "pink" | "red" | "gray_background" | "brown_background" | "orange_background" | "yellow_background" | "green_background" | "blue_background" | "purple_background" | "pink_background" | "red_background"; }; plain_text: string; href: string | null; }>; } | { type: "rich_text"; rich_text: Array<{ type: "text"; text: { content: string; link: { url: TextRequest; } | null; }; annotations: { bold: boolean; italic: boolean; strikethrough: boolean; underline: boolean; code: boolean; color: "default" | "gray" | "brown" | "orange" | "yellow" | "green" | "blue" | "purple" | "pink" | "red" | "gray_background" | "brown_background" | "orange_background" | "yellow_background" | "green_background" | "blue_background" | "purple_background" | "pink_background" | "red_background"; }; plain_text: string; href: string | null; } | { type: "mention"; mention: { type: "user"; user: { id: IdRequest; object: "user"; } | { type: "person"; person: { email?: string; }; name: string | null; avatar_url: string | null; id: IdRequest; object: "user"; } | { type: "bot"; bot: EmptyObject | { owner: { type: "user"; user: { type: "person"; person: { email: string; }; name: string | null; avatar_url: string | null; id: IdRequest; object: "user"; } | { id: IdRequest; object: "user"; }; } | { type: "workspace"; workspace: true; }; }; name: string | null; avatar_url: string | null; id: IdRequest; object: "user"; }; } | { type: "date"; date: { start: string; end: string | null; time_zone: TimeZoneRequest | null; }; } | { type: "link_preview"; link_preview: { url: TextRequest; }; } | { type: "page"; page: { id: IdRequest; }; } | { type: "database"; database: { id: IdRequest; }; }; annotations: { bold: boolean; italic: boolean; strikethrough: boolean; underline: boolean; code: boolean; color: "default" | "gray" | "brown" | "orange" | "yellow" | "green" | "blue" | "purple" | "pink" | "red" | "gray_background" | "brown_background" | "orange_background" | "yellow_background" | "green_background" | "blue_background" | "purple_background" | "pink_background" | "red_background"; }; plain_text: string; href: string | null; } | { type: "equation"; equation: { expression: TextRequest; }; annotations: { bold: boolean; italic: boolean; strikethrough: boolean; underline: boolean; code: boolean; color: "default" | "gray" | "brown" | "orange" | "yellow" | "green" | "blue" | "purple" | "pink" | "red" | "gray_background" | "brown_background" | "orange_background" | "yellow_background" | "green_background" | "blue_background" | "purple_background" | "pink_background" | "red_background"; }; plain_text: string; href: string | null; }>; } | { type: "number"; number: number | null; } | { type: "url"; url: string | null; } | { type: "select"; select: { id: StringRequest; name: StringRequest; color: "default" | "gray" | "brown" | "orange" | "yellow" | "green" | "blue" | "purple" | "pink" | "red"; } | null; } | { type: "multi_select"; multi_select: Array<{ id: StringRequest; name: StringRequest; color: "default" | "gray" | "brown" | "orange" | "yellow" | "green" | "blue" | "purple" | "pink" | "red"; }>; } | { type: "people"; people: Array<{ id: IdRequest; object: "user"; } | { type: "person"; person: { email?: string; }; name: string | null; avatar_url: string | null; id: IdRequest; object: "user"; } | { type: "bot"; bot: EmptyObject | { owner: { type: "user"; user: { type: "person"; person: { email: string; }; name: string | null; avatar_url: string | null; id: IdRequest; object: "user"; } | { id: IdRequest; object: "user"; }; } | { type: "workspace"; workspace: true; }; }; name: string | null; avatar_url: string | null; id: IdRequest; object: "user"; }>; } | { type: "email"; email: string | null; } | { type: "phone_number"; phone_number: string | null; } | { type: "date"; date: { start: string; end: string | null; time_zone: TimeZoneRequest | null; } | null; } | { type: "files"; files: Array<{ file: { url: string; expiry_time: string; }; name: StringRequest; type?: "file"; } | { external: { url: TextRequest; }; name: StringRequest; type?: "external"; }>; } | { type: "checkbox"; checkbox: boolean; } | { type: "formula"; formula: { type: "string"; string: string | null; } | { type: "date"; date: { start: string; end: string | null; time_zone: TimeZoneRequest | null; } | null; } | { type: "number"; number: number | null; } | { type: "boolean"; boolean: boolean | null; }; } | { type: "relation"; relation: Array<{ id: string; }>; } | { type: "created_time"; created_time: string; } | { type: "created_by"; created_by: { id: IdRequest; object: "user"; } | { type: "person"; person: { email?: string; }; name: string | null; avatar_url: string | null; id: IdRequest; object: "user"; } | { type: "bot"; bot: EmptyObject | { owner: { type: "user"; user: { type: "person"; person: { email: string; }; name: string | null; avatar_url: string | null; id: IdRequest; object: "user"; } | { id: IdRequest; object: "user"; }; } | { type: "workspace"; workspace: true; }; }; name: string | null; avatar_url: string | null; id: IdRequest; object: "user"; }; } | { type: "last_edited_time"; last_edited_time: string; } | { type: "last_edited_by"; last_edited_by: { id: IdRequest; object: "user"; } | { type: "person"; person: { email?: string; }; name: string | null; avatar_url: string | null; id: IdRequest; object: "user"; } | { type: "bot"; bot: EmptyObject | { owner: { type: "user"; user: { type: "person"; person: { email: string; }; name: string | null; avatar_url: string | null; id: IdRequest; object: "user"; } | { id: IdRequest; object: "user"; }; } | { type: "workspace"; workspace: true; }; }; name: string | null; avatar_url: string | null; id: IdRequest; object: "user"; }; }>; function: "count" | "count_values" | "empty" | "not_empty" | "unique" | "show_unique" | "percent_empty" | "percent_not_empty" | "sum" | "average" | "median" | "min" | "max" | "range" | "earliest_date" | "latest_date" | "date_range" | "checked" | "unchecked" | "percent_checked" | "percent_unchecked" | "show_original"; } | { type: "unsupported"; unsupported: EmptyObject; function: "count" | "count_values" | "empty" | "not_empty" | "unique" | "show_unique" | "percent_empty" | "percent_not_empty" | "sum" | "average" | "median" | "min" | "max" | "range" | "earliest_date" | "latest_date" | "date_range" | "checked" | "unchecked" | "percent_checked" | "percent_unchecked" | "show_original"; }; id: string; }>; icon: { type: "emoji"; emoji: EmojiRequest; } | null | { type: "external"; external: { url: TextRequest; }; } | null | { type: "file"; file: { url: string; expiry_time: string; }; } | null; cover: { type: "external"; external: { url: TextRequest; }; } | null | { type: "file"; file: { url: string; expiry_time: string; }; } | null; object: "page"; id: string; created_time: string; last_edited_time: string; archived: boolean; url: string; } | { object: "page"; id: string; }>; next_cursor: string | null; has_more: boolean; } | { object: "list"; results: Array<{ parent: { type: "database_id"; database_id: IdRequest; } | { type: "page_id"; page_id: IdRequest; } | { type: "workspace"; workspace: true; }; properties: Record; id: string; } | { type: "rich_text"; rich_text: Array<{ type: "text"; text: { content: string; link: { url: TextRequest; } | null; }; annotations: { bold: boolean; italic: boolean; strikethrough: boolean; underline: boolean; code: boolean; color: "default" | "gray" | "brown" | "orange" | "yellow" | "green" | "blue" | "purple" | "pink" | "red" | "gray_background" | "brown_background" | "orange_background" | "yellow_background" | "green_background" | "blue_background" | "purple_background" | "pink_background" | "red_background"; }; plain_text: string; href: string | null; } | { type: "mention"; mention: { type: "user"; user: { id: IdRequest; object: "user"; } | { type: "person"; person: { email?: string; }; name: string | null; avatar_url: string | null; id: IdRequest; object: "user"; } | { type: "bot"; bot: EmptyObject | { owner: { type: "user"; user: { type: "person"; person: { email: string; }; name: string | null; avatar_url: string | null; id: IdRequest; object: "user"; } | { id: IdRequest; object: "user"; }; } | { type: "workspace"; workspace: true; }; }; name: string | null; avatar_url: string | null; id: IdRequest; object: "user"; }; } | { type: "date"; date: { start: string; end: string | null; time_zone: TimeZoneRequest | null; }; } | { type: "link_preview"; link_preview: { url: TextRequest; }; } | { type: "page"; page: { id: IdRequest; }; } | { type: "database"; database: { id: IdRequest; }; }; annotations: { bold: boolean; italic: boolean; strikethrough: boolean; underline: boolean; code: boolean; color: "default" | "gray" | "brown" | "orange" | "yellow" | "green" | "blue" | "purple" | "pink" | "red" | "gray_background" | "brown_background" | "orange_background" | "yellow_background" | "green_background" | "blue_background" | "purple_background" | "pink_background" | "red_background"; }; plain_text: string; href: string | null; } | { type: "equation"; equation: { expression: TextRequest; }; annotations: { bold: boolean; italic: boolean; strikethrough: boolean; underline: boolean; code: boolean; color: "default" | "gray" | "brown" | "orange" | "yellow" | "green" | "blue" | "purple" | "pink" | "red" | "gray_background" | "brown_background" | "orange_background" | "yellow_background" | "green_background" | "blue_background" | "purple_background" | "pink_background" | "red_background"; }; plain_text: string; href: string | null; }>; id: string; } | { type: "number"; number: number | null; id: string; } | { type: "url"; url: string | null; id: string; } | { type: "select"; select: { id: StringRequest; name: StringRequest; color: "default" | "gray" | "brown" | "orange" | "yellow" | "green" | "blue" | "purple" | "pink" | "red"; } | null; id: string; } | { type: "multi_select"; multi_select: Array<{ id: StringRequest; name: StringRequest; color: "default" | "gray" | "brown" | "orange" | "yellow" | "green" | "blue" | "purple" | "pink" | "red"; }>; id: string; } | { type: "people"; people: Array<{ id: IdRequest; object: "user"; } | { type: "person"; person: { email?: string; }; name: string | null; avatar_url: string | null; id: IdRequest; object: "user"; } | { type: "bot"; bot: EmptyObject | { owner: { type: "user"; user: { type: "person"; person: { email: string; }; name: string | null; avatar_url: string | null; id: IdRequest; object: "user"; } | { id: IdRequest; object: "user"; }; } | { type: "workspace"; workspace: true; }; }; name: string | null; avatar_url: string | null; id: IdRequest; object: "user"; }>; id: string; } | { type: "email"; email: string | null; id: string; } | { type: "phone_number"; phone_number: string | null; id: string; } | { type: "date"; date: { start: string; end: string | null; time_zone: TimeZoneRequest | null; } | null; id: string; } | { type: "files"; files: Array<{ file: { url: string; expiry_time: string; }; name: StringRequest; type?: "file"; } | { external: { url: TextRequest; }; name: StringRequest; type?: "external"; }>; id: string; } | { type: "checkbox"; checkbox: boolean; id: string; } | { type: "formula"; formula: { type: "string"; string: string | null; } | { type: "date"; date: { start: string; end: string | null; time_zone: TimeZoneRequest | null; } | null; } | { type: "number"; number: number | null; } | { type: "boolean"; boolean: boolean | null; }; id: string; } | { type: "relation"; relation: Array<{ id: string; }>; id: string; } | { type: "created_time"; created_time: string; id: string; } | { type: "created_by"; created_by: { id: IdRequest; object: "user"; } | { type: "person"; person: { email?: string; }; name: string | null; avatar_url: string | null; id: IdRequest; object: "user"; } | { type: "bot"; bot: EmptyObject | { owner: { type: "user"; user: { type: "person"; person: { email: string; }; name: string | null; avatar_url: string | null; id: IdRequest; object: "user"; } | { id: IdRequest; object: "user"; }; } | { type: "workspace"; workspace: true; }; }; name: string | null; avatar_url: string | null; id: IdRequest; object: "user"; }; id: string; } | { type: "last_edited_time"; last_edited_time: string; id: string; } | { type: "last_edited_by"; last_edited_by: { id: IdRequest; object: "user"; } | { type: "person"; person: { email?: string; }; name: string | null; avatar_url: string | null; id: IdRequest; object: "user"; } | { type: "bot"; bot: EmptyObject | { owner: { type: "user"; user: { type: "person"; person: { email: string; }; name: string | null; avatar_url: string | null; id: IdRequest; object: "user"; } | { id: IdRequest; object: "user"; }; } | { type: "workspace"; workspace: true; }; }; name: string | null; avatar_url: string | null; id: IdRequest; object: "user"; }; id: string; } | { type: "rollup"; rollup: { type: "number"; number: number | null; function: "count" | "count_values" | "empty" | "not_empty" | "unique" | "show_unique" | "percent_empty" | "percent_not_empty" | "sum" | "average" | "median" | "min" | "max" | "range" | "earliest_date" | "latest_date" | "date_range" | "checked" | "unchecked" | "percent_checked" | "percent_unchecked" | "show_original"; } | { type: "date"; date: { start: string; end: string | null; time_zone: TimeZoneRequest | null; } | null; function: "count" | "count_values" | "empty" | "not_empty" | "unique" | "show_unique" | "percent_empty" | "percent_not_empty" | "sum" | "average" | "median" | "min" | "max" | "range" | "earliest_date" | "latest_date" | "date_range" | "checked" | "unchecked" | "percent_checked" | "percent_unchecked" | "show_original"; } | { type: "array"; array: Array<{ type: "title"; title: Array<{ type: "text"; text: { content: string; link: { url: TextRequest; } | null; }; annotations: { bold: boolean; italic: boolean; strikethrough: boolean; underline: boolean; code: boolean; color: "default" | "gray" | "brown" | "orange" | "yellow" | "green" | "blue" | "purple" | "pink" | "red" | "gray_background" | "brown_background" | "orange_background" | "yellow_background" | "green_background" | "blue_background" | "purple_background" | "pink_background" | "red_background"; }; plain_text: string; href: string | null; } | { type: "mention"; mention: { type: "user"; user: { id: IdRequest; object: "user"; } | { type: "person"; person: { email?: string; }; name: string | null; avatar_url: string | null; id: IdRequest; object: "user"; } | { type: "bot"; bot: EmptyObject | { owner: { type: "user"; user: { type: "person"; person: { email: string; }; name: string | null; avatar_url: string | null; id: IdRequest; object: "user"; } | { id: IdRequest; object: "user"; }; } | { type: "workspace"; workspace: true; }; }; name: string | null; avatar_url: string | null; id: IdRequest; object: "user"; }; } | { type: "date"; date: { start: string; end: string | null; time_zone: TimeZoneRequest | null; }; } | { type: "link_preview"; link_preview: { url: TextRequest; }; } | { type: "page"; page: { id: IdRequest; }; } | { type: "database"; database: { id: IdRequest; }; }; annotations: { bold: boolean; italic: boolean; strikethrough: boolean; underline: boolean; code: boolean; color: "default" | "gray" | "brown" | "orange" | "yellow" | "green" | "blue" | "purple" | "pink" | "red" | "gray_background" | "brown_background" | "orange_background" | "yellow_background" | "green_background" | "blue_background" | "purple_background" | "pink_background" | "red_background"; }; plain_text: string; href: string | null; } | { type: "equation"; equation: { expression: TextRequest; }; annotations: { bold: boolean; italic: boolean; strikethrough: boolean; underline: boolean; code: boolean; color: "default" | "gray" | "brown" | "orange" | "yellow" | "green" | "blue" | "purple" | "pink" | "red" | "gray_background" | "brown_background" | "orange_background" | "yellow_background" | "green_background" | "blue_background" | "purple_background" | "pink_background" | "red_background"; }; plain_text: string; href: string | null; }>; } | { type: "rich_text"; rich_text: Array<{ type: "text"; text: { content: string; link: { url: TextRequest; } | null; }; annotations: { bold: boolean; italic: boolean; strikethrough: boolean; underline: boolean; code: boolean; color: "default" | "gray" | "brown" | "orange" | "yellow" | "green" | "blue" | "purple" | "pink" | "red" | "gray_background" | "brown_background" | "orange_background" | "yellow_background" | "green_background" | "blue_background" | "purple_background" | "pink_background" | "red_background"; }; plain_text: string; href: string | null; } | { type: "mention"; mention: { type: "user"; user: { id: IdRequest; object: "user"; } | { type: "person"; person: { email?: string; }; name: string | null; avatar_url: string | null; id: IdRequest; object: "user"; } | { type: "bot"; bot: EmptyObject | { owner: { type: "user"; user: { type: "person"; person: { email: string; }; name: string | null; avatar_url: string | null; id: IdRequest; object: "user"; } | { id: IdRequest; object: "user"; }; } | { type: "workspace"; workspace: true; }; }; name: string | null; avatar_url: string | null; id: IdRequest; object: "user"; }; } | { type: "date"; date: { start: string; end: string | null; time_zone: TimeZoneRequest | null; }; } | { type: "link_preview"; link_preview: { url: TextRequest; }; } | { type: "page"; page: { id: IdRequest; }; } | { type: "database"; database: { id: IdRequest; }; }; annotations: { bold: boolean; italic: boolean; strikethrough: boolean; underline: boolean; code: boolean; color: "default" | "gray" | "brown" | "orange" | "yellow" | "green" | "blue" | "purple" | "pink" | "red" | "gray_background" | "brown_background" | "orange_background" | "yellow_background" | "green_background" | "blue_background" | "purple_background" | "pink_background" | "red_background"; }; plain_text: string; href: string | null; } | { type: "equation"; equation: { expression: TextRequest; }; annotations: { bold: boolean; italic: boolean; strikethrough: boolean; underline: boolean; code: boolean; color: "default" | "gray" | "brown" | "orange" | "yellow" | "green" | "blue" | "purple" | "pink" | "red" | "gray_background" | "brown_background" | "orange_background" | "yellow_background" | "green_background" | "blue_background" | "purple_background" | "pink_background" | "red_background"; }; plain_text: string; href: string | null; }>; } | { type: "number"; number: number | null; } | { type: "url"; url: string | null; } | { type: "select"; select: { id: StringRequest; name: StringRequest; color: "default" | "gray" | "brown" | "orange" | "yellow" | "green" | "blue" | "purple" | "pink" | "red"; } | null; } | { type: "multi_select"; multi_select: Array<{ id: StringRequest; name: StringRequest; color: "default" | "gray" | "brown" | "orange" | "yellow" | "green" | "blue" | "purple" | "pink" | "red"; }>; } | { type: "people"; people: Array<{ id: IdRequest; object: "user"; } | { type: "person"; person: { email?: string; }; name: string | null; avatar_url: string | null; id: IdRequest; object: "user"; } | { type: "bot"; bot: EmptyObject | { owner: { type: "user"; user: { type: "person"; person: { email: string; }; name: string | null; avatar_url: string | null; id: IdRequest; object: "user"; } | { id: IdRequest; object: "user"; }; } | { type: "workspace"; workspace: true; }; }; name: string | null; avatar_url: string | null; id: IdRequest; object: "user"; }>; } | { type: "email"; email: string | null; } | { type: "phone_number"; phone_number: string | null; } | { type: "date"; date: { start: string; end: string | null; time_zone: TimeZoneRequest | null; } | null; } | { type: "files"; files: Array<{ file: { url: string; expiry_time: string; }; name: StringRequest; type?: "file"; } | { external: { url: TextRequest; }; name: StringRequest; type?: "external"; }>; } | { type: "checkbox"; checkbox: boolean; } | { type: "formula"; formula: { type: "string"; string: string | null; } | { type: "date"; date: { start: string; end: string | null; time_zone: TimeZoneRequest | null; } | null; } | { type: "number"; number: number | null; } | { type: "boolean"; boolean: boolean | null; }; } | { type: "relation"; relation: Array<{ id: string; }>; } | { type: "created_time"; created_time: string; } | { type: "created_by"; created_by: { id: IdRequest; object: "user"; } | { type: "person"; person: { email?: string; }; name: string | null; avatar_url: string | null; id: IdRequest; object: "user"; } | { type: "bot"; bot: EmptyObject | { owner: { type: "user"; user: { type: "person"; person: { email: string; }; name: string | null; avatar_url: string | null; id: IdRequest; object: "user"; } | { id: IdRequest; object: "user"; }; } | { type: "workspace"; workspace: true; }; }; name: string | null; avatar_url: string | null; id: IdRequest; object: "user"; }; } | { type: "last_edited_time"; last_edited_time: string; } | { type: "last_edited_by"; last_edited_by: { id: IdRequest; object: "user"; } | { type: "person"; person: { email?: string; }; name: string | null; avatar_url: string | null; id: IdRequest; object: "user"; } | { type: "bot"; bot: EmptyObject | { owner: { type: "user"; user: { type: "person"; person: { email: string; }; name: string | null; avatar_url: string | null; id: IdRequest; object: "user"; } | { id: IdRequest; object: "user"; }; } | { type: "workspace"; workspace: true; }; }; name: string | null; avatar_url: string | null; id: IdRequest; object: "user"; }; }>; function: "count" | "count_values" | "empty" | "not_empty" | "unique" | "show_unique" | "percent_empty" | "percent_not_empty" | "sum" | "average" | "median" | "min" | "max" | "range" | "earliest_date" | "latest_date" | "date_range" | "checked" | "unchecked" | "percent_checked" | "percent_unchecked" | "show_original"; } | { type: "unsupported"; unsupported: EmptyObject; function: "count" | "count_values" | "empty" | "not_empty" | "unique" | "show_unique" | "percent_empty" | "percent_not_empty" | "sum" | "average" | "median" | "min" | "max" | "range" | "earliest_date" | "latest_date" | "date_range" | "checked" | "unchecked" | "percent_checked" | "percent_unchecked" | "show_original"; }; id: string; }>; icon: { type: "emoji"; emoji: EmojiRequest; } | null | { type: "external"; external: { url: TextRequest; }; } | null | { type: "file"; file: { url: string; expiry_time: string; }; } | null; cover: { type: "external"; external: { url: TextRequest; }; } | null | { type: "file"; file: { url: string; expiry_time: string; }; } | null; object: "page"; id: string; created_time: string; last_edited_time: string; archived: boolean; url: string; } | { object: "page"; id: string; }>; next_cursor: string | null; has_more: boolean; }; ```
danopia commented 2 years ago

I've found that the generate-random-data SDK example shows the same issue with the newer partial-response typings. So here's how I fixed it:

  const queryResponse = await notion.databases.query({
    database_id: databaseId,
  })
  queryResponse.results.forEach(pageIn => {
    // if (pageIn.object) return;
    const page = pageIn as Extract<typeof pageIn, {parent: {}}>;

I really expected the if (page.object) return; check to narrow the union, but it didn't actually change anything in the types so the Extract cast is doing the actual work. I'm not sure if I believe that 'object' isn't always there at this point.

justjake commented 2 years ago

Hello,

I have released my personal Notion API toolkit to NPM here: @jitl/notion-api. This package exports types like Block<BlockType>, Page, RichText, etc derived from the official types exported from this repo (which I recently shrank from 2.2mb to 300kb). @jitl/notion-api also provides utility functions for iterating paginated APIs, narrowing types like GetBlockResponse, and more. See the README section on API helpers.

To re-write @danopia's example with my library:

import { isFullPage, iteratePaginatedAPI } from '@jitl/notion-api';

for await (const page of iteratePaginatedAPI(notion.databases.query, {
  database_id: databaseId,
})) {
  if (isFullPage(page)) {
    // page is a "full" page object
    doThings(page.properties)
  }
}

To re-write @Niceplace's example with my library:

import { isFullPage, iteratePaginatedAPI, asyncIterableToArray } from '@jitl/notion-api';

/**
 * Query all pages and return all records from a Notion database object
 *  Will log a warning if database has no records
 * @param parameters To specify database id, control sorting, filtering and
 *   pagination, directly using Notion's sdk types
 * @returns A list of all the results from the database or an empty array
 */
export const queryAll = async (
  parameters: QueryDatabaseParameters
): Promise<Page[]> => {
  const params: typeof parameters = { ...parameters };
  const resultsWithPartialPages = await asyncIterableToArray(
    // getNotionClient() returns an authenticated instance of the notion SDK
    iteratePaginatedAPI(getNotionClient().databases.query, parameters)
  );

  // Filter out partial pages
  const fullPages = resultsWithPartialPages.filter(isFullPage);

  if (!fullPages.length) {
    logger.warn(`No results found in database ${params.database_id}`);
  }
  return fullPages;
};

@jitl/notion-api is not an official Notion product. I wrote it for my own use, to support my website and other personal projects, although I welcome contributions of any kind.

jonrimmer commented 2 years ago

I'm experimenting with writing a Notion integration, but I cannot figure out how to use the types generated for search. The API documentation says that the results have properties like created_by, title, etc. But the only properties TS thinks they have is id and object:

image

Even if I check the value of object, I still cannot access any of these metadata fields:

image

Am I missing something?

justjake commented 2 years ago

@jonrimmer there are two types here - “partial” object types, which are returned for objects that an integration can locate, but not access, and “full” object types that have all of the properties referenced in the documentation.

You need to check for the presence of a full-object-only key in the response to prove to Typescript that the object is “full”. This is annoying to do, so I published a helper library (linked in the above post) that exports functions like isFullPage, etc.

From my library’s README:


The Notion API can sometimes return "partial" object data that contain only the block's ID:

// In @notionhq/client typings:
type PartialBlockObjectResponse = { object: 'block'; id: string };
export type GetBlockResponse = PartialBlockObjectResponse | BlockObjectResponse;

Checking that a GetBlockResponse (or similar type) is a "full" block gets old pretty fast, so this library exports type guard functions to handle common cases, like isFullPage(page) and isFullBlock(block).

isFullBlock can optionally narrow the type of block as well:

if (isFullBlock(block, 'paragraph')) {
  // It's a full paragraph block
  console.log(richTextAsPlainText(block.paragraph.text));
}
Niceplace commented 2 years ago

@jonrimmer This is most likely because of the heavy usage of union types, as explained in my previous comment here: https://github.com/makenotion/notion-sdk-js/issues/219#issuecomment-1015943168

It acts like a least common denominator for type properties that you can see.

sebastiankade commented 2 years ago

This is still a gross situation as of @notionhq/client@1.04 ....

One way to solve is to use @justjake 's @jitl/notion-api as mentioned above, the types are good and utils are helpful. However they are not up to date with latest API types from notion (e.g. status) that have been recently released.

Alternatively if you want to battle your way with pulling out the types from the unions here are some type util helpers:

type ArrayType<ArrType> = ArrType extends readonly (infer ElementType)[]
  ? ElementType
  : never;

export type DatabaseItem = ArrayType<QueryDatabaseResponse["results"]>;
export type Page = Extract<DatabaseItem, { properties: any }>;
export type PageReference = Exclude<DatabaseItem, Page>;

@rhart92 you guys should consider exporting these as helper types as it does take a lot of messing around to start getting value out of the notion database queries atm.

wcauchois commented 2 years ago

Hi, I recently made some changes to name many more response types and export them from this package in #319, so I am going to close this issue. These changes are available in version 2.1.0 of the SDK. If there are any more specific issues around types please open a new GitHub issue, thank you!