# Notion Client

Query databases, create pages, and manage content in Notion workspaces.

## Methods

| Method                                   | Description                                        |
| ---------------------------------------- | -------------------------------------------------- |
| `apiRequest(options, schema, metadata?)` | Make any Notion API request with schema validation |

## Usage

### Query a Database

```typescript
import { api, z, notion } from "@superblocksteam/sdk-api";

// Integration ID from the integrations panel
const PROD_NOTION = "a1b2c3d4-5678-90ab-cdef-notion000001";

const PageSchema = z.object({
  object: z.literal("page"),
  id: z.string(),
  created_time: z.string(),
  last_edited_time: z.string(),
  archived: z.boolean(),
  properties: z.record(z.unknown()),
  url: z.string(),
});

const QueryDatabaseResponseSchema = z.object({
  object: z.literal("list"),
  results: z.array(PageSchema),
  next_cursor: z.string().nullable(),
  has_more: z.boolean(),
});

export default api({
  name: "NotionExample",
  integrations: {
    notion: notion(PROD_NOTION),
  },
  input: z.object({
    databaseId: z.string(),
    status: z.string(),
  }),
  output: z.object({
    pages: z.array(z.object({ id: z.string(), url: z.string() })),
  }),
  async run(ctx, { databaseId, status }) {
    const result = await ctx.integrations.notion.apiRequest(
      {
        method: "POST",
        path: `/v1/databases/${databaseId}/query`,
        body: {
          filter: {
            property: "Status",
            select: {
              equals: status,
            },
          },
        },
      },
      { response: QueryDatabaseResponseSchema },
    );

    return {
      pages: result.results.map((page) => ({
        id: page.id,
        url: page.url,
      })),
    };
  },
});
```

### Create a Page

```typescript
const CreatePageResponseSchema = z.object({
  object: z.literal("page"),
  id: z.string(),
  url: z.string(),
});

const result = await ctx.integrations.notion.apiRequest(
  {
    method: "POST",
    path: "/v1/pages",
    body: {
      parent: { database_id: "abc123-database-id" },
      properties: {
        Name: {
          title: [
            {
              text: { content: "New Task" },
            },
          ],
        },
        Status: {
          select: { name: "To Do" },
        },
        Priority: {
          select: { name: "High" },
        },
        "Due Date": {
          date: { start: "2024-12-31" },
        },
      },
    },
  },
  { response: CreatePageResponseSchema },
);

console.log(`Created page: ${result.url}`);
```

### Update Page Properties

```typescript
const result = await ctx.integrations.notion.apiRequest(
  {
    method: "PATCH",
    path: `/v1/pages/${pageId}`,
    body: {
      properties: {
        Status: {
          select: { name: "Done" },
        },
        "Completed Date": {
          date: { start: new Date().toISOString().split("T")[0] },
        },
      },
    },
  },
  { response: PageSchema },
);
```

### Get a Page

```typescript
const result = await ctx.integrations.notion.apiRequest(
  {
    method: "GET",
    path: `/v1/pages/${pageId}`,
  },
  { response: PageSchema },
);

console.log(`Page: ${result.id}, Archived: ${result.archived}`);
```

### Add Content to a Page (Blocks)

```typescript
const AppendBlocksResponseSchema = z.object({
  object: z.literal("list"),
  results: z.array(z.object({ id: z.string(), type: z.string() })),
});

const result = await ctx.integrations.notion.apiRequest(
  {
    method: "PATCH",
    path: `/v1/blocks/${pageId}/children`,
    body: {
      children: [
        {
          object: "block",
          type: "heading_2",
          heading_2: {
            rich_text: [{ type: "text", text: { content: "Section Title" } }],
          },
        },
        {
          object: "block",
          type: "paragraph",
          paragraph: {
            rich_text: [
              { type: "text", text: { content: "Paragraph content here." } },
            ],
          },
        },
        {
          object: "block",
          type: "bulleted_list_item",
          bulleted_list_item: {
            rich_text: [{ type: "text", text: { content: "List item 1" } }],
          },
        },
      ],
    },
  },
  { response: AppendBlocksResponseSchema },
);
```

### Search

```typescript
const SearchResponseSchema = z.object({
  object: z.literal("list"),
  results: z.array(
    z.object({
      object: z.enum(["page", "database"]),
      id: z.string(),
    }),
  ),
  has_more: z.boolean(),
});

const result = await ctx.integrations.notion.apiRequest(
  {
    method: "POST",
    path: "/v1/search",
    body: {
      query: "project planning",
      filter: { property: "object", value: "page" },
      sort: { direction: "descending", timestamp: "last_edited_time" },
    },
  },
  { response: SearchResponseSchema },
);
```

### Get Database Schema

