code icon Code

Query Notion Database

Query a Notion database with filters and sorts, handling pagination automatically

Source Code

import fs from "fs";

const [
  databaseId,
  filterJson = "",
  sortsJson = "",
  maxResults = "100",
  outputPath = "session/notion-query.json",
] = process.argv.slice(2);

if (!databaseId) {
  console.error("Error: databaseId is required");
  process.exit(1);
}

const maxResultsNum = Math.min(parseInt(maxResults) || 100, 500);

// Parse filter and sorts if provided
let filter = null;
let sorts = null;

if (filterJson) {
  try {
    filter = JSON.parse(filterJson);
  } catch (e) {
    console.error("Error parsing filter JSON:", e.message);
    process.exit(1);
  }
}

if (sortsJson) {
  try {
    sorts = JSON.parse(sortsJson);
  } catch (e) {
    console.error("Error parsing sorts JSON:", e.message);
    process.exit(1);
  }
}

console.log(`Querying database ${databaseId}...`);
if (filter) console.log(`  Filter: ${JSON.stringify(filter)}`);
if (sorts) console.log(`  Sort: ${JSON.stringify(sorts)}`);

const NOTION_API = "https://api.notion.com/v1";
const headers = {
  Authorization: "Bearer PLACEHOLDER_TOKEN",
  "Content-Type": "application/json",
  "Notion-Version": "2022-06-28",
};

/**
 * Extract readable value from a Notion property
 */
function extractPropertyValue(prop) {
  if (!prop) return null;

  switch (prop.type) {
    case "title":
      return prop.title?.map((t) => t.plain_text).join("") || "";
    case "rich_text":
      return prop.rich_text?.map((t) => t.plain_text).join("") || "";
    case "number":
      return prop.number;
    case "select":
      return prop.select?.name || null;
    case "multi_select":
      return prop.multi_select?.map((s) => s.name) || [];
    case "date":
      if (!prop.date) return null;
      return prop.date.end
        ? { start: prop.date.start, end: prop.date.end }
        : prop.date.start;
    case "people":
      return prop.people?.map((p) => p.name || p.id) || [];
    case "files":
      return (
        prop.files?.map((f) => ({
          name: f.name,
          url: f.file?.url || f.external?.url,
        })) || []
      );
    case "checkbox":
      return prop.checkbox;
    case "url":
      return prop.url;
    case "email":
      return prop.email;
    case "phone_number":
      return prop.phone_number;
    case "formula":
      return prop.formula?.[prop.formula.type];
    case "relation":
      return prop.relation?.map((r) => r.id) || [];
    case "rollup":
      if (prop.rollup?.type === "array") {
        return prop.rollup.array?.map((item) => extractPropertyValue(item));
      }
      return prop.rollup?.[prop.rollup.type];
    case "status":
      return prop.status?.name || null;
    case "created_time":
      return prop.created_time;
    case "last_edited_time":
      return prop.last_edited_time;
    case "created_by":
      return prop.created_by?.name || prop.created_by?.id;
    case "last_edited_by":
      return prop.last_edited_by?.name || prop.last_edited_by?.id;
    case "unique_id":
      return prop.unique_id?.prefix
        ? `${prop.unique_id.prefix}-${prop.unique_id.number}`
        : prop.unique_id?.number;
    default:
      return null;
  }
}

