Fix: Handle Notion database not found errors gracefully (#8860)

* Fix: Handle Notion database not found errors gracefully

* Fix: Use Logger.warn instead of console.log in Notion import task

* Applied automatic fixes

* Touch to trigger actions

* Fix: Implement additional improvements for Notion import error handling

* Applied automatic fixes

* Change to trigger CI

* Fix TypeScript error: Add type assertion for filtered parsedPages

---------

Co-authored-by: codegen-sh[bot] <131295404+codegen-sh[bot]@users.noreply.github.com>
Co-authored-by: Tom Moor <tom@getoutline.com>
Co-authored-by: Codegen <codegen@example.com>
This commit is contained in:
codegen-sh[bot]
2025-04-06 09:04:23 -07:00
committed by GitHub
parent bde9d5fbf4
commit 66e4ec32ed
3 changed files with 59 additions and 26 deletions

View File

@@ -1,6 +1,8 @@
import { APIResponseError, APIErrorCode } from "@notionhq/client";
import { ImportTaskInput, ImportTaskOutput } from "@shared/schema";
import { IntegrationService, ProsemirrorDoc } from "@shared/types";
import { ProsemirrorHelper } from "@shared/utils/ProsemirrorHelper";
import Logger from "@server/logging/Logger";
import { Integration } from "@server/models";
import ImportTask from "@server/models/ImportTask";
import APIImportTask, {
@@ -39,7 +41,10 @@ export default class NotionAPIImportTask extends APIImportTask<IntegrationServic
importTask.input.map(async (item) => this.processPage({ item, client }))
);
const taskOutput: ImportTaskOutput = parsedPages.map((parsedPage) => ({
// Filter out any null results (from pages/databases that couldn't be accessed)
const validParsedPages = parsedPages.filter(Boolean) as ParsePageOutput[];
const taskOutput: ImportTaskOutput = validParsedPages.map((parsedPage) => ({
externalId: parsedPage.externalId,
title: parsedPage.title,
emoji: parsedPage.emoji,
@@ -50,7 +55,7 @@ export default class NotionAPIImportTask extends APIImportTask<IntegrationServic
}));
const childTasksInput: ImportTaskInput<IntegrationService.Notion> =
parsedPages.flatMap((parsedPage) =>
validParsedPages.flatMap((parsedPage) =>
parsedPage.children.map((childPage) => ({
type: childPage.type,
externalId: childPage.externalId,
@@ -88,36 +93,55 @@ export default class NotionAPIImportTask extends APIImportTask<IntegrationServic
}: {
item: ImportTaskInput<IntegrationService.Notion>[number];
client: NotionClient;
}): Promise<ParsePageOutput> {
}): Promise<ParsePageOutput | null> {
const collectionExternalId = item.collectionExternalId ?? item.externalId;
// Convert Notion database to an empty page with "pages in database" as its children.
if (item.type === PageType.Database) {
const { pages, ...databaseInfo } = await client.fetchDatabase(
item.externalId
);
try {
// Convert Notion database to an empty page with "pages in database" as its children.
if (item.type === PageType.Database) {
const { pages, ...databaseInfo } = await client.fetchDatabase(
item.externalId
);
return {
...databaseInfo,
externalId: item.externalId,
content: ProsemirrorHelper.getEmptyDocument() as ProsemirrorDoc,
collectionExternalId,
children: pages.map((page) => ({
type: page.type,
externalId: page.id,
})),
};
}
const { blocks, ...pageInfo } = await client.fetchPage(item.externalId);
return {
...databaseInfo,
...pageInfo,
externalId: item.externalId,
content: ProsemirrorHelper.getEmptyDocument() as ProsemirrorDoc,
content: NotionConverter.page({ children: blocks } as NotionPage),
collectionExternalId,
children: pages.map((page) => ({
type: page.type,
externalId: page.id,
})),
children: this.parseChildPages(blocks),
};
} catch (error) {
if (error instanceof APIResponseError) {
// Skip this page/database if it's not found or not accessible
if (
error.code === APIErrorCode.ObjectNotFound ||
error.code === APIErrorCode.Unauthorized
) {
Logger.warn(
`Skipping Notion ${
item.type === PageType.Database ? "database" : "page"
} ${item.externalId} - Error code: ${error.code} - ${error.message}`
);
return null;
}
}
// Re-throw other errors to be handled by the parent try/catch
throw error;
}
const { blocks, ...pageInfo } = await client.fetchPage(item.externalId);
return {
...pageInfo,
externalId: item.externalId,
content: NotionConverter.page({ children: blocks } as NotionPage),
collectionExternalId,
children: this.parseChildPages(blocks),
};
}
/**

View File

@@ -304,6 +304,15 @@ export default abstract class ImportsProcessor<
const output = outputMap[externalId];
// Skip this item if it has no output (likely due to an error during processing)
if (!output) {
Logger.debug(
"processor",
`Skipping item with no output: ${externalId}`
);
continue;
}
const collectionItem = importInput[externalId];
const attachments = await Attachment.findAll({
@@ -444,7 +453,7 @@ export default abstract class ImportsProcessor<
importInput: Record<string, ImportInput<any>[number]>;
actorId: string;
}): ProsemirrorDoc {
// special case when the doc content is empty
// special case when the doc content is empty.
if (!content.content.length) {
return content;
}

View File

@@ -170,7 +170,7 @@ export default abstract class APIImportTask<
await importTask.save({ transaction });
const associatedImport = importTask.import;
associatedImport.documentCount += importTask.input.length;
associatedImport.documentCount += taskOutputWithReplacements.length;
await associatedImport.saveWithCtx(
createContext({
user: associatedImport.createdBy,