```typescript
const DatabaseResponseSchema = z.object({
  object: z.literal("database"),
  id: z.string(),
  title: z.array(z.object({ plain_text: z.string() })),
  properties: z.record(
    z.object({
      id: z.string(),
      name: z.string(),
      type: z.string(),
    }),
  ),
});

const result = await ctx.integrations.notion.apiRequest(
  {
    method: "GET",
    path: `/v1/databases/${databaseId}`,
  },
  { response: DatabaseResponseSchema },
);

console.log("Properties:", Object.keys(result.properties));
```

## Trace Metadata

All methods accept an optional `metadata` parameter as the last argument for diagnostics labeling. See the [root SDK README](../../../README.md#trace-metadata) for details.

## Common Pitfalls

### No Specialized Methods

```typescript
// WRONG - These methods do not exist
await notion.queryDatabase({ ... });
await notion.createPage({ ... });

// CORRECT - Use apiRequest
await ctx.integrations.notion.apiRequest(
  { method: "POST", path: `/v1/databases/${id}/query`, body: { ... } },
  { response: QueryDatabaseResponseSchema }
);
```

### Property IDs vs Names

Notion uses property IDs internally but displays names in the UI. When filtering, use the display name:

```typescript
// Use property NAME (as shown in Notion UI)
const filter = {
  property: "Status", // Display name
  select: { equals: "Done" },
};

// NOT the property ID
const wrongFilter = {
  property: "abc123", // ID won't work in filters
  select: { equals: "Done" },
};
```

### Property Type Structure

Each property type has a specific structure:

```typescript
// Title property
const title = {
  title: [{ text: { content: "Page Title" } }],
};

// Select property
const select = {
  select: { name: "Option Name" },
};

// Multi-select property
const multiSelect = {
  multi_select: [{ name: "Tag1" }, { name: "Tag2" }],
};

// Date property
const date = {
  date: { start: "2024-01-15", end: "2024-01-20" },
};

// Rich text property
const richText = {
  rich_text: [{ text: { content: "Text content" } }],
};

// Number property
const number = {
  number: 42,
};

// Checkbox property
const checkbox = {
  checkbox: true,
};

// URL property
const url = {
  url: "https://example.com",
};

// Email property
const email = {
  email: "user@example.com",
};

// Relation property
const relation = {
  relation: [{ id: "page-id-1" }, { id: "page-id-2" }],
};
```

### Filter Syntax

Different property types have different filter operators:

```typescript
// Select/Status filter
{ property: "Status", select: { equals: "Done" } }
{ property: "Status", select: { does_not_equal: "Archived" } }

// Text filter
{ property: "Name", rich_text: { contains: "project" } }
{ property: "Name", rich_text: { starts_with: "Q1" } }

// Date filter
{ property: "Due Date", date: { before: "2024-12-31" } }
{ property: "Due Date", date: { on_or_after: "2024-01-01" } }

// Checkbox filter
{ property: "Completed", checkbox: { equals: true } }

// Compound filters
{
  and: [
    { property: "Status", select: { equals: "In Progress" } },
    { property: "Priority", select: { equals: "High" } },
  ],
}
```

### API Version Header

Notion requires an API version header. The SDK should handle this, but be aware:

```typescript
// The integration should be configured with the Notion-Version header
// e.g., "Notion-Version": "2022-06-28"
```

### Pagination

Large result sets require pagination:

```typescript
async function getAllPages(notion: NotionClient, databaseId: string) {
  const allPages: Page[] = [];
  let cursor: string | undefined;

  do {
    const result = await ctx.integrations.notion.apiRequest(
      {
        method: "POST",
        path: `/v1/databases/${databaseId}/query`,
        body: {
          start_cursor: cursor,
          page_size: 100,
        },
      },
      { response: QueryDatabaseResponseSchema },
    );

    allPages.push(...result.results);
    cursor = result.next_cursor ?? undefined;
  } while (cursor);

  return allPages;
}
```

## Error Handling

```typescript
import { RestApiValidationError } from "@superblocksteam/sdk-api";

try {
  const result = await ctx.integrations.notion.apiRequest(
    { method: "POST", path: `/v1/databases/${id}/query`, body: { ... } },
    { response: QueryDatabaseResponseSchema }
  );
} catch (error) {
  if (error instanceof RestApiValidationError) {
    console.error("Validation failed:", error.details.zodError);
  }
}
```

## API Reference

- [Notion API Documentation](https://developers.notion.com/reference/intro)
- [Query Database](https://developers.notion.com/reference/post-database-query)
- [Create Page](https://developers.notion.com/reference/post-page)
- [Working with Databases](https://developers.notion.com/docs/working-with-databases)
- [Property Values](https://developers.notion.com/reference/property-value-object)