try {
  // First, get database schema
  const dbRes = await fetch(`${NOTION_API}/databases/${databaseId}`, {
    method: "GET",
    headers,
  });

  if (!dbRes.ok) {
    const errorText = await dbRes.text();
    console.error(`Failed to retrieve database: ${dbRes.status}`);
    console.error(errorText);
    throw new Error(`Failed to retrieve database: ${dbRes.status}`);
  }

  const dbInfo = await dbRes.json();
  const dbTitle = dbInfo.title?.[0]?.plain_text || "Untitled Database";
  const schema = {};

  for (const [name, prop] of Object.entries(dbInfo.properties || {})) {
    schema[name] = {
      type: prop.type,
      ...(prop.select && { options: prop.select.options?.map((o) => o.name) }),
      ...(prop.multi_select && {
        options: prop.multi_select.options?.map((o) => o.name),
      }),
      ...(prop.status && {
        options: prop.status.options?.map((o) => o.name),
        groups: prop.status.groups?.map((g) => g.name),
      }),
    };
  }

  console.log(`  Database: ${dbTitle}`);
  console.log(`  Properties: ${Object.keys(schema).join(", ")}`);

  // Query the database
  const entries = [];
  let cursor = undefined;
  let totalFetched = 0;

  while (totalFetched < maxResultsNum) {
    const pageSize = Math.min(maxResultsNum - totalFetched, 100);

    const body = {
      page_size: pageSize,
      ...(filter && { filter }),
      ...(sorts && { sorts }),
      ...(cursor && { start_cursor: cursor }),
    };

    const queryRes = await fetch(
      `${NOTION_API}/databases/${databaseId}/query`,
      {
        method: "POST",
        headers,
        body: JSON.stringify(body),
      }
    );

    if (!queryRes.ok) {
      const errorText = await queryRes.text();
      console.error(`Query failed: ${queryRes.status}`);
      console.error(errorText);
      throw new Error(`Query failed: ${queryRes.status}`);
    }

    const response = await queryRes.json();

    for (const page of response.results) {
      if (totalFetched >= maxResultsNum) break;

      const entry = {
        id: page.id,
        url: page.url,
        createdTime: page.created_time,
        lastEditedTime: page.last_edited_time,
        properties: {},
      };

      // Extract icon
      if (page.icon) {
        entry.icon =
          page.icon.type === "emoji"
            ? page.icon.emoji
            : page.icon.external?.url || page.icon.file?.url;
      }

      // Extract all property values
      for (const [name, prop] of Object.entries(page.properties || {})) {
        entry.properties[name] = extractPropertyValue(prop);
      }

      entries.push(entry);
      totalFetched++;
    }

    if (!response.has_more || totalFetched >= maxResultsNum) break;
    cursor = response.next_cursor;

    console.log(`  Fetched ${totalFetched} entries...`);
  }

  // Ensure output directory exists
  const dir = outputPath.substring(0, outputPath.lastIndexOf("/"));
  if (dir) {
    fs.mkdirSync(dir, { recursive: true });
  }

  const output = {
    databaseId,
    databaseTitle: dbTitle,
    schema,
    filter: filter || null,
    sorts: sorts || null,
    queriedAt: new Date().toISOString(),
    count: entries.length,
    entries,
  };

  fs.writeFileSync(outputPath, JSON.stringify(output, null, 2));

  // Log summary
  console.log(`\nāœ“ Found ${entries.length} entries`);
  console.log(`  Written to: ${outputPath}`);

  // Show first few entries
  if (entries.length > 0) {
    console.log(`\n  Sample entries:`);
    for (const entry of entries.slice(0, 3)) {
      // Find title property
      const titleProp = Object.entries(entry.properties).find(
        ([k]) => schema[k]?.type === "title"
      );
      const title = titleProp ? titleProp[1] : "Untitled";
      console.log(`    ${entry.icon || "šŸ“„"} ${title}`);
    }
    if (entries.length > 3) {
      console.log(`    ... and ${entries.length - 3} more`);
    }
  }

  console.log(
    JSON.stringify({
      success: true,
      outputPath,
      databaseTitle: dbTitle,
      count: entries.length,
      propertyCount: Object.keys(schema).length,
    })
  );
} catch (error) {
  console.error("Error querying database:", error.message);
  throw error;
}
import fs from "fs";

const [
  databaseId,
  filterJson = "",
  sortsJson = "",
  maxResults = "100",
  outputPath = "session/notion-query.json",
] = process.argv.slice(2);

if (!databaseId) {
  console.error("Error: databaseId is required");
  process.exit(1);
}

const maxResultsNum = Math.min(parseInt(maxResults) || 100, 500);

// Parse filter and sorts if provided
let filter = null;
let sorts = null;

if (filterJson) {
  try {
    filter = JSON.parse(filterJson);
  } catch (e) {
    console.error("Error parsing filter JSON:", e.message);
    process.exit(1);
  }
}

if (sortsJson) {
  try {
    sorts = JSON.parse(sortsJson);
  } catch (e) {
    console.error("Error parsing sorts JSON:", e.message);
    process.exit(1);
  }
}

console.log(`Querying database ${databaseId}...`);
if (filter) console.log(`  Filter: ${JSON.stringify(filter)}`);
if (sorts) console.log(`  Sort: ${JSON.stringify(sorts)}`);

const NOTION_API = "https://api.notion.com/v1";
const headers = {
  Authorization: "Bearer PLACEHOLDER_TOKEN",
  "Content-Type": "application/json",
  "Notion-Version": "2022-06-28",
};

/**
 * Extract readable value from a Notion property
 */
function extractPropertyValue(prop) {
  if (!prop) return null;

  switch (prop.type) {
    case "title":
      return prop.title?.map((t) => t.plain_text).join("") || "";
    case "rich_text":
      return prop.rich_text?.map((t) => t.plain_text).join("") || "";
    case "number":
      return prop.number;
    case "select":
      return prop.select?.name || null;
    case "multi_select":
      return prop.multi_select?.map((s) => s.name) || [];
    case "date":
      if (!prop.date) return null;
      return prop.date.end
        ? { start: prop.date.start, end: prop.date.end }
        : prop.date.start;
    case "people":
      return prop.people?.map((p) => p.name || p.id) || [];
    case "files":
      return (
        prop.files?.map((f) => ({
          name: f.name,
          url: f.file?.url || f.external?.url,
        })) || []
      );
    case "checkbox":
      return prop.checkbox;
    case "url":
      return prop.url;
    case "email":
      return prop.email;
    case "phone_number":
      return prop.phone_number;
    case "formula":
      return prop.formula?.[prop.formula.type];
    case "relation":
      return prop.relation?.map((r) => r.id) || [];
    case "rollup":
      if (prop.rollup?.type === "array") {
        return prop.rollup.array?.map((item) => extractPropertyValue(item));
      }
      return prop.rollup?.[prop.rollup.type];
    case "status":
      return prop.status?.name || null;
    case "created_time":
      return prop.created_time;
    case "last_edited_time":
      return prop.last_edited_time;
    case "created_by":
      return prop.created_by?.name || prop.created_by?.id;
    case "last_edited_by":
      return prop.last_edited_by?.name || prop.last_edited_by?.id;
    case "unique_id":
      return prop.unique_id?.prefix
        ? `${prop.unique_id.prefix}-${prop.unique_id.number}`
        : prop.unique_id?.number;
    default:
      return null;
  }
}

