From 8f6078fb837a90cab3d07d2b392edc68e0bc179f Mon Sep 17 00:00:00 2001 From: dat-devuap Date: Thu, 23 Jan 2025 10:32:22 +0700 Subject: [PATCH 01/14] feat(notion database): add api to view database structure --- src/api-docs/openAPIDocumentGenerator.ts | 2 + .../notionDatabase/notionDatabaseModel.ts | 19 ++++ .../notionDatabase/notionDatabaseRouter.ts | 107 ++++++++++++++++++ src/server.ts | 3 + 4 files changed, 131 insertions(+) create mode 100644 src/routes/notionDatabase/notionDatabaseModel.ts create mode 100644 src/routes/notionDatabase/notionDatabaseRouter.ts diff --git a/src/api-docs/openAPIDocumentGenerator.ts b/src/api-docs/openAPIDocumentGenerator.ts index 40a6788..b430210 100644 --- a/src/api-docs/openAPIDocumentGenerator.ts +++ b/src/api-docs/openAPIDocumentGenerator.ts @@ -2,6 +2,7 @@ import { OpenApiGeneratorV3, OpenAPIRegistry } from '@asteasolutions/zod-to-open import { excelGeneratorRegistry } from '@/routes/excelGenerator/excelGeneratorRouter'; import { healthCheckRegistry } from '@/routes/healthCheck/healthCheckRouter'; +import { notionDatabaseRegistry } from '@/routes/notionDatabase/notionDatabaseRouter'; import { powerpointGeneratorRegistry } from '@/routes/powerpointGenerator/powerpointGeneratorRouter'; import { articleReaderRegistry } from '@/routes/webPageReader/webPageReaderRouter'; import { wordGeneratorRegistry } from '@/routes/wordGenerator/wordGeneratorRouter'; @@ -15,6 +16,7 @@ export function generateOpenAPIDocument() { powerpointGeneratorRegistry, wordGeneratorRegistry, excelGeneratorRegistry, + notionDatabaseRegistry, ]); const generator = new OpenApiGeneratorV3(registry.definitions); diff --git a/src/routes/notionDatabase/notionDatabaseModel.ts b/src/routes/notionDatabase/notionDatabaseModel.ts new file mode 100644 index 0000000..85ec152 --- /dev/null +++ b/src/routes/notionDatabase/notionDatabaseModel.ts @@ -0,0 +1,19 @@ +import { extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi'; +import { z } from 'zod'; + +extendZodWithOpenApi(z); + +// Define Notion Database Structure Reader +export type NotionDatabaseStructureViewerResponse = z.infer; +export const NotionDatabaseStructureViewerResponseSchema = z.object({}); +// Request Body Schema +export const NotionDatabaseStructureViewerRequestBodySchema = z.object({ + databaseId: z.string().openapi({ + description: 'The ID of the Notion database whose structure is being viewed.', + }), + notionApiKey: z.string().openapi({ + description: + 'The Notion API Key getting from Notion Integration Page at https://www.notion.so/profile/integrations', + }), +}); +export type NotionDatabaseStructureViewerRequestBody = z.infer; diff --git a/src/routes/notionDatabase/notionDatabaseRouter.ts b/src/routes/notionDatabase/notionDatabaseRouter.ts new file mode 100644 index 0000000..815e0b8 --- /dev/null +++ b/src/routes/notionDatabase/notionDatabaseRouter.ts @@ -0,0 +1,107 @@ +import { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi'; +import express, { Request, Response, Router } from 'express'; +import { StatusCodes } from 'http-status-codes'; + +import { createApiRequestBody } from '@/api-docs/openAPIRequestBuilders'; +import { createApiResponse } from '@/api-docs/openAPIResponseBuilders'; +import { ResponseStatus, ServiceResponse } from '@/common/models/serviceResponse'; +import { handleServiceResponse } from '@/common/utils/httpHandlers'; + +import { + NotionDatabaseStructureViewerRequestBodySchema, + NotionDatabaseStructureViewerResponseSchema, +} from './notionDatabaseModel'; +export const COMPRESS = true; +export const notionDatabaseRegistry = new OpenAPIRegistry(); +notionDatabaseRegistry.register('Notion Database Structure Viewer', NotionDatabaseStructureViewerResponseSchema); +notionDatabaseRegistry.registerPath({ + method: 'post', + path: '/notion-database/view-structure', + tags: ['Notion Database'], + request: { + body: createApiRequestBody(NotionDatabaseStructureViewerRequestBodySchema, 'application/json'), + }, + responses: createApiResponse(NotionDatabaseStructureViewerResponseSchema, 'Success'), +}); + +// Helper to fetch the database structure +async function fetchDatabaseStructure(databaseId: string, apiKey: string) { + // Headers for Notion API requests + const headers = { + Authorization: `Bearer ${apiKey}`, + 'Content-Type': 'application/json', + 'Notion-Version': '2022-06-28', // or the version you are using + }; + try { + const response = await fetch(`https://api.notion.com/v1/databases/${databaseId}`, { + method: 'GET', + headers: headers, + }); + + if (!response.ok) { + throw new Error(`Error fetching database structure: ${response.statusText}`); + } + + const data = await response.json(); + console.log(JSON.stringify(data)); + return data; + } catch (error: any) { + throw new Error(`Failed to fetch database structure: ${error.message}`); + } +} + +export const notionDatabaseRouter: Router = (() => { + const router = express.Router(); + + router.post('/view-structure', async (_req: Request, res: Response) => { + const { notionApiKey, databaseId } = _req.body; + if (!notionApiKey) { + const validateServiceResponse = new ServiceResponse( + ResponseStatus.Failed, + '[Validation Error] Notion Key is required!', + 'Please make sure you have sent the Notion Key from TypingMind.', + StatusCodes.BAD_REQUEST + ); + return handleServiceResponse(validateServiceResponse, res); + } + + if (!databaseId) { + const validateServiceResponse = new ServiceResponse( + ResponseStatus.Failed, + '[Validation Error] Database ID is required!', + 'Please make sure you have sent the Database ID from TypingMind.', + StatusCodes.BAD_REQUEST + ); + return handleServiceResponse(validateServiceResponse, res); + } + + try { + const dbStructure = await fetchDatabaseStructure(databaseId, notionApiKey); + const result = { + databaseId: databaseId, + structure: dbStructure.properties, + }; + const serviceResponse = new ServiceResponse( + ResponseStatus.Success, + 'Structure retrieved successfully', + result, + StatusCodes.OK + ); + return handleServiceResponse(serviceResponse, res); + } catch (error) { + const errorMessage = (error as Error).message; + let responseObject = ''; + if (errorMessage.includes('')) { + responseObject = `Sorry, we couldn't get the database structure.`; + } + const errorServiceResponse = new ServiceResponse( + ResponseStatus.Failed, + `Error ${errorMessage}`, + responseObject, + StatusCodes.INTERNAL_SERVER_ERROR + ); + return handleServiceResponse(errorServiceResponse, res); + } + }); + return router; +})(); diff --git a/src/server.ts b/src/server.ts index 0da49de..90d8872 100644 --- a/src/server.ts +++ b/src/server.ts @@ -11,6 +11,7 @@ import requestLogger from '@/common/middleware/requestLogger'; import { healthCheckRouter } from '@/routes/healthCheck/healthCheckRouter'; import { excelGeneratorRouter } from './routes/excelGenerator/excelGeneratorRouter'; +import { notionDatabaseRouter } from './routes/notionDatabase/notionDatabaseRouter'; import { powerpointGeneratorRouter } from './routes/powerpointGenerator/powerpointGeneratorRouter'; import { webPageReaderRouter } from './routes/webPageReader/webPageReaderRouter'; import { wordGeneratorRouter } from './routes/wordGenerator/wordGeneratorRouter'; @@ -42,6 +43,8 @@ app.use('/web-page-reader', webPageReaderRouter); app.use('/powerpoint-generator', powerpointGeneratorRouter); app.use('/word-generator', wordGeneratorRouter); app.use('/excel-generator', excelGeneratorRouter); +app.use('/notion-database', notionDatabaseRouter); + // Swagger UI app.use(openAPIRouter); From b425326ac35f73462f864b1e4687b3bdc4f7b679 Mon Sep 17 00:00:00 2001 From: dat-devuap Date: Fri, 24 Jan 2025 12:12:11 +0700 Subject: [PATCH 02/14] feat(notion database crud): add api handle creating page in the database --- .../notionDatabase/notionDatabaseModel.ts | 16 ++ .../notionDatabase/notionDatabaseRouter.ts | 171 +++++++++++++++++- 2 files changed, 184 insertions(+), 3 deletions(-) diff --git a/src/routes/notionDatabase/notionDatabaseModel.ts b/src/routes/notionDatabase/notionDatabaseModel.ts index 85ec152..957bffd 100644 --- a/src/routes/notionDatabase/notionDatabaseModel.ts +++ b/src/routes/notionDatabase/notionDatabaseModel.ts @@ -17,3 +17,19 @@ export const NotionDatabaseStructureViewerRequestBodySchema = z.object({ }), }); export type NotionDatabaseStructureViewerRequestBody = z.infer; + +// Define Notion Database CRUD +export type NotionDatabaseCreatePageResponse = z.infer; +export const NotionDatabaseCreatePageResponseSchema = z.object({}); +// Request Body Schema +export const NotionDatabaseCreatePageRequestBodySchema = z.object({ + databaseId: z.string().openapi({ + description: 'The ID of the Notion database whose structure is being viewed.', + }), + notionApiKey: z.string().openapi({ + description: + 'The Notion API Key getting from Notion Integration Page at https://www.notion.so/profile/integrations', + }), + properties: z.array(z.object({})), +}); +export type NotionDatabaseCreatePageRequestBody = z.infer; diff --git a/src/routes/notionDatabase/notionDatabaseRouter.ts b/src/routes/notionDatabase/notionDatabaseRouter.ts index 815e0b8..a8764da 100644 --- a/src/routes/notionDatabase/notionDatabaseRouter.ts +++ b/src/routes/notionDatabase/notionDatabaseRouter.ts @@ -8,12 +8,13 @@ import { ResponseStatus, ServiceResponse } from '@/common/models/serviceResponse import { handleServiceResponse } from '@/common/utils/httpHandlers'; import { + NotionDatabaseCreatePageRequestBodySchema, NotionDatabaseStructureViewerRequestBodySchema, NotionDatabaseStructureViewerResponseSchema, } from './notionDatabaseModel'; export const COMPRESS = true; export const notionDatabaseRegistry = new OpenAPIRegistry(); -notionDatabaseRegistry.register('Notion Database Structure Viewer', NotionDatabaseStructureViewerResponseSchema); +notionDatabaseRegistry.register('Notion Database', NotionDatabaseStructureViewerResponseSchema); notionDatabaseRegistry.registerPath({ method: 'post', path: '/notion-database/view-structure', @@ -24,16 +25,29 @@ notionDatabaseRegistry.registerPath({ responses: createApiResponse(NotionDatabaseStructureViewerResponseSchema, 'Success'), }); +notionDatabaseRegistry.registerPath({ + method: 'post', + path: '/notion-database/create-page', + tags: ['Notion Database'], + request: { + body: createApiRequestBody(NotionDatabaseCreatePageRequestBodySchema, 'application/json'), + }, + responses: createApiResponse(NotionDatabaseCreatePageRequestBodySchema, 'Success'), +}); + +const NOTION_API_URL = 'https://api.notion.com/v1'; +const NOTION_VERSION = '2022-06-28'; + // Helper to fetch the database structure async function fetchDatabaseStructure(databaseId: string, apiKey: string) { // Headers for Notion API requests const headers = { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json', - 'Notion-Version': '2022-06-28', // or the version you are using + 'Notion-Version': NOTION_VERSION, // or the version you are using }; try { - const response = await fetch(`https://api.notion.com/v1/databases/${databaseId}`, { + const response = await fetch(`${NOTION_API_URL}/databases/${databaseId}`, { method: 'GET', headers: headers, }); @@ -50,6 +64,106 @@ async function fetchDatabaseStructure(databaseId: string, apiKey: string) { } } +function mapNotionPropertyRequestBody(properties: any[] = []) { + // Construct the properties object from the notionProperties array + const notionProperties: any = {}; + properties.forEach((property: any) => { + const { propertyName, propertyType, value } = property; + + // Map each property type to the appropriate format for the Notion API + switch (propertyType) { + case 'title': + case 'rich_text': + notionProperties[propertyName] = { + [propertyType]: value.map((item: any) => ({ + type: 'text', + text: { content: item.text.content }, + annotations: item.annotations, + })), + }; + break; + case 'number': + notionProperties[propertyName] = { + number: value, + }; + break; + case 'select': + case 'status': + notionProperties[propertyName] = { + [propertyType]: { + name: value.name, + }, + }; + break; + case 'multi_select': + notionProperties[propertyName] = { + multi_select: value.map((item: any) => ({ + name: item.name, + })), + }; + break; + case 'date': + notionProperties[propertyName] = { + date: { + start: value.start, + end: value.end || null, + time_zone: value.time_zone || null, + }, + }; + break; + case 'url': + notionProperties[propertyName] = { + url: value, + }; + break; + case 'email': + notionProperties[propertyName] = { + email: value, + }; + break; + case 'phone_number': + notionProperties[propertyName] = { + phone_number: value, + }; + break; + default: + throw new Error(`Unknown property type: ${propertyType}`); + } + }); + + return notionProperties; +} + +async function createPageInNotionDatabase(apiKey: string, databaseId: string, properties: object) { + // Headers for Notion API requests + const headers = { + Authorization: `Bearer ${apiKey}`, + 'Content-Type': 'application/json', + 'Notion-Version': NOTION_VERSION, + }; + const requestBody = { + parent: { database_id: databaseId }, + properties: properties, + }; + + try { + const response = await fetch(`${NOTION_API_URL}/pages`, { + method: 'POST', + headers: headers, + body: JSON.stringify(requestBody), + }); + + if (!response.ok) { + throw new Error(`Error adding new page to database: ${response.statusText}`); + } + + const data = await response.json(); + return data; + } catch (error: any) { + throw new Error(`Failed to add database to database: ${error.message}`); + } +} + export const notionDatabaseRouter: Router = (() => { const router = express.Router(); @@ -91,6 +205,7 @@ export const notionDatabaseRouter: Router = (() => { } catch (error) { const errorMessage = (error as Error).message; let responseObject = ''; + ``; if (errorMessage.includes('')) { responseObject = `Sorry, we couldn't get the database structure.`; } @@ -103,5 +218,55 @@ export const notionDatabaseRouter: Router = (() => { return handleServiceResponse(errorServiceResponse, res); } }); + + router.post('/create-page', async (_req: Request, res: Response) => { + const { notionApiKey, databaseId, properties } = _req.body; + + if (!notionApiKey) { + const validateServiceResponse = new ServiceResponse( + ResponseStatus.Failed, + '[Validation Error] Notion Key is required!', + 'Please make sure you have sent the Notion Key from TypingMind.', + StatusCodes.BAD_REQUEST + ); + return handleServiceResponse(validateServiceResponse, res); + } + + if (!databaseId) { + const validateServiceResponse = new ServiceResponse( + ResponseStatus.Failed, + '[Validation Error] Database ID is required!', + 'Please make sure you have sent the Database ID from TypingMind.', + StatusCodes.BAD_REQUEST + ); + return handleServiceResponse(validateServiceResponse, res); + } + + const notionProperties = mapNotionPropertyRequestBody(properties); + try { + const result = await createPageInNotionDatabase(notionApiKey, databaseId, notionProperties); + const serviceResponse = new ServiceResponse( + ResponseStatus.Success, + 'Page created successfully', + result, + StatusCodes.OK + ); + return handleServiceResponse(serviceResponse, res); + } catch (error) { + const errorMessage = (error as Error).message; + let responseObject = ''; + ``; + if (errorMessage.includes('')) { + responseObject = `Sorry, we couldn't create new page in the Notion database.`; + } + const errorServiceResponse = new ServiceResponse( + ResponseStatus.Failed, + `Error ${errorMessage}`, + responseObject, + StatusCodes.INTERNAL_SERVER_ERROR + ); + return handleServiceResponse(errorServiceResponse, res); + } + }); return router; })(); From 122cecda2a3881c1c263dcdf2ce211220725d89e Mon Sep 17 00:00:00 2001 From: dat-devuap Date: Fri, 24 Jan 2025 13:43:06 +0700 Subject: [PATCH 03/14] feat(notion database crud): do not throw error on unknown property type --- src/routes/notionDatabase/notionDatabaseRouter.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/routes/notionDatabase/notionDatabaseRouter.ts b/src/routes/notionDatabase/notionDatabaseRouter.ts index a8764da..78b655d 100644 --- a/src/routes/notionDatabase/notionDatabaseRouter.ts +++ b/src/routes/notionDatabase/notionDatabaseRouter.ts @@ -127,7 +127,8 @@ function mapNotionPropertyRequestBody(properties: any[] = []) { }; break; default: - throw new Error(`Unknown property type: ${propertyType}`); + console.info(`Unknown property type: ${propertyType}`); + break; } }); From 41c1d21d8c17294a6d3cf6f758c21258bba73301 Mon Sep 17 00:00:00 2001 From: dat-devuap Date: Fri, 24 Jan 2025 17:40:54 +0700 Subject: [PATCH 04/14] feat(notion database crud): add api handle updating page in the database --- .../notionDatabase/notionDatabaseModel.ts | 18 +++- .../notionDatabase/notionDatabaseRouter.ts | 95 ++++++++++++++++++- 2 files changed, 111 insertions(+), 2 deletions(-) diff --git a/src/routes/notionDatabase/notionDatabaseModel.ts b/src/routes/notionDatabase/notionDatabaseModel.ts index 957bffd..457385e 100644 --- a/src/routes/notionDatabase/notionDatabaseModel.ts +++ b/src/routes/notionDatabase/notionDatabaseModel.ts @@ -18,7 +18,7 @@ export const NotionDatabaseStructureViewerRequestBodySchema = z.object({ }); export type NotionDatabaseStructureViewerRequestBody = z.infer; -// Define Notion Database CRUD +// Define Notion Database Create export type NotionDatabaseCreatePageResponse = z.infer; export const NotionDatabaseCreatePageResponseSchema = z.object({}); // Request Body Schema @@ -33,3 +33,19 @@ export const NotionDatabaseCreatePageRequestBodySchema = z.object({ properties: z.array(z.object({})), }); export type NotionDatabaseCreatePageRequestBody = z.infer; + +// Define Notion Database Update +export type NotionDatabaseUpdatePageResponse = z.infer; +export const NotionDatabaseUpdatePageResponseSchema = z.object({}); +// Request Body Schema +export const NotionDatabaseUpdatePageRequestBodySchema = z.object({ + pageId: z.string().openapi({ + description: 'The ID of the Notion Page whose structure is being viewed.', + }), + notionApiKey: z.string().openapi({ + description: + 'The Notion API Key getting from Notion Integration Page at https://www.notion.so/profile/integrations', + }), + properties: z.array(z.object({})), +}); +export type NotionDatabaseUpdatePageRequestBody = z.infer; diff --git a/src/routes/notionDatabase/notionDatabaseRouter.ts b/src/routes/notionDatabase/notionDatabaseRouter.ts index 78b655d..168a11f 100644 --- a/src/routes/notionDatabase/notionDatabaseRouter.ts +++ b/src/routes/notionDatabase/notionDatabaseRouter.ts @@ -9,8 +9,11 @@ import { handleServiceResponse } from '@/common/utils/httpHandlers'; import { NotionDatabaseCreatePageRequestBodySchema, + NotionDatabaseCreatePageResponseSchema, NotionDatabaseStructureViewerRequestBodySchema, NotionDatabaseStructureViewerResponseSchema, + NotionDatabaseUpdatePageRequestBodySchema, + NotionDatabaseUpdatePageResponseSchema, } from './notionDatabaseModel'; export const COMPRESS = true; export const notionDatabaseRegistry = new OpenAPIRegistry(); @@ -32,7 +35,17 @@ notionDatabaseRegistry.registerPath({ request: { body: createApiRequestBody(NotionDatabaseCreatePageRequestBodySchema, 'application/json'), }, - responses: createApiResponse(NotionDatabaseCreatePageRequestBodySchema, 'Success'), + responses: createApiResponse(NotionDatabaseCreatePageResponseSchema, 'Success'), +}); + +notionDatabaseRegistry.registerPath({ + method: 'post', + path: '/notion-database/update-page', + tags: ['Notion Database'], + request: { + body: createApiRequestBody(NotionDatabaseUpdatePageRequestBodySchema, 'application/json'), + }, + responses: createApiResponse(NotionDatabaseUpdatePageResponseSchema, 'Success'), }); const NOTION_API_URL = 'https://api.notion.com/v1'; @@ -165,6 +178,36 @@ async function createPageInNotionDatabase(apiKey: string, databaseId: string, pr } } +async function updatePageInNotionDatabase(apiKey: string, pageId: string, properties: object) { + // Headers for Notion API requests + const headers = { + Authorization: `Bearer ${apiKey}`, + 'Content-Type': 'application/json', + 'Notion-Version': NOTION_VERSION, + }; + + const requestBody = { + properties: properties, + }; + + try { + const response = await fetch(`${NOTION_API_URL}/pages/${pageId}`, { + method: 'PATCH', + headers: headers, + body: JSON.stringify(requestBody), + }); + + if (!response.ok) { + throw new Error(`Error adding update page: ${response.statusText}`); + } + + const data = await response.json(); + return data; + } catch (error: any) { + throw new Error(`Failed to update page: ${error.message}`); + } +} + export const notionDatabaseRouter: Router = (() => { const router = express.Router(); @@ -269,5 +312,55 @@ export const notionDatabaseRouter: Router = (() => { return handleServiceResponse(errorServiceResponse, res); } }); + + router.post('/update-page', async (_req: Request, res: Response) => { + const { notionApiKey, pageId, properties } = _req.body; + + if (!notionApiKey) { + const validateServiceResponse = new ServiceResponse( + ResponseStatus.Failed, + '[Validation Error] Notion Key is required!', + 'Please make sure you have sent the Notion Key from TypingMind.', + StatusCodes.BAD_REQUEST + ); + return handleServiceResponse(validateServiceResponse, res); + } + + if (!pageId) { + const validateServiceResponse = new ServiceResponse( + ResponseStatus.Failed, + '[Validation Error] Page ID is required!', + 'Please make sure you have sent the Page ID from TypingMind.', + StatusCodes.BAD_REQUEST + ); + return handleServiceResponse(validateServiceResponse, res); + } + + const notionProperties = mapNotionPropertyRequestBody(properties); + try { + const result = await updatePageInNotionDatabase(notionApiKey, pageId, notionProperties); + const serviceResponse = new ServiceResponse( + ResponseStatus.Success, + 'Page updated successfully', + result, + StatusCodes.OK + ); + return handleServiceResponse(serviceResponse, res); + } catch (error) { + const errorMessage = (error as Error).message; + let responseObject = ''; + if (errorMessage.includes('')) { + responseObject = `Sorry, we couldn't update the page!`; + } + const errorServiceResponse = new ServiceResponse( + ResponseStatus.Failed, + `Error ${errorMessage}`, + responseObject, + StatusCodes.INTERNAL_SERVER_ERROR + ); + return handleServiceResponse(errorServiceResponse, res); + } + }); + return router; })(); From b6971755cb31605b1f60f60b0f7161c56e15e945 Mon Sep 17 00:00:00 2001 From: dat-devuap Date: Fri, 24 Jan 2025 19:43:33 +0700 Subject: [PATCH 05/14] feat(notion database crud): add api handle removing page in the database --- .../notionDatabase/notionDatabaseModel.ts | 15 +++ .../notionDatabase/notionDatabaseRouter.ts | 92 ++++++++++++++++++- 2 files changed, 106 insertions(+), 1 deletion(-) diff --git a/src/routes/notionDatabase/notionDatabaseModel.ts b/src/routes/notionDatabase/notionDatabaseModel.ts index 457385e..c8fe270 100644 --- a/src/routes/notionDatabase/notionDatabaseModel.ts +++ b/src/routes/notionDatabase/notionDatabaseModel.ts @@ -49,3 +49,18 @@ export const NotionDatabaseUpdatePageRequestBodySchema = z.object({ properties: z.array(z.object({})), }); export type NotionDatabaseUpdatePageRequestBody = z.infer; + +// Define Notion Database Delete +export type NotionDatabaseArchivePageResponse = z.infer; +export const NotionDatabaseArchivePageResponseSchema = z.object({}); +// Request Body Schema +export const NotionDatabaseArchivePageRequestBodySchema = z.object({ + pageId: z.string().openapi({ + description: 'The ID of the Notion Page whose structure is being viewed.', + }), + notionApiKey: z.string().openapi({ + description: + 'The Notion API Key getting from Notion Integration Page at https://www.notion.so/profile/integrations', + }), +}); +export type NotionDatabaseArchivePageRequestBody = z.infer; diff --git a/src/routes/notionDatabase/notionDatabaseRouter.ts b/src/routes/notionDatabase/notionDatabaseRouter.ts index 168a11f..b053da4 100644 --- a/src/routes/notionDatabase/notionDatabaseRouter.ts +++ b/src/routes/notionDatabase/notionDatabaseRouter.ts @@ -8,6 +8,8 @@ import { ResponseStatus, ServiceResponse } from '@/common/models/serviceResponse import { handleServiceResponse } from '@/common/utils/httpHandlers'; import { + NotionDatabaseArchivePageRequestBodySchema, + NotionDatabaseArchivePageResponseSchema, NotionDatabaseCreatePageRequestBodySchema, NotionDatabaseCreatePageResponseSchema, NotionDatabaseStructureViewerRequestBodySchema, @@ -39,7 +41,7 @@ notionDatabaseRegistry.registerPath({ }); notionDatabaseRegistry.registerPath({ - method: 'post', + method: 'patch', path: '/notion-database/update-page', tags: ['Notion Database'], request: { @@ -48,6 +50,16 @@ notionDatabaseRegistry.registerPath({ responses: createApiResponse(NotionDatabaseUpdatePageResponseSchema, 'Success'), }); +notionDatabaseRegistry.registerPath({ + method: 'patch', + path: '/notion-database/archive-page', + tags: ['Notion Database'], + request: { + body: createApiRequestBody(NotionDatabaseArchivePageRequestBodySchema, 'application/json'), + }, + responses: createApiResponse(NotionDatabaseArchivePageResponseSchema, 'Success'), +}); + const NOTION_API_URL = 'https://api.notion.com/v1'; const NOTION_VERSION = '2022-06-28'; @@ -208,6 +220,36 @@ async function updatePageInNotionDatabase(apiKey: string, pageId: string, proper } } +async function archivePageInNotionDatabase(apiKey: string, pageId: string) { + // Headers for Notion API requests + const headers = { + Authorization: `Bearer ${apiKey}`, + 'Content-Type': 'application/json', + 'Notion-Version': NOTION_VERSION, + }; + + const requestBody = { + archived: true, + }; + + try { + const response = await fetch(`${NOTION_API_URL}/pages/${pageId}`, { + method: 'PATCH', + headers: headers, + body: JSON.stringify(requestBody), + }); + + if (!response.ok) { + throw new Error(`Error removing page: ${response.statusText}`); + } + + const data = await response.json(); + return data; + } catch (error: any) { + throw new Error(`Failed to removing page: ${error.message}`); + } +} + export const notionDatabaseRouter: Router = (() => { const router = express.Router(); @@ -362,5 +404,53 @@ export const notionDatabaseRouter: Router = (() => { } }); + router.post('/archive-page', async (_req: Request, res: Response) => { + const { notionApiKey, pageId } = _req.body; + + if (!notionApiKey) { + const validateServiceResponse = new ServiceResponse( + ResponseStatus.Failed, + '[Validation Error] Notion Key is required!', + 'Please make sure you have sent the Notion Key from TypingMind.', + StatusCodes.BAD_REQUEST + ); + return handleServiceResponse(validateServiceResponse, res); + } + + if (!pageId) { + const validateServiceResponse = new ServiceResponse( + ResponseStatus.Failed, + '[Validation Error] Page ID is required!', + 'Please make sure you have sent the Page ID from TypingMind.', + StatusCodes.BAD_REQUEST + ); + return handleServiceResponse(validateServiceResponse, res); + } + + try { + const result = await archivePageInNotionDatabase(notionApiKey, pageId); + const serviceResponse = new ServiceResponse( + ResponseStatus.Success, + 'Page removed successfully', + result, + StatusCodes.OK + ); + return handleServiceResponse(serviceResponse, res); + } catch (error) { + const errorMessage = (error as Error).message; + let responseObject = ''; + if (errorMessage.includes('')) { + responseObject = `Sorry, we couldn't remove the page!`; + } + const errorServiceResponse = new ServiceResponse( + ResponseStatus.Failed, + `Error ${errorMessage}`, + responseObject, + StatusCodes.INTERNAL_SERVER_ERROR + ); + return handleServiceResponse(errorServiceResponse, res); + } + }); + return router; })(); From 25c09d15e40b32c48deabece799f64de19c94fb1 Mon Sep 17 00:00:00 2001 From: dat-devuap Date: Sat, 25 Jan 2025 00:11:27 +0700 Subject: [PATCH 06/14] feat(notion database crud): add api handle querying pages in the database --- .../notionDatabase/notionDatabaseModel.ts | 19 ++++ .../notionDatabase/notionDatabaseRouter.ts | 100 ++++++++++++++++++ 2 files changed, 119 insertions(+) diff --git a/src/routes/notionDatabase/notionDatabaseModel.ts b/src/routes/notionDatabase/notionDatabaseModel.ts index c8fe270..5f24194 100644 --- a/src/routes/notionDatabase/notionDatabaseModel.ts +++ b/src/routes/notionDatabase/notionDatabaseModel.ts @@ -64,3 +64,22 @@ export const NotionDatabaseArchivePageRequestBodySchema = z.object({ }), }); export type NotionDatabaseArchivePageRequestBody = z.infer; + +// Define Notion Database Query +export type NotionDatabaseQueryPageResponse = z.infer; +export const NotionDatabaseQueryPageResponseSchema = z.object({}); +// Request Body Schema +export const NotionDatabaseQueryPageRequestBodySchema = z.object({ + databaseId: z.string().openapi({ + description: 'The ID of the Notion Database whose structure is being viewed.', + }), + notionApiKey: z.string().openapi({ + description: + 'The Notion API Key getting from Notion Integration Page at https://www.notion.so/profile/integrations', + }), + query: z.object({}), + sorts: z.array(z.object({})), + pageSize: z.number().optional(), + startCursor: z.any().optional(), +}); +export type NotionDatabaseQueryPageRequestBody = z.infer; diff --git a/src/routes/notionDatabase/notionDatabaseRouter.ts b/src/routes/notionDatabase/notionDatabaseRouter.ts index b053da4..6cecd12 100644 --- a/src/routes/notionDatabase/notionDatabaseRouter.ts +++ b/src/routes/notionDatabase/notionDatabaseRouter.ts @@ -12,6 +12,8 @@ import { NotionDatabaseArchivePageResponseSchema, NotionDatabaseCreatePageRequestBodySchema, NotionDatabaseCreatePageResponseSchema, + NotionDatabaseQueryPageRequestBodySchema, + NotionDatabaseQueryPageResponseSchema, NotionDatabaseStructureViewerRequestBodySchema, NotionDatabaseStructureViewerResponseSchema, NotionDatabaseUpdatePageRequestBodySchema, @@ -60,6 +62,16 @@ notionDatabaseRegistry.registerPath({ responses: createApiResponse(NotionDatabaseArchivePageResponseSchema, 'Success'), }); +notionDatabaseRegistry.registerPath({ + method: 'post', + path: '/notion-database/query-pages', + tags: ['Notion Database'], + request: { + body: createApiRequestBody(NotionDatabaseQueryPageRequestBodySchema, 'application/json'), + }, + responses: createApiResponse(NotionDatabaseQueryPageResponseSchema, 'Success'), +}); + const NOTION_API_URL = 'https://api.notion.com/v1'; const NOTION_VERSION = '2022-06-28'; @@ -250,6 +262,46 @@ async function archivePageInNotionDatabase(apiKey: string, pageId: string) { } } +async function queryPagesInNotionDatabase( + apiKey: string, + databaseId: string, + filter: object, + sorts: any[], + pageSize: number, + startCursor: string | undefined +) { + // Headers for Notion API requests + const headers = { + Authorization: `Bearer ${apiKey}`, + 'Content-Type': 'application/json', + 'Notion-Version': NOTION_VERSION, + }; + + const requestBody = { + filter: filter, + sorts: sorts, + page_size: pageSize, + start_cursor: startCursor, + }; + + try { + const response = await fetch(`${NOTION_API_URL}/databases/${databaseId}/query`, { + method: 'POST', + headers: headers, + body: JSON.stringify(requestBody), + }); + + if (!response.ok) { + throw new Error(`Error query pages: ${response.statusText}`); + } + + const data = await response.json(); + return data; + } catch (error: any) { + throw new Error(`Failed to query pages: ${error.message}`); + } +} + export const notionDatabaseRouter: Router = (() => { const router = express.Router(); @@ -452,5 +504,53 @@ export const notionDatabaseRouter: Router = (() => { } }); + router.post('/query-pages', async (_req: Request, res: Response) => { + const { notionApiKey, databaseId, filter = {}, sorts = [], pageSize = 100, startCursor } = _req.body; + + if (!notionApiKey) { + const validateServiceResponse = new ServiceResponse( + ResponseStatus.Failed, + '[Validation Error] Notion Key is required!', + 'Please make sure you have sent the Notion Key from TypingMind.', + StatusCodes.BAD_REQUEST + ); + return handleServiceResponse(validateServiceResponse, res); + } + + if (!databaseId) { + const validateServiceResponse = new ServiceResponse( + ResponseStatus.Failed, + '[Validation Error] Database ID is required!', + 'Please make sure you have sent the Database ID from TypingMind.', + StatusCodes.BAD_REQUEST + ); + return handleServiceResponse(validateServiceResponse, res); + } + + try { + const result = await queryPagesInNotionDatabase(notionApiKey, databaseId, filter, sorts, pageSize, startCursor); + const serviceResponse = new ServiceResponse( + ResponseStatus.Success, + 'Pages query successfully', + result, + StatusCodes.OK + ); + return handleServiceResponse(serviceResponse, res); + } catch (error) { + const errorMessage = (error as Error).message; + let responseObject = ''; + if (errorMessage.includes('')) { + responseObject = `Sorry, we couldn't query the pages!`; + } + const errorServiceResponse = new ServiceResponse( + ResponseStatus.Failed, + `Error ${errorMessage}`, + responseObject, + StatusCodes.INTERNAL_SERVER_ERROR + ); + return handleServiceResponse(errorServiceResponse, res); + } + }); + return router; })(); From bf45b14e1e99a5b18fb6f1bd591e52f8642162be Mon Sep 17 00:00:00 2001 From: dat-devuap Date: Sat, 25 Jan 2025 15:27:19 +0700 Subject: [PATCH 07/14] feat(notion database crud): update mapping for checbox, status, files, and annotation rich_text --- .../notionDatabase/notionDatabaseRouter.ts | 50 +++++++++++++++++-- 1 file changed, 45 insertions(+), 5 deletions(-) diff --git a/src/routes/notionDatabase/notionDatabaseRouter.ts b/src/routes/notionDatabase/notionDatabaseRouter.ts index 6cecd12..43ed769 100644 --- a/src/routes/notionDatabase/notionDatabaseRouter.ts +++ b/src/routes/notionDatabase/notionDatabaseRouter.ts @@ -74,6 +74,13 @@ notionDatabaseRegistry.registerPath({ const NOTION_API_URL = 'https://api.notion.com/v1'; const NOTION_VERSION = '2022-06-28'; +const DEFAULT_ANNOTATIONS = { + italic: false, + bold: false, + color: 'default', + strikethrough: false, + underline: false, +}; // Helper to fetch the database structure async function fetchDatabaseStructure(databaseId: string, apiKey: string) { @@ -112,11 +119,26 @@ function mapNotionPropertyRequestBody(properties: any[] = []) { case 'title': case 'rich_text': notionProperties[propertyName] = { - [propertyType]: value.map((item: any) => ({ - type: 'text', - text: { content: item.text.content }, - annotations: item.annotations, - })), + [propertyType]: value.map((item: any) => { + const annotationConfigs = item.annotations || DEFAULT_ANNOTATIONS; + return { + type: 'text', + text: { content: item.text.content }, + annotations: { + italic: annotationConfigs.italic !== undefined ? annotationConfigs.italic : DEFAULT_ANNOTATIONS.italic, + bold: annotationConfigs.bold !== undefined ? annotationConfigs.bold : DEFAULT_ANNOTATIONS.bold, + color: annotationConfigs.color ? annotationConfigs.color : DEFAULT_ANNOTATIONS.color, + strikethrough: + annotationConfigs.strikethrough !== undefined + ? annotationConfigs.strikethrough + : DEFAULT_ANNOTATIONS.strikethrough, + underline: + annotationConfigs.underline !== undefined + ? annotationConfigs.underline + : DEFAULT_ANNOTATIONS.underline, + }, + }; + }), }; break; case 'number': @@ -163,6 +185,24 @@ function mapNotionPropertyRequestBody(properties: any[] = []) { phone_number: value, }; break; + case 'checkbox': + notionProperties[propertyName] = { + checkbox: value, + }; + break; + case 'files': + notionProperties[propertyName] = { + // Note: This verion only support external files + files: value + .filter((file: any) => file.external && file.external.url) + .map((file: any) => ({ + name: file.name, + external: { + url: file.external.url, + }, + })), + }; + break; default: console.info(`Unknown property type: ${propertyType}`); break; From 568ee56206f8044a89d96c273b9ccc8c50a04813 Mon Sep 17 00:00:00 2001 From: dat-devuap Date: Sun, 26 Jan 2025 14:34:48 +0700 Subject: [PATCH 08/14] feat(notion database query): add database structure validation --- .../notionDatabase/notionDatabaseModel.ts | 49 +++++++++++++++++++ .../notionDatabase/notionDatabaseRouter.ts | 24 ++++++--- src/routes/notionDatabase/utils.ts | 41 ++++++++++++++++ 3 files changed, 106 insertions(+), 8 deletions(-) create mode 100644 src/routes/notionDatabase/utils.ts diff --git a/src/routes/notionDatabase/notionDatabaseModel.ts b/src/routes/notionDatabase/notionDatabaseModel.ts index 5f24194..5d714bf 100644 --- a/src/routes/notionDatabase/notionDatabaseModel.ts +++ b/src/routes/notionDatabase/notionDatabaseModel.ts @@ -73,6 +73,55 @@ export const NotionDatabaseQueryPageRequestBodySchema = z.object({ databaseId: z.string().openapi({ description: 'The ID of the Notion Database whose structure is being viewed.', }), + databaseStructure: z + .array( + z.object({ + name: z.string().openapi({ + description: 'The name of the property.', + }), + type: z + .string() + .openapi({ + description: 'The type of the property.', + }) + .refine( + (value) => + [ + 'title', + 'number', + 'multi_select', + 'select', + 'checkbox', + 'url', + 'status', + 'email', + 'date', + 'files', + 'phone_number', + 'rich_text', + ].includes(value), + { + message: 'Invalid type', + } + ), + options: z + .array( + z.object({ + name: z.string().openapi({ + description: 'Name of the option.', + }), + }) + ) + .optional() + .openapi({ + description: 'List of options for select, multi-select, and status properties.', + }), + }) + ) + .openapi({ + description: + 'An array of properties from the Notion database structure, used to generate filter or sort criteria.', + }), notionApiKey: z.string().openapi({ description: 'The Notion API Key getting from Notion Integration Page at https://www.notion.so/profile/integrations', diff --git a/src/routes/notionDatabase/notionDatabaseRouter.ts b/src/routes/notionDatabase/notionDatabaseRouter.ts index 43ed769..63f9841 100644 --- a/src/routes/notionDatabase/notionDatabaseRouter.ts +++ b/src/routes/notionDatabase/notionDatabaseRouter.ts @@ -19,6 +19,7 @@ import { NotionDatabaseUpdatePageRequestBodySchema, NotionDatabaseUpdatePageResponseSchema, } from './notionDatabaseModel'; +import { validateDatabaseQueryConfig } from './utils'; export const COMPRESS = true; export const notionDatabaseRegistry = new OpenAPIRegistry(); notionDatabaseRegistry.register('Notion Database', NotionDatabaseStructureViewerResponseSchema); @@ -545,7 +546,15 @@ export const notionDatabaseRouter: Router = (() => { }); router.post('/query-pages', async (_req: Request, res: Response) => { - const { notionApiKey, databaseId, filter = {}, sorts = [], pageSize = 100, startCursor } = _req.body; + const { + notionApiKey, + databaseId, + databaseStructure = [], + filter = {}, + sorts = [], + pageSize = 100, + startCursor, + } = _req.body; if (!notionApiKey) { const validateServiceResponse = new ServiceResponse( @@ -568,6 +577,9 @@ export const notionDatabaseRouter: Router = (() => { } try { + // Validate databaseStructure against filters and sorts + validateDatabaseQueryConfig(databaseStructure, filter, sorts); + const result = await queryPagesInNotionDatabase(notionApiKey, databaseId, filter, sorts, pageSize, startCursor); const serviceResponse = new ServiceResponse( ResponseStatus.Success, @@ -578,15 +590,11 @@ export const notionDatabaseRouter: Router = (() => { return handleServiceResponse(serviceResponse, res); } catch (error) { const errorMessage = (error as Error).message; - let responseObject = ''; - if (errorMessage.includes('')) { - responseObject = `Sorry, we couldn't query the pages!`; - } const errorServiceResponse = new ServiceResponse( ResponseStatus.Failed, - `Error ${errorMessage}`, - responseObject, - StatusCodes.INTERNAL_SERVER_ERROR + `Error: ${errorMessage}`, + `Sorry, we couldn't query the pages!`, + errorMessage.includes('[Validation Error]') ? StatusCodes.BAD_REQUEST : StatusCodes.INTERNAL_SERVER_ERROR ); return handleServiceResponse(errorServiceResponse, res); } diff --git a/src/routes/notionDatabase/utils.ts b/src/routes/notionDatabase/utils.ts new file mode 100644 index 0000000..93e7e58 --- /dev/null +++ b/src/routes/notionDatabase/utils.ts @@ -0,0 +1,41 @@ +function flattenConditions(conditions: any[]): any[] { + return conditions.reduce((acc, condition) => { + if (condition.and) { + // Recursively flatten "and" conditions + return acc.concat(flattenConditions(condition.and)); + } else if (condition.or) { + // Recursively flatten "or" conditions + return acc.concat(flattenConditions(condition.or)); + } + // Base case: direct condition object + return acc.concat(condition); + }, []); +} + +export function validateDatabaseQueryConfig(databaseStructures: any[], filter: any, sorts: any[]): void { + const validPropertyNames = databaseStructures.map((property) => property.name); + + // Flatten filter conditions recursively + let flatConditions = []; + if (Object.prototype.hasOwnProperty.call(filter, 'and') || Object.prototype.hasOwnProperty.call(filter, 'or')) { + flatConditions = flattenConditions(filter.and || filter.or); + } + + // Validate flattened filter conditions + for (const condition of flatConditions) { + if (!validPropertyNames.includes(condition.property)) { + throw new Error( + `[Validation Error] The property '${condition.property}' used in filters is invalid. Make sure it matches with current database structure.` + ); + } + } + + // Validate sorts + for (const sort of sorts) { + if (!validPropertyNames.includes(sort.property)) { + throw new Error( + `[Validation Error] The property '${sort.property}' used in sorts is invalid. Make sure it matches with current database structure.` + ); + } + } +} From 1b1986cc71349aef2a083fd4eb8a15dc894748cb Mon Sep 17 00:00:00 2001 From: dat-devuap Date: Sun, 26 Jan 2025 15:39:05 +0700 Subject: [PATCH 09/14] feat(notion database modifier): add database structure validation --- .../notionDatabase/notionDatabaseRouter.ts | 36 ++++++++----------- src/routes/notionDatabase/utils.ts | 20 +++++++++++ 2 files changed, 35 insertions(+), 21 deletions(-) diff --git a/src/routes/notionDatabase/notionDatabaseRouter.ts b/src/routes/notionDatabase/notionDatabaseRouter.ts index 63f9841..87b41fc 100644 --- a/src/routes/notionDatabase/notionDatabaseRouter.ts +++ b/src/routes/notionDatabase/notionDatabaseRouter.ts @@ -19,7 +19,7 @@ import { NotionDatabaseUpdatePageRequestBodySchema, NotionDatabaseUpdatePageResponseSchema, } from './notionDatabaseModel'; -import { validateDatabaseQueryConfig } from './utils'; +import { validateDatabaseQueryConfig, validateNotionProperties } from './utils'; export const COMPRESS = true; export const notionDatabaseRegistry = new OpenAPIRegistry(); notionDatabaseRegistry.register('Notion Database', NotionDatabaseStructureViewerResponseSchema); @@ -399,7 +399,7 @@ export const notionDatabaseRouter: Router = (() => { }); router.post('/create-page', async (_req: Request, res: Response) => { - const { notionApiKey, databaseId, properties } = _req.body; + const { notionApiKey, databaseId, properties, databaseStructure = [] } = _req.body; if (!notionApiKey) { const validateServiceResponse = new ServiceResponse( @@ -420,9 +420,10 @@ export const notionDatabaseRouter: Router = (() => { ); return handleServiceResponse(validateServiceResponse, res); } - - const notionProperties = mapNotionPropertyRequestBody(properties); try { + // Validate properties before creating + validateNotionProperties(databaseStructure, properties); + const notionProperties = mapNotionPropertyRequestBody(properties); const result = await createPageInNotionDatabase(notionApiKey, databaseId, notionProperties); const serviceResponse = new ServiceResponse( ResponseStatus.Success, @@ -433,23 +434,18 @@ export const notionDatabaseRouter: Router = (() => { return handleServiceResponse(serviceResponse, res); } catch (error) { const errorMessage = (error as Error).message; - let responseObject = ''; - ``; - if (errorMessage.includes('')) { - responseObject = `Sorry, we couldn't create new page in the Notion database.`; - } const errorServiceResponse = new ServiceResponse( ResponseStatus.Failed, - `Error ${errorMessage}`, - responseObject, - StatusCodes.INTERNAL_SERVER_ERROR + `Error: ${errorMessage}`, + `Sorry, we couldn't create new page in the Notion database!`, + errorMessage.includes('[Validation Error]') ? StatusCodes.BAD_REQUEST : StatusCodes.INTERNAL_SERVER_ERROR ); return handleServiceResponse(errorServiceResponse, res); } }); router.post('/update-page', async (_req: Request, res: Response) => { - const { notionApiKey, pageId, properties } = _req.body; + const { notionApiKey, pageId, properties, databaseStructure = [] } = _req.body; if (!notionApiKey) { const validateServiceResponse = new ServiceResponse( @@ -471,8 +467,10 @@ export const notionDatabaseRouter: Router = (() => { return handleServiceResponse(validateServiceResponse, res); } - const notionProperties = mapNotionPropertyRequestBody(properties); try { + // Validate properties before creating + validateNotionProperties(databaseStructure, properties); + const notionProperties = mapNotionPropertyRequestBody(properties); const result = await updatePageInNotionDatabase(notionApiKey, pageId, notionProperties); const serviceResponse = new ServiceResponse( ResponseStatus.Success, @@ -483,15 +481,11 @@ export const notionDatabaseRouter: Router = (() => { return handleServiceResponse(serviceResponse, res); } catch (error) { const errorMessage = (error as Error).message; - let responseObject = ''; - if (errorMessage.includes('')) { - responseObject = `Sorry, we couldn't update the page!`; - } const errorServiceResponse = new ServiceResponse( ResponseStatus.Failed, - `Error ${errorMessage}`, - responseObject, - StatusCodes.INTERNAL_SERVER_ERROR + `Error: ${errorMessage}`, + `Sorry, we couldn't update the page!!`, + errorMessage.includes('[Validation Error]') ? StatusCodes.BAD_REQUEST : StatusCodes.INTERNAL_SERVER_ERROR ); return handleServiceResponse(errorServiceResponse, res); } diff --git a/src/routes/notionDatabase/utils.ts b/src/routes/notionDatabase/utils.ts index 93e7e58..92abdaf 100644 --- a/src/routes/notionDatabase/utils.ts +++ b/src/routes/notionDatabase/utils.ts @@ -39,3 +39,23 @@ export function validateDatabaseQueryConfig(databaseStructures: any[], filter: a } } } + +export function validateNotionProperties(databaseStructure: any[], properties: any[]): void { + const validProperties = new Map(databaseStructure.map((prop) => [prop.name, prop.type])); + + properties.forEach((property) => { + const { propertyName, propertyType } = property; + + if (!validProperties.has(propertyName)) { + throw new Error( + `[Validation Error] Property '${propertyName}' does not exist in the database structure. Make sure it matches with the current database structure.` + ); + } + + if (validProperties.get(propertyName) !== propertyType) { + throw new Error( + `[Validation Error] Property '${propertyName}' has an invalid type '${propertyType}'. Expected type is '${validProperties.get(propertyName)}'.` + ); + } + }); +} From 8d8b53f41438cefeed43355ed9e8770294f3f16d Mon Sep 17 00:00:00 2001 From: dat-devuap Date: Mon, 27 Jan 2025 09:39:25 +0700 Subject: [PATCH 10/14] feat(notion database): using Notion SDK for JavaScript --- package-lock.json | 73 +++++++++ package.json | 1 + .../notionDatabase/notionDatabaseRouter.ts | 142 +++--------------- 3 files changed, 97 insertions(+), 119 deletions(-) diff --git a/package-lock.json b/package-lock.json index a507c15..cdbcfd8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@asteasolutions/zod-to-openapi": "^7.3.0", "@mozilla/readability": "^0.5.0", + "@notionhq/client": "^2.2.15", "@types/got": "^9.6.12", "@types/jsdom": "^21.1.6", "body-parser": "^1.20.2", @@ -1320,6 +1321,18 @@ "node": ">= 8" } }, + "node_modules/@notionhq/client": { + "version": "2.2.15", + "resolved": "https://registry.npmjs.org/@notionhq/client/-/client-2.2.15.tgz", + "integrity": "sha512-XhdSY/4B1D34tSco/GION+23GMjaS9S2zszcqYkMHo8RcWInymF6L1x+Gk7EmHdrSxNFva2WM8orhC4BwQCwgw==", + "dependencies": { + "@types/node-fetch": "^2.5.10", + "node-fetch": "^2.6.1" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@octokit/auth-token": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz", @@ -1991,6 +2004,28 @@ "integrity": "sha512-0ikrnug3/IyneSHqCBeslAhlK2aBfYek1fGo4bP4QnZPmiqSGRK+Oy7ZMisLWkesffJvQ1cqAcBnJC+8+nxIAg==", "dev": true }, + "node_modules/@types/node-fetch": { + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.12.tgz", + "integrity": "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/node-fetch/node_modules/form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/@types/normalize-package-data": { "version": "2.4.4", "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", @@ -7928,6 +7963,44 @@ "node": ">=6.0.0" } }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/normalize-package-data": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.2.tgz", diff --git a/package.json b/package.json index 17fa854..ab5fde9 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "dependencies": { "@asteasolutions/zod-to-openapi": "^7.3.0", "@mozilla/readability": "^0.5.0", + "@notionhq/client": "^2.2.15", "@types/got": "^9.6.12", "@types/jsdom": "^21.1.6", "body-parser": "^1.20.2", diff --git a/src/routes/notionDatabase/notionDatabaseRouter.ts b/src/routes/notionDatabase/notionDatabaseRouter.ts index 87b41fc..6cdd43f 100644 --- a/src/routes/notionDatabase/notionDatabaseRouter.ts +++ b/src/routes/notionDatabase/notionDatabaseRouter.ts @@ -1,4 +1,5 @@ import { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi'; +import { Client as NotionClient } from '@notionhq/client'; import express, { Request, Response, Router } from 'express'; import { StatusCodes } from 'http-status-codes'; @@ -20,6 +21,7 @@ import { NotionDatabaseUpdatePageResponseSchema, } from './notionDatabaseModel'; import { validateDatabaseQueryConfig, validateNotionProperties } from './utils'; + export const COMPRESS = true; export const notionDatabaseRegistry = new OpenAPIRegistry(); notionDatabaseRegistry.register('Notion Database', NotionDatabaseStructureViewerResponseSchema); @@ -83,30 +85,9 @@ const DEFAULT_ANNOTATIONS = { underline: false, }; -// Helper to fetch the database structure -async function fetchDatabaseStructure(databaseId: string, apiKey: string) { - // Headers for Notion API requests - const headers = { - Authorization: `Bearer ${apiKey}`, - 'Content-Type': 'application/json', - 'Notion-Version': NOTION_VERSION, // or the version you are using - }; - try { - const response = await fetch(`${NOTION_API_URL}/databases/${databaseId}`, { - method: 'GET', - headers: headers, - }); - - if (!response.ok) { - throw new Error(`Error fetching database structure: ${response.statusText}`); - } - - const data = await response.json(); - console.log(JSON.stringify(data)); - return data; - } catch (error: any) { - throw new Error(`Failed to fetch database structure: ${error.message}`); - } +// Helper function to initialize the Notion client +export function initNotionClient(apiKey: string) { + return new NotionClient({ auth: apiKey }); } function mapNotionPropertyRequestBody(properties: any[] = []) { @@ -213,96 +194,6 @@ function mapNotionPropertyRequestBody(properties: any[] = []) { return notionProperties; } -async function createPageInNotionDatabase(apiKey: string, databaseId: string, properties: object) { - // Headers for Notion API requests - const headers = { - Authorization: `Bearer ${apiKey}`, - 'Content-Type': 'application/json', - 'Notion-Version': NOTION_VERSION, - }; - const requestBody = { - parent: { database_id: databaseId }, - properties: properties, - }; - - try { - const response = await fetch(`${NOTION_API_URL}/pages`, { - method: 'POST', - headers: headers, - body: JSON.stringify(requestBody), - }); - - if (!response.ok) { - throw new Error(`Error adding new page to database: ${response.statusText}`); - } - - const data = await response.json(); - return data; - } catch (error: any) { - throw new Error(`Failed to add database to database: ${error.message}`); - } -} - -async function updatePageInNotionDatabase(apiKey: string, pageId: string, properties: object) { - // Headers for Notion API requests - const headers = { - Authorization: `Bearer ${apiKey}`, - 'Content-Type': 'application/json', - 'Notion-Version': NOTION_VERSION, - }; - - const requestBody = { - properties: properties, - }; - - try { - const response = await fetch(`${NOTION_API_URL}/pages/${pageId}`, { - method: 'PATCH', - headers: headers, - body: JSON.stringify(requestBody), - }); - - if (!response.ok) { - throw new Error(`Error adding update page: ${response.statusText}`); - } - - const data = await response.json(); - return data; - } catch (error: any) { - throw new Error(`Failed to update page: ${error.message}`); - } -} - -async function archivePageInNotionDatabase(apiKey: string, pageId: string) { - // Headers for Notion API requests - const headers = { - Authorization: `Bearer ${apiKey}`, - 'Content-Type': 'application/json', - 'Notion-Version': NOTION_VERSION, - }; - - const requestBody = { - archived: true, - }; - - try { - const response = await fetch(`${NOTION_API_URL}/pages/${pageId}`, { - method: 'PATCH', - headers: headers, - body: JSON.stringify(requestBody), - }); - - if (!response.ok) { - throw new Error(`Error removing page: ${response.statusText}`); - } - - const data = await response.json(); - return data; - } catch (error: any) { - throw new Error(`Failed to removing page: ${error.message}`); - } -} - async function queryPagesInNotionDatabase( apiKey: string, databaseId: string, @@ -369,10 +260,11 @@ export const notionDatabaseRouter: Router = (() => { } try { - const dbStructure = await fetchDatabaseStructure(databaseId, notionApiKey); + const notion = initNotionClient(notionApiKey); + const database = await notion.databases.retrieve({ database_id: databaseId }); const result = { databaseId: databaseId, - structure: dbStructure.properties, + structure: database.properties, }; const serviceResponse = new ServiceResponse( ResponseStatus.Success, @@ -421,10 +313,14 @@ export const notionDatabaseRouter: Router = (() => { return handleServiceResponse(validateServiceResponse, res); } try { + const notion = initNotionClient(notionApiKey); // Validate properties before creating validateNotionProperties(databaseStructure, properties); const notionProperties = mapNotionPropertyRequestBody(properties); - const result = await createPageInNotionDatabase(notionApiKey, databaseId, notionProperties); + const result = await notion.pages.create({ + parent: { database_id: databaseId }, + properties: notionProperties, + }); const serviceResponse = new ServiceResponse( ResponseStatus.Success, 'Page created successfully', @@ -468,10 +364,14 @@ export const notionDatabaseRouter: Router = (() => { } try { + const notion = initNotionClient(notionApiKey); // Validate properties before creating validateNotionProperties(databaseStructure, properties); const notionProperties = mapNotionPropertyRequestBody(properties); - const result = await updatePageInNotionDatabase(notionApiKey, pageId, notionProperties); + const result = await notion.pages.update({ + page_id: pageId, + properties: notionProperties, + }); const serviceResponse = new ServiceResponse( ResponseStatus.Success, 'Page updated successfully', @@ -515,7 +415,11 @@ export const notionDatabaseRouter: Router = (() => { } try { - const result = await archivePageInNotionDatabase(notionApiKey, pageId); + const notion = initNotionClient(notionApiKey); + const result = await notion.pages.update({ + page_id: pageId, + archived: true, + }); const serviceResponse = new ServiceResponse( ResponseStatus.Success, 'Page removed successfully', From 0f22550c3d8094f3094401a82836a4144af7f254 Mon Sep 17 00:00:00 2001 From: dat-devuap Date: Mon, 27 Jan 2025 09:46:56 +0700 Subject: [PATCH 11/14] feat(notion database query): using Notion SDK for JavaScript --- .../notionDatabase/notionDatabaseRouter.ts | 52 +++---------------- 1 file changed, 8 insertions(+), 44 deletions(-) diff --git a/src/routes/notionDatabase/notionDatabaseRouter.ts b/src/routes/notionDatabase/notionDatabaseRouter.ts index 6cdd43f..ab884b5 100644 --- a/src/routes/notionDatabase/notionDatabaseRouter.ts +++ b/src/routes/notionDatabase/notionDatabaseRouter.ts @@ -75,8 +75,6 @@ notionDatabaseRegistry.registerPath({ responses: createApiResponse(NotionDatabaseQueryPageResponseSchema, 'Success'), }); -const NOTION_API_URL = 'https://api.notion.com/v1'; -const NOTION_VERSION = '2022-06-28'; const DEFAULT_ANNOTATIONS = { italic: false, bold: false, @@ -194,46 +192,6 @@ function mapNotionPropertyRequestBody(properties: any[] = []) { return notionProperties; } -async function queryPagesInNotionDatabase( - apiKey: string, - databaseId: string, - filter: object, - sorts: any[], - pageSize: number, - startCursor: string | undefined -) { - // Headers for Notion API requests - const headers = { - Authorization: `Bearer ${apiKey}`, - 'Content-Type': 'application/json', - 'Notion-Version': NOTION_VERSION, - }; - - const requestBody = { - filter: filter, - sorts: sorts, - page_size: pageSize, - start_cursor: startCursor, - }; - - try { - const response = await fetch(`${NOTION_API_URL}/databases/${databaseId}/query`, { - method: 'POST', - headers: headers, - body: JSON.stringify(requestBody), - }); - - if (!response.ok) { - throw new Error(`Error query pages: ${response.statusText}`); - } - - const data = await response.json(); - return data; - } catch (error: any) { - throw new Error(`Failed to query pages: ${error.message}`); - } -} - export const notionDatabaseRouter: Router = (() => { const router = express.Router(); @@ -475,10 +433,16 @@ export const notionDatabaseRouter: Router = (() => { } try { + const notion = initNotionClient(notionApiKey); // Validate databaseStructure against filters and sorts validateDatabaseQueryConfig(databaseStructure, filter, sorts); - - const result = await queryPagesInNotionDatabase(notionApiKey, databaseId, filter, sorts, pageSize, startCursor); + const result = await notion.databases.query({ + database_id: databaseId, + filter, + sorts, + page_size: pageSize, + start_cursor: startCursor, + }); const serviceResponse = new ServiceResponse( ResponseStatus.Success, 'Pages query successfully', From a599aa8bf4f7d7c8504fc85913478286399deb2c Mon Sep 17 00:00:00 2001 From: dat-devuap Date: Wed, 29 Jan 2025 00:57:52 +0700 Subject: [PATCH 12/14] feat(notion database maker): add api handle creating a database --- .../notionDatabase/notionDatabaseModel.ts | 137 ++++++++++++++++++ .../notionDatabase/notionDatabaseRouter.ts | 114 ++++++++++++++- src/routes/notionDatabase/utils.ts | 104 +++++++++++++ 3 files changed, 354 insertions(+), 1 deletion(-) diff --git a/src/routes/notionDatabase/notionDatabaseModel.ts b/src/routes/notionDatabase/notionDatabaseModel.ts index 5d714bf..b24e311 100644 --- a/src/routes/notionDatabase/notionDatabaseModel.ts +++ b/src/routes/notionDatabase/notionDatabaseModel.ts @@ -132,3 +132,140 @@ export const NotionDatabaseQueryPageRequestBodySchema = z.object({ startCursor: z.any().optional(), }); export type NotionDatabaseQueryPageRequestBody = z.infer; + +// Define Notion Database Maker +export type NotionDatabaseMakerResponse = z.infer; +export const NotionDatabaseMakerResponseSchema = z.object({}); +// Request Body Schema +export const NotionDatabaseMakerRequestBodySchema = z.object({ + notionApiKey: z.string().openapi({ + description: + 'The Notion API Key getting from Notion Integration Page at https://www.notion.so/profile/integrations', + }), + parent: z + .object({ + type: z.enum(['page_id', 'database_id']), + pageId: z.string().optional(), + databaseId: z.string().optional(), + }) + .refine((data) => data.pageId || data.databaseId, { + message: 'Either `pageId` or `databaseId` must be provided.', + }), + icon: z.string().optional(), + cover: z.string().url().optional(), + isInline: z.boolean().optional(), + title: z + .array( + z.object({ + type: z.literal('text'), + text: z + .object({ + content: z.string().nonempty('Content is required.'), + }) + .required(), + annotations: z + .object({ + italic: z.boolean().default(false), + bold: z.boolean().default(false), + color: z.string().default('default'), + strikethrough: z.boolean().default(false), + underline: z.boolean().default(false), + }) + .default({ + italic: false, + bold: false, + color: 'default', + strikethrough: false, + underline: false, + }) + .optional(), + }) + ) + .nonempty('Title is required.'), + description: z + .array( + z.object({ + type: z.literal('text'), + text: z + .object({ + content: z.string().nonempty('Content is required.'), + }) + .required(), + annotations: z + .object({ + italic: z.boolean().default(false), + bold: z.boolean().default(false), + color: z.string().default('default'), + strikethrough: z.boolean().default(false), + underline: z.boolean().default(false), + }) + .default({ + italic: false, + bold: false, + color: 'default', + strikethrough: false, + underline: false, + }) + .optional(), + }) + ) + .optional(), + notionProperties: z + .array( + z.object({ + propertyName: z.string().nonempty('Property name is required.'), + propertyType: z.enum([ + 'title', + 'rich_text', + 'number', + 'select', + 'status', + 'multi_select', + 'date', + 'url', + 'email', + 'phone_number', + 'checkbox', + 'files', + 'formula', + ]), + options: z + .array( + z.object({ + name: z.string().nonempty('Option name is required.'), + color: z + .enum(['default', 'gray', 'brown', 'orange', 'yellow', 'green', 'blue', 'purple', 'pink', 'red']) + .optional(), + }) + ) + .optional(), + format: z + .enum([ + 'number', + 'number_with_commas', + 'percent', + 'dollar', + 'euro', + 'pound', + 'yen', + 'ruble', + 'rupee', + 'won', + 'yuan', + 'real', + 'lira', + 'franc', + 'singapore_dollar', + 'australian_dollar', + 'canadian_dollar', + 'hong_kong_dollar', + 'new_zealand_dollar', + ]) + .optional(), + formula: z.string().optional(), + dateFormat: z.string().optional(), + }) + ) + .nonempty('Notion properties are required.'), +}); +export type NotionDatabaseMakerRequestBody = z.infer; diff --git a/src/routes/notionDatabase/notionDatabaseRouter.ts b/src/routes/notionDatabase/notionDatabaseRouter.ts index ab884b5..c68b62a 100644 --- a/src/routes/notionDatabase/notionDatabaseRouter.ts +++ b/src/routes/notionDatabase/notionDatabaseRouter.ts @@ -13,6 +13,8 @@ import { NotionDatabaseArchivePageResponseSchema, NotionDatabaseCreatePageRequestBodySchema, NotionDatabaseCreatePageResponseSchema, + NotionDatabaseMakerRequestBodySchema, + NotionDatabaseMakerResponseSchema, NotionDatabaseQueryPageRequestBodySchema, NotionDatabaseQueryPageResponseSchema, NotionDatabaseStructureViewerRequestBodySchema, @@ -20,7 +22,12 @@ import { NotionDatabaseUpdatePageRequestBodySchema, NotionDatabaseUpdatePageResponseSchema, } from './notionDatabaseModel'; -import { validateDatabaseQueryConfig, validateNotionProperties } from './utils'; +import { + buildColumnSchema, + mapNotionRichTextProperty, + validateDatabaseQueryConfig, + validateNotionProperties, +} from './utils'; export const COMPRESS = true; export const notionDatabaseRegistry = new OpenAPIRegistry(); @@ -75,6 +82,16 @@ notionDatabaseRegistry.registerPath({ responses: createApiResponse(NotionDatabaseQueryPageResponseSchema, 'Success'), }); +notionDatabaseRegistry.registerPath({ + method: 'post', + path: '/notion-database/create-database', + tags: ['Notion Database'], + request: { + body: createApiRequestBody(NotionDatabaseMakerRequestBodySchema, 'application/json'), + }, + responses: createApiResponse(NotionDatabaseMakerResponseSchema, 'Success'), +}); + const DEFAULT_ANNOTATIONS = { italic: false, bold: false, @@ -462,5 +479,100 @@ export const notionDatabaseRouter: Router = (() => { } }); + router.post('/create-database', async (_req: Request, res: Response) => { + const { + notionApiKey, + parent, + icon, + cover, + title, + description, + isInline = false, + notionProperties = [], + } = _req.body; + + if (!notionApiKey) { + const validateServiceResponse = new ServiceResponse( + ResponseStatus.Failed, + '[Validation Error] Notion Key is required!', + 'Please make sure you have sent the Notion Key from TypingMind.', + StatusCodes.BAD_REQUEST + ); + return handleServiceResponse(validateServiceResponse, res); + } + + if (parent && parent.type === 'page_id' && !parent.pageId) { + const validateServiceResponse = new ServiceResponse( + ResponseStatus.Failed, + '[Validation Error] Page ID is required!. Please provide specific Page ID or Page URL', + 'Please make sure you have sent the Page ID from TypingMind.', + StatusCodes.BAD_REQUEST + ); + return handleServiceResponse(validateServiceResponse, res); + } + + if (parent && parent.type === 'database_id' && !parent.databaseId) { + const validateServiceResponse = new ServiceResponse( + ResponseStatus.Failed, + '[Validation Error] Database ID is required!. Please provide specific Database ID or Database URL', + 'Please make sure you have sent the Database ID from TypingMind.', + StatusCodes.BAD_REQUEST + ); + return handleServiceResponse(validateServiceResponse, res); + } + + try { + const notion = initNotionClient(notionApiKey); + + // Initialize an empty object to hold properties + const databaseProperties: Record = {}; + // Iterate over notionProperties and maintain the order + notionProperties.forEach((property: any) => { + const schema = buildColumnSchema(property); // Assume this builds the required schema + for (const [key, value] of Object.entries(schema.properties)) { + databaseProperties[key] = value; + } + }); + + const parentSchema: any = { type: parent.type }; + if (parent.type == 'page_id') { + parentSchema.page_id = parent.pageId || undefined; + } else if (parent.type == 'database_id') { + parentSchema.database_id = parent.databaseId || undefined; + } + + // Prepare the request payload to create the Notion database + const payload: any = { + parent: parentSchema, + icon: { type: 'emoji', emoji: icon }, + cover: { type: 'external', external: { url: cover } }, + title: mapNotionRichTextProperty(title), + description: mapNotionRichTextProperty(description), + is_inline: isInline, + properties: databaseProperties, + }; + + // Call the Notion client to create the database + const result = await notion.databases.create(payload); + + const serviceResponse = new ServiceResponse( + ResponseStatus.Success, + 'Database created successfully', + result, + StatusCodes.OK + ); + return handleServiceResponse(serviceResponse, res); + } catch (error) { + const errorMessage = (error as Error).message; + const errorServiceResponse = new ServiceResponse( + ResponseStatus.Failed, + `Error: ${errorMessage}`, + `Sorry, we couldn't create Database!`, + errorMessage.includes('[Validation Error]') ? StatusCodes.BAD_REQUEST : StatusCodes.INTERNAL_SERVER_ERROR + ); + return handleServiceResponse(errorServiceResponse, res); + } + }); + return router; })(); diff --git a/src/routes/notionDatabase/utils.ts b/src/routes/notionDatabase/utils.ts index 92abdaf..58ea80e 100644 --- a/src/routes/notionDatabase/utils.ts +++ b/src/routes/notionDatabase/utils.ts @@ -59,3 +59,107 @@ export function validateNotionProperties(databaseStructure: any[], properties: a } }); } + +export function buildColumnSchema({ propertyName, propertyType, options = [], format, formatDate, formula = '' }: any) { + const schema: any = { + properties: {}, + }; + + // Define properties based on the column type + switch (propertyType) { + case 'title': + schema.properties[propertyName] = { title: {} }; + break; + case 'rich_text': + schema.properties[propertyName] = { rich_text: {} }; + break; + case 'number': + schema.properties[propertyName] = { number: { format: format } }; + break; + case 'select': + schema.properties[propertyName] = { + select: { + options: options.map((option: any) => ({ + name: option.name, + color: option.color || 'default', + })), + }, + }; + break; + case 'multi_select': + schema.properties[propertyName] = { + multi_select: { + options: options.map((option: any) => ({ + name: option.name, + color: option.color || 'default', + })), + }, + }; + break; + case 'date': + schema.properties[propertyName] = { + date: { + format: formatDate, + }, + }; + break; + case 'checkbox': + schema.properties[propertyName] = { checkbox: {} }; + break; + case 'url': + schema.properties[propertyName] = { url: {} }; + break; + case 'email': + schema.properties[propertyName] = { email: {} }; + break; + case 'phone_number': + schema.properties[propertyName] = { phone_number: {} }; + break; + case 'formula': + schema.properties[propertyName] = { + formula: { + expression: formula, // Formula expression goes here + }, + }; + break; + case 'status': + schema.properties[propertyName] = { status: {} }; + break; + case 'files': + schema.properties[propertyName] = { files: {} }; + break; + default: + throw new Error(`Unknown column type: ${propertyType}`); + } + + return schema; +} + +export function mapNotionRichTextProperty(value: any[]) { + const DEFAULT_ANNOTATIONS = { + italic: false, + bold: false, + color: 'default', + strikethrough: false, + underline: false, + }; + + return value.map((item) => { + const annotationConfigs = item.annotations || DEFAULT_ANNOTATIONS; + return { + type: 'text', + text: { content: item.text.content }, + annotations: { + italic: annotationConfigs.italic !== undefined ? annotationConfigs.italic : DEFAULT_ANNOTATIONS.italic, + bold: annotationConfigs.bold !== undefined ? annotationConfigs.bold : DEFAULT_ANNOTATIONS.bold, + color: annotationConfigs.color ? annotationConfigs.color : DEFAULT_ANNOTATIONS.color, + strikethrough: + annotationConfigs.strikethrough !== undefined + ? annotationConfigs.strikethrough + : DEFAULT_ANNOTATIONS.strikethrough, + underline: + annotationConfigs.underline !== undefined ? annotationConfigs.underline : DEFAULT_ANNOTATIONS.underline, + }, + }; + }); +} From 609b6a20f4177bcde6ea4b9bf444e7fccbade272 Mon Sep 17 00:00:00 2001 From: dat-devuap Date: Wed, 29 Jan 2025 01:34:50 +0700 Subject: [PATCH 13/14] fix(notion database maker): handle optional adding icon and cover to request body --- src/routes/notionDatabase/notionDatabaseRouter.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/routes/notionDatabase/notionDatabaseRouter.ts b/src/routes/notionDatabase/notionDatabaseRouter.ts index c68b62a..e25c746 100644 --- a/src/routes/notionDatabase/notionDatabaseRouter.ts +++ b/src/routes/notionDatabase/notionDatabaseRouter.ts @@ -544,14 +544,20 @@ export const notionDatabaseRouter: Router = (() => { // Prepare the request payload to create the Notion database const payload: any = { parent: parentSchema, - icon: { type: 'emoji', emoji: icon }, - cover: { type: 'external', external: { url: cover } }, title: mapNotionRichTextProperty(title), description: mapNotionRichTextProperty(description), is_inline: isInline, properties: databaseProperties, }; + if (icon) { + payload.icon = { type: 'emoji', emoji: icon }; + } + + if (cover) { + payload.cover = { type: 'external', external: { url: cover } }; + } + // Call the Notion client to create the database const result = await notion.databases.create(payload); From fbad22f6d9fda01b5b2be044a39062b77bb8834f Mon Sep 17 00:00:00 2001 From: dat-devuap Date: Wed, 29 Jan 2025 14:57:26 +0700 Subject: [PATCH 14/14] fix(notion database maker): only support parent type of page id for creating database --- .../notionDatabase/notionDatabaseModel.ts | 6 +++--- .../notionDatabase/notionDatabaseRouter.ts | 19 +------------------ 2 files changed, 4 insertions(+), 21 deletions(-) diff --git a/src/routes/notionDatabase/notionDatabaseModel.ts b/src/routes/notionDatabase/notionDatabaseModel.ts index b24e311..4616e89 100644 --- a/src/routes/notionDatabase/notionDatabaseModel.ts +++ b/src/routes/notionDatabase/notionDatabaseModel.ts @@ -144,12 +144,12 @@ export const NotionDatabaseMakerRequestBodySchema = z.object({ }), parent: z .object({ - type: z.enum(['page_id', 'database_id']), + type: z.enum(['page_id']), pageId: z.string().optional(), databaseId: z.string().optional(), }) - .refine((data) => data.pageId || data.databaseId, { - message: 'Either `pageId` or `databaseId` must be provided.', + .refine((data) => data.pageId, { + message: 'Page ID must be provided.', }), icon: z.string().optional(), cover: z.string().url().optional(), diff --git a/src/routes/notionDatabase/notionDatabaseRouter.ts b/src/routes/notionDatabase/notionDatabaseRouter.ts index e25c746..284b07e 100644 --- a/src/routes/notionDatabase/notionDatabaseRouter.ts +++ b/src/routes/notionDatabase/notionDatabaseRouter.ts @@ -511,16 +511,6 @@ export const notionDatabaseRouter: Router = (() => { return handleServiceResponse(validateServiceResponse, res); } - if (parent && parent.type === 'database_id' && !parent.databaseId) { - const validateServiceResponse = new ServiceResponse( - ResponseStatus.Failed, - '[Validation Error] Database ID is required!. Please provide specific Database ID or Database URL', - 'Please make sure you have sent the Database ID from TypingMind.', - StatusCodes.BAD_REQUEST - ); - return handleServiceResponse(validateServiceResponse, res); - } - try { const notion = initNotionClient(notionApiKey); @@ -534,16 +524,9 @@ export const notionDatabaseRouter: Router = (() => { } }); - const parentSchema: any = { type: parent.type }; - if (parent.type == 'page_id') { - parentSchema.page_id = parent.pageId || undefined; - } else if (parent.type == 'database_id') { - parentSchema.database_id = parent.databaseId || undefined; - } - // Prepare the request payload to create the Notion database const payload: any = { - parent: parentSchema, + parent: { type: parent.type, page_id: parent.pageId }, title: mapNotionRichTextProperty(title), description: mapNotionRichTextProperty(description), is_inline: isInline,