try {
  // First, get database schema
  const dbRes = await fetch(`${NOTION_API}/databases/${databaseId}`, {
    method: "GET",
    headers,
  });

  if (!dbRes.ok) {
    const errorText = await dbRes.text();
    console.error(`Failed to retrieve database: ${dbRes.status}`);
    console.error(errorText);
    throw new Error(`Failed to retrieve database: ${dbRes.status}`);
  }

  const dbInfo = await dbRes.json();
  const dbTitle = dbInfo.title?.[0]?.plain_text || "Untitled Database";
  const schema = {};

  for (const [name, prop] of Object.entries(dbInfo.properties || {})) {
    schema[name] = {
      type: prop.type,
      ...(prop.select && { options: prop.select.options?.map((o) => o.name) }),
      ...(prop.multi_select && {
        options: prop.multi_select.options?.map((o) => o.name),
      }),
      ...(prop.status && {
        options: prop.status.options?.map((o) => o.name),
        groups: prop.status.groups?.map((g) => g.name),
      }),
    };
  }

  console.log(`  Database: ${dbTitle}`);
  console.log(`  Properties: ${Object.keys(schema).join(", ")}`);

  // Query the database
  const entries = [];
  let cursor = undefined;
  let totalFetched = 0;

  while (totalFetched < maxResultsNum) {
    const pageSize = Math.min(maxResultsNum - totalFetched, 100);

    const body = {
      page_size: pageSize,
      ...(filter && { filter }),
      ...(sorts && { sorts }),
      ...(cursor && { start_cursor: cursor }),
    };

    const queryRes = await fetch(
      `${NOTION_API}/databases/${databaseId}/query`,
      {
        method: "POST",
        headers,
        body: JSON.stringify(body),
      }
    );

    if (!queryRes.ok) {
      const errorText = await queryRes.text();
      console.error(`Query failed: ${queryRes.status}`);
      console.error(errorText);
      throw new Error(`Query failed: ${queryRes.status}`);
    }

    const response = await queryRes.json();

    for (const page of response.results) {
      if (totalFetched >= maxResultsNum) break;

      const entry = {
        id: page.id,
        url: page.url,
        createdTime: page.created_time,
        lastEditedTime: page.last_edited_time,
        properties: {},
      };

      // Extract icon
      if (page.icon) {
        entry.icon =
          page.icon.type === "emoji"
            ? page.icon.emoji
            : page.icon.external?.url || page.icon.file?.url;
      }

      // Extract all property values
      for (const [name, prop] of Object.entries(page.properties || {})) {
        entry.properties[name] = extractPropertyValue(prop);
      }

      entries.push(entry);
      totalFetched++;
    }

    if (!response.has_more || totalFetched >= maxResultsNum) break;
    cursor = response.next_cursor;

    console.log(`  Fetched ${totalFetched} entries...`);
  }

  // Ensure output directory exists
  const dir = outputPath.substring(0, outputPath.lastIndexOf("/"));
  if (dir) {
    fs.mkdirSync(dir, { recursive: true });
  }

  const output = {
    databaseId,
    databaseTitle: dbTitle,
    schema,
    filter: filter || null,
    sorts: sorts || null,
    queriedAt: new Date().toISOString(),
    count: entries.length,
    entries,
  };

  fs.writeFileSync(outputPath, JSON.stringify(output, null, 2));

  // Log summary
  console.log(`\nāœ“ Found ${entries.length} entries`);
  console.log(`  Written to: ${outputPath}`);

  // Show first few entries
  if (entries.length > 0) {
    console.log(`\n  Sample entries:`);
    for (const entry of entries.slice(0, 3)) {
      // Find title property
      const titleProp = Object.entries(entry.properties).find(
        ([k]) => schema[k]?.type === "title"
      );
      const title = titleProp ? titleProp[1] : "Untitled";
      console.log(`    ${entry.icon || "šŸ“„"} ${title}`);
    }
    if (entries.length > 3) {
      console.log(`    ... and ${entries.length - 3} more`);
    }
  }

  console.log(
    JSON.stringify({
      success: true,
      outputPath,
      databaseTitle: dbTitle,
      count: entries.length,
      propertyCount: Object.keys(schema).length,
    })
  );
} catch (error) {
  console.error("Error querying database:", error.message);
  throw error;
}