diff --git a/golemcore/notion/plugin.yaml b/golemcore/notion/plugin.yaml
index dbe787d..9530697 100644
--- a/golemcore/notion/plugin.yaml
+++ b/golemcore/notion/plugin.yaml
@@ -1,12 +1,12 @@
id: golemcore/notion
provider: golemcore
name: notion
-version: 1.0.1
+version: 1.1.0
pluginApiVersion: 1
engineVersion: ">=0.0.0 <1.0.0"
entrypoint: me.golemcore.plugins.golemcore.notion.NotionPluginBootstrap
-description: Notion vault plugin backed by the official Notion HTTP API.
-sourceUrl: https://github.com/alexk-dev/golemcore-plugins/tree/main/golemcore/notion
-license: Apache-2.0
+description: "Notion vault plugin backed by the official Notion HTTP API."
+sourceUrl: "https://github.com/alexk-dev/golemcore-plugins/tree/main/golemcore/notion"
+license: "Apache-2.0"
maintainers:
- alexk-dev
diff --git a/golemcore/notion/pom.xml b/golemcore/notion/pom.xml
index 6c434ce..91990e9 100644
--- a/golemcore/notion/pom.xml
+++ b/golemcore/notion/pom.xml
@@ -11,7 +11,7 @@
../../pom.xml
- 1.0.1
+ 1.1.0
golemcore-notion-plugin
golemcore/notion
Notion vault plugin for GolemCore
diff --git a/golemcore/notion/src/main/java/me/golemcore/plugins/golemcore/notion/NotionVaultService.java b/golemcore/notion/src/main/java/me/golemcore/plugins/golemcore/notion/NotionVaultService.java
index f581a59..e83180c 100644
--- a/golemcore/notion/src/main/java/me/golemcore/plugins/golemcore/notion/NotionVaultService.java
+++ b/golemcore/notion/src/main/java/me/golemcore/plugins/golemcore/notion/NotionVaultService.java
@@ -1,17 +1,28 @@
package me.golemcore.plugins.golemcore.notion;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
import me.golemcore.plugin.api.extension.model.ToolFailureKind;
import me.golemcore.plugin.api.extension.model.ToolResult;
import me.golemcore.plugins.golemcore.notion.support.NotionApiClient;
import me.golemcore.plugins.golemcore.notion.support.NotionApiException;
+import me.golemcore.plugins.golemcore.notion.support.NotionChildSummary;
+import me.golemcore.plugins.golemcore.notion.support.NotionDataSourceQueryResult;
+import me.golemcore.plugins.golemcore.notion.support.NotionDataSourceSummary;
+import me.golemcore.plugins.golemcore.notion.support.NotionDatabaseSummary;
+import me.golemcore.plugins.golemcore.notion.support.NotionFileAttachmentSummary;
+import me.golemcore.plugins.golemcore.notion.support.NotionFileUploadSummary;
import me.golemcore.plugins.golemcore.notion.support.NotionLocalIndexService;
+import me.golemcore.plugins.golemcore.notion.support.NotionPageDetails;
import me.golemcore.plugins.golemcore.notion.support.NotionPageSummary;
import me.golemcore.plugins.golemcore.notion.support.NotionPathValidator;
+import me.golemcore.plugins.golemcore.notion.support.NotionRagSyncService;
import me.golemcore.plugins.golemcore.notion.support.NotionSearchHit;
import me.golemcore.plugins.golemcore.notion.support.NotionTransportException;
-import me.golemcore.plugins.golemcore.notion.support.NotionRagSyncService;
import org.springframework.stereotype.Service;
+import java.nio.file.Path;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -24,6 +35,7 @@ public class NotionVaultService {
private final NotionLocalIndexService localIndexService;
private final NotionRagSyncService ragSyncService;
private final NotionPathValidator pathValidator = new NotionPathValidator();
+ private final ObjectMapper objectMapper = new ObjectMapper();
public NotionVaultService(
NotionApiClient apiClient,
@@ -40,15 +52,15 @@ public ToolResult listDirectory(String path) {
try {
String normalizedPath = pathValidator.normalizeNotePath(path);
ResolvedPage page = resolveExistingPage(normalizedPath);
- List entries = apiClient.listChildPages(page.pageId()).stream()
- .map(NotionPageSummary::title)
- .toList();
+ List items = apiClient.listChildItems(page.pageId());
+ List entries = items.stream().map(NotionChildSummary::title).toList();
Map data = new LinkedHashMap<>();
data.put("path", normalizedPath);
data.put("entries", entries);
data.put("files", entries);
+ data.put("items", items.stream().map(this::toChildItem).toList());
return ToolResult.success(
- "Listed " + entries.size() + " item(s) in " + displayPath(normalizedPath),
+ "Listed " + items.size() + " item(s) in " + displayPath(normalizedPath),
data);
} catch (IllegalArgumentException | NotionApiException | NotionTransportException ex) {
return executionFailure(ex.getMessage());
@@ -64,11 +76,14 @@ public ToolResult readNote(String path) {
int maxReadChars = configService.getConfig().getMaxReadChars();
boolean truncated = originalLength > maxReadChars;
String visibleContent = truncated ? content.substring(0, maxReadChars) : content;
+ NotionPageDetails details = apiClient.retrievePageDetails(page.pageId());
Map data = new LinkedHashMap<>();
data.put("path", normalizedPath);
+ data.put("page_id", page.pageId());
data.put("content", visibleContent);
data.put("truncated", truncated);
+ data.put("files", details.files().stream().map(this::toFileAttachment).toList());
if (truncated) {
data.put("originalLength", originalLength);
}
@@ -108,7 +123,7 @@ public ToolResult createNote(String path, String content) {
String parentPath = pathValidator.parentPath(normalizedPath);
String title = pathValidator.leafName(normalizedPath);
ResolvedPage parentPage = resolveExistingPage(parentPath);
- ensureChildAbsent(parentPage.pageId(), title);
+ ensureChildTitleAbsent(parentPage.pageId(), title);
NotionPageSummary created = apiClient.createChildPage(parentPage.pageId(), title, content);
refreshIndexesAfterMutation(
() -> ragSyncService.upsertDocument(
@@ -180,7 +195,7 @@ public ToolResult moveNote(String path, String targetPath) {
String targetParentPath = pathValidator.parentPath(normalizedTargetPath);
String targetTitle = pathValidator.leafName(normalizedTargetPath);
ResolvedPage targetParent = resolveExistingPage(targetParentPath);
- ensureChildAbsent(targetParent.pageId(), targetTitle);
+ ensureChildTitleAbsent(targetParent.pageId(), targetTitle);
if (!source.parentPageId().equals(targetParent.pageId())) {
apiClient.movePage(source.pageId(), targetParent.pageId());
}
@@ -216,7 +231,7 @@ public ToolResult renameNote(String path, String newName) {
throw new IllegalArgumentException("Source and target paths must differ");
}
ResolvedPage source = resolveExistingPage(normalizedPath);
- ensureChildAbsent(source.parentPageId(), pathValidator.leafName(targetPath));
+ ensureChildTitleAbsent(source.parentPageId(), pathValidator.leafName(targetPath));
apiClient.renamePage(source.pageId(), pathValidator.leafName(targetPath));
String content = apiClient.retrievePageMarkdown(source.pageId());
refreshIndexesAfterMutation(
@@ -230,7 +245,336 @@ public ToolResult renameNote(String path, String newName) {
data.put("path", normalizedPath);
data.put("target_path", targetPath);
return ToolResult.success("Renamed note from " + normalizedPath + " to " + targetPath, data);
- } catch (IllegalArgumentException ex) {
+ } catch (IllegalArgumentException | NotionApiException | NotionTransportException ex) {
+ return executionFailure(ex.getMessage());
+ }
+ }
+
+ public ToolResult createDatabase(
+ String parentPath,
+ String title,
+ String description,
+ String propertiesJson,
+ Boolean inline,
+ String iconEmoji,
+ String coverUrl) {
+ if (!Boolean.TRUE.equals(configService.getConfig().getAllowWrite())) {
+ return ToolResult.failure(ToolFailureKind.POLICY_DENIED, "Notion write is disabled in plugin settings");
+ }
+ try {
+ String normalizedParentPath = pathValidator.normalizeNotePath(parentPath);
+ ResolvedPage parentPage = resolveExistingPage(normalizedParentPath);
+ ensureChildTitleAbsent(parentPage.pageId(), requiredText(title, "title"));
+ NotionDatabaseSummary database = apiClient.createDatabase(
+ parentPage.pageId(),
+ requiredText(title, "title"),
+ description,
+ parseJsonObject(propertiesJson),
+ Boolean.TRUE.equals(inline),
+ iconEmoji,
+ coverUrl);
+ return ToolResult.success(
+ "Created database " + database.title(),
+ Map.of(
+ "parent_path", normalizedParentPath,
+ "database", toDatabaseSummary(database)));
+ } catch (IllegalArgumentException | NotionApiException | NotionTransportException ex) {
+ return executionFailure(ex.getMessage());
+ }
+ }
+
+ public ToolResult readDatabase(String databaseId) {
+ try {
+ NotionDatabaseSummary database = apiClient.retrieveDatabase(requiredText(databaseId, "database_id"));
+ return ToolResult.success(
+ "Retrieved database " + database.title(),
+ Map.of("database", toDatabaseSummary(database)));
+ } catch (IllegalArgumentException | NotionApiException | NotionTransportException ex) {
+ return executionFailure(ex.getMessage());
+ }
+ }
+
+ public ToolResult updateDatabase(
+ String databaseId,
+ String title,
+ String description,
+ String iconEmoji,
+ String coverUrl) {
+ if (!Boolean.TRUE.equals(configService.getConfig().getAllowWrite())) {
+ return ToolResult.failure(ToolFailureKind.POLICY_DENIED, "Notion write is disabled in plugin settings");
+ }
+ try {
+ NotionDatabaseSummary database = apiClient.updateDatabase(
+ requiredText(databaseId, "database_id"),
+ title,
+ description,
+ iconEmoji,
+ coverUrl);
+ return ToolResult.success(
+ "Updated database " + database.title(),
+ Map.of("database", toDatabaseSummary(database)));
+ } catch (IllegalArgumentException | NotionApiException | NotionTransportException ex) {
+ return executionFailure(ex.getMessage());
+ }
+ }
+
+ public ToolResult createDataSource(String databaseId, String title, String propertiesJson, String iconEmoji) {
+ if (!Boolean.TRUE.equals(configService.getConfig().getAllowWrite())) {
+ return ToolResult.failure(ToolFailureKind.POLICY_DENIED, "Notion write is disabled in plugin settings");
+ }
+ try {
+ NotionDataSourceSummary dataSource = apiClient.createDataSource(
+ requiredText(databaseId, "database_id"),
+ title,
+ parseRequiredJsonObject(propertiesJson, "properties_json"),
+ iconEmoji);
+ return ToolResult.success(
+ "Created data source " + dataSource.title(),
+ Map.of("data_source", toDataSourceSummary(dataSource)));
+ } catch (IllegalArgumentException | NotionApiException | NotionTransportException ex) {
+ return executionFailure(ex.getMessage());
+ }
+ }
+
+ public ToolResult readDataSource(String dataSourceId) {
+ try {
+ NotionDataSourceSummary dataSource = apiClient
+ .retrieveDataSource(requiredText(dataSourceId, "data_source_id"));
+ return ToolResult.success(
+ "Retrieved data source " + dataSource.title(),
+ Map.of("data_source", toDataSourceSummary(dataSource)));
+ } catch (IllegalArgumentException | NotionApiException | NotionTransportException ex) {
+ return executionFailure(ex.getMessage());
+ }
+ }
+
+ public ToolResult updateDataSource(String dataSourceId, String title, String propertiesJson, String iconEmoji) {
+ if (!Boolean.TRUE.equals(configService.getConfig().getAllowWrite())) {
+ return ToolResult.failure(ToolFailureKind.POLICY_DENIED, "Notion write is disabled in plugin settings");
+ }
+ try {
+ NotionDataSourceSummary dataSource = apiClient.updateDataSource(
+ requiredText(dataSourceId, "data_source_id"),
+ title,
+ parseJsonObject(propertiesJson),
+ iconEmoji);
+ return ToolResult.success(
+ "Updated data source " + dataSource.title(),
+ Map.of("data_source", toDataSourceSummary(dataSource)));
+ } catch (IllegalArgumentException | NotionApiException | NotionTransportException ex) {
+ return executionFailure(ex.getMessage());
+ }
+ }
+
+ public ToolResult queryDatabase(
+ String databaseId,
+ String dataSourceId,
+ String filterJson,
+ String sortsJson,
+ Integer limit,
+ String cursor) {
+ try {
+ String resolvedDataSourceId = resolveDataSourceId(databaseId, dataSourceId);
+ NotionDataSourceQueryResult queryResult = apiClient.queryDataSource(
+ resolvedDataSourceId,
+ filterJson,
+ sortsJson,
+ limit,
+ cursor);
+ Map data = new LinkedHashMap<>();
+ data.put("data_source_id", resolvedDataSourceId);
+ data.put("count", queryResult.count());
+ data.put("has_more", queryResult.hasMore());
+ data.put("next_cursor", queryResult.nextCursor());
+ data.put("results", queryResult.results());
+ return ToolResult.success("Retrieved " + queryResult.count() + " database item(s)", data);
+ } catch (IllegalArgumentException | NotionApiException | NotionTransportException ex) {
+ return executionFailure(ex.getMessage());
+ }
+ }
+
+ public ToolResult createDatabaseEntry(
+ String databaseId,
+ String dataSourceId,
+ String propertiesJson,
+ String markdown,
+ String contentJson,
+ String iconEmoji,
+ String coverUrl) {
+ if (!Boolean.TRUE.equals(configService.getConfig().getAllowWrite())) {
+ return ToolResult.failure(ToolFailureKind.POLICY_DENIED, "Notion write is disabled in plugin settings");
+ }
+ try {
+ String resolvedDataSourceId = resolveDataSourceId(databaseId, dataSourceId);
+ NotionPageSummary entry = apiClient.createDataSourceEntry(
+ resolvedDataSourceId,
+ requiredText(propertiesJson, "properties_json"),
+ markdown,
+ contentJson,
+ iconEmoji,
+ coverUrl);
+ return ToolResult.success(
+ "Created database entry " + entry.title(),
+ Map.of(
+ "data_source_id", resolvedDataSourceId,
+ "page_id", entry.id(),
+ "title", entry.title(),
+ "url", entry.url()));
+ } catch (IllegalArgumentException | NotionApiException | NotionTransportException ex) {
+ return executionFailure(ex.getMessage());
+ }
+ }
+
+ public ToolResult readDatabaseEntry(String pageId) {
+ try {
+ NotionPageDetails page = apiClient.retrievePageDetails(requiredText(pageId, "page_id"));
+ return ToolResult.success(
+ "Retrieved database entry " + page.title(),
+ Map.of("page", toPageDetails(page)));
+ } catch (IllegalArgumentException | NotionApiException | NotionTransportException ex) {
+ return executionFailure(ex.getMessage());
+ }
+ }
+
+ public ToolResult updateDatabaseEntry(String pageId, String propertiesJson, String iconEmoji, String coverUrl) {
+ if (!Boolean.TRUE.equals(configService.getConfig().getAllowWrite())) {
+ return ToolResult.failure(ToolFailureKind.POLICY_DENIED, "Notion write is disabled in plugin settings");
+ }
+ try {
+ NotionPageSummary page = apiClient.updateDataSourceEntry(
+ requiredText(pageId, "page_id"),
+ propertiesJson,
+ iconEmoji,
+ coverUrl);
+ return ToolResult.success(
+ "Updated database entry " + page.title(),
+ Map.of(
+ "page_id", page.id(),
+ "title", page.title(),
+ "url", page.url()));
+ } catch (IllegalArgumentException | NotionApiException | NotionTransportException ex) {
+ return executionFailure(ex.getMessage());
+ }
+ }
+
+ public ToolResult listPageFiles(String pageId) {
+ try {
+ NotionPageDetails page = apiClient.retrievePageDetails(requiredText(pageId, "page_id"));
+ Map data = new LinkedHashMap<>();
+ data.put("page_id", page.id());
+ data.put("title", page.title());
+ data.put("count", page.files().size());
+ data.put("files", page.files().stream().map(this::toFileAttachment).toList());
+ return ToolResult.success("Found " + page.files().size() + " file(s) on page " + page.title(), data);
+ } catch (IllegalArgumentException | NotionApiException | NotionTransportException ex) {
+ return executionFailure(ex.getMessage());
+ }
+ }
+
+ public ToolResult createFileUpload(
+ String mode,
+ String filename,
+ String contentType,
+ Integer numberOfParts,
+ String externalUrl) {
+ if (!Boolean.TRUE.equals(configService.getConfig().getAllowWrite())) {
+ return ToolResult.failure(ToolFailureKind.POLICY_DENIED, "Notion write is disabled in plugin settings");
+ }
+ try {
+ NotionFileUploadSummary upload = apiClient.createFileUpload(
+ mode,
+ filename,
+ contentType,
+ numberOfParts,
+ externalUrl);
+ return ToolResult.success(
+ "Created file upload " + upload.id(),
+ Map.of("file_upload", toFileUploadSummary(upload)));
+ } catch (IllegalArgumentException | NotionApiException | NotionTransportException ex) {
+ return executionFailure(ex.getMessage());
+ }
+ }
+
+ public ToolResult uploadFileContent(String fileUploadId, String localPath, String contentType) {
+ if (!Boolean.TRUE.equals(configService.getConfig().getAllowWrite())) {
+ return ToolResult.failure(ToolFailureKind.POLICY_DENIED, "Notion write is disabled in plugin settings");
+ }
+ try {
+ NotionFileUploadSummary upload = apiClient.uploadFileContent(
+ requiredText(fileUploadId, "file_upload_id"),
+ Path.of(requiredText(localPath, "local_path")),
+ contentType);
+ return ToolResult.success(
+ "Uploaded file content for " + upload.id(),
+ Map.of("file_upload", toFileUploadSummary(upload)));
+ } catch (IllegalArgumentException | NotionApiException | NotionTransportException ex) {
+ return executionFailure(ex.getMessage());
+ }
+ }
+
+ public ToolResult completeFileUpload(String fileUploadId) {
+ if (!Boolean.TRUE.equals(configService.getConfig().getAllowWrite())) {
+ return ToolResult.failure(ToolFailureKind.POLICY_DENIED, "Notion write is disabled in plugin settings");
+ }
+ try {
+ NotionFileUploadSummary upload = apiClient.completeFileUpload(requiredText(fileUploadId, "file_upload_id"));
+ return ToolResult.success(
+ "Completed file upload " + upload.id(),
+ Map.of("file_upload", toFileUploadSummary(upload)));
+ } catch (IllegalArgumentException | NotionApiException | NotionTransportException ex) {
+ return executionFailure(ex.getMessage());
+ }
+ }
+
+ public ToolResult readFileUpload(String fileUploadId) {
+ try {
+ NotionFileUploadSummary upload = apiClient.retrieveFileUpload(requiredText(fileUploadId, "file_upload_id"));
+ return ToolResult.success(
+ "Retrieved file upload " + upload.id(),
+ Map.of("file_upload", toFileUploadSummary(upload)));
+ } catch (IllegalArgumentException | NotionApiException | NotionTransportException ex) {
+ return executionFailure(ex.getMessage());
+ }
+ }
+
+ public ToolResult listFileUploads(String status, Integer limit, String cursor) {
+ try {
+ List uploads = apiClient.listFileUploads(status, limit, cursor);
+ Map data = new LinkedHashMap<>();
+ data.put("count", uploads.size());
+ data.put("uploads", uploads.stream().map(this::toFileUploadSummary).toList());
+ return ToolResult.success("Listed " + uploads.size() + " file upload(s)", data);
+ } catch (IllegalArgumentException | NotionApiException | NotionTransportException ex) {
+ return executionFailure(ex.getMessage());
+ }
+ }
+
+ public ToolResult attachFileToPage(
+ String pageId,
+ String fileUploadId,
+ String externalUrl,
+ String fileName,
+ String caption,
+ String blockType) {
+ if (!Boolean.TRUE.equals(configService.getConfig().getAllowWrite())) {
+ return ToolResult.failure(ToolFailureKind.POLICY_DENIED, "Notion write is disabled in plugin settings");
+ }
+ try {
+ NotionPageSummary block = apiClient.appendFileBlock(
+ requiredText(pageId, "page_id"),
+ fileUploadId,
+ externalUrl,
+ fileName,
+ caption,
+ blockType);
+ return ToolResult.success(
+ "Attached file to page " + pageId,
+ Map.of(
+ "page_id", pageId,
+ "block_id", block.id(),
+ "url", block.url(),
+ "file_name", fileName != null ? fileName : ""));
+ } catch (IllegalArgumentException | NotionApiException | NotionTransportException ex) {
return executionFailure(ex.getMessage());
}
}
@@ -268,11 +612,11 @@ private ResolvedPage resolveExistingPage(String normalizedPath) {
return new ResolvedPage(currentPageId, parentPageId, normalizedPath, title, url);
}
- private void ensureChildAbsent(String parentPageId, String title) {
- boolean exists = apiClient.listChildPages(parentPageId).stream()
- .anyMatch(page -> title.equals(page.title()));
+ private void ensureChildTitleAbsent(String parentPageId, String title) {
+ boolean exists = apiClient.listChildItems(parentPageId).stream()
+ .anyMatch(item -> title.equals(item.title()));
if (exists) {
- throw new IllegalArgumentException("Target page already exists: " + title);
+ throw new IllegalArgumentException("Target item already exists: " + title);
}
}
@@ -288,6 +632,58 @@ private void requireNonRootPath(String normalizedPath, String operation) {
}
}
+ private String resolveDataSourceId(String databaseId, String dataSourceId) {
+ if (dataSourceId != null && !dataSourceId.isBlank()) {
+ return dataSourceId;
+ }
+ if (databaseId == null || databaseId.isBlank()) {
+ throw new IllegalArgumentException("database_id or data_source_id is required");
+ }
+ NotionDatabaseSummary database = apiClient.retrieveDatabase(databaseId);
+ if (database.dataSources().isEmpty()) {
+ throw new IllegalArgumentException("Database has no data sources: " + databaseId);
+ }
+ if (database.dataSources().size() > 1) {
+ throw new IllegalArgumentException(
+ "Database has multiple data sources; provide data_source_id explicitly: " + databaseId);
+ }
+ Object id = database.dataSources().getFirst().get("id");
+ if (!(id instanceof String text) || text.isBlank()) {
+ throw new IllegalArgumentException("Database data source ID is missing: " + databaseId);
+ }
+ return text;
+ }
+
+ private String requiredText(String value, String fieldName) {
+ if (value == null || value.isBlank()) {
+ throw new IllegalArgumentException(fieldName + " is required");
+ }
+ return value;
+ }
+
+ private Map parseJsonObject(String rawJson) {
+ if (rawJson == null || rawJson.isBlank()) {
+ return Map.of();
+ }
+ try {
+ return objectMapper.readValue(rawJson, new TypeReference<>() {
+ });
+ } catch (JsonProcessingException ex) {
+ throw new IllegalArgumentException("Expected JSON object: " + ex.getMessage(), ex);
+ }
+ }
+
+ private Map parseRequiredJsonObject(String rawJson, String fieldName) {
+ if (rawJson == null || rawJson.isBlank()) {
+ throw new IllegalArgumentException(fieldName + " is required");
+ }
+ Map parsed = parseJsonObject(rawJson);
+ if (parsed.isEmpty()) {
+ throw new IllegalArgumentException(fieldName + " must not be empty");
+ }
+ return parsed;
+ }
+
private ToolResult executionFailure(String message) {
return ToolResult.failure(ToolFailureKind.EXECUTION_FAILED, message);
}
@@ -307,6 +703,66 @@ private Map toSearchResult(NotionSearchHit hit) {
return data;
}
+ private Map toChildItem(NotionChildSummary child) {
+ Map data = new LinkedHashMap<>();
+ data.put("id", child.id());
+ data.put("title", child.title());
+ data.put("url", child.url());
+ data.put("type", child.kind());
+ return data;
+ }
+
+ private Map toDatabaseSummary(NotionDatabaseSummary database) {
+ Map data = new LinkedHashMap<>();
+ data.put("id", database.id());
+ data.put("title", database.title());
+ data.put("url", database.url());
+ data.put("data_sources", database.dataSources());
+ return data;
+ }
+
+ private Map toDataSourceSummary(NotionDataSourceSummary dataSource) {
+ Map data = new LinkedHashMap<>();
+ data.put("id", dataSource.id());
+ data.put("title", dataSource.title());
+ data.put("url", dataSource.url());
+ data.put("properties", dataSource.properties());
+ return data;
+ }
+
+ private Map toPageDetails(NotionPageDetails page) {
+ Map data = new LinkedHashMap<>();
+ data.put("id", page.id());
+ data.put("title", page.title());
+ data.put("url", page.url());
+ data.put("files", page.files().stream().map(this::toFileAttachment).toList());
+ data.put("properties", page.rawProperties());
+ return data;
+ }
+
+ private Map toFileUploadSummary(NotionFileUploadSummary upload) {
+ Map data = new LinkedHashMap<>();
+ data.put("id", upload.id());
+ data.put("status", upload.status());
+ data.put("filename", upload.filename());
+ data.put("content_type", upload.contentType());
+ data.put("content_length", upload.contentLength());
+ data.put("upload_url", upload.uploadUrl());
+ data.put("expiry_time", upload.expiryTime());
+ return data;
+ }
+
+ private Map toFileAttachment(NotionFileAttachmentSummary file) {
+ Map data = new LinkedHashMap<>();
+ data.put("name", file.name());
+ data.put("type", file.type());
+ data.put("url", file.url());
+ data.put("expiry_time", file.expiryTime());
+ data.put("source_kind", file.sourceKind());
+ data.put("source_name", file.sourceName());
+ return data;
+ }
+
private void refreshIndexesAfterMutation(Runnable ragSyncMutation) {
if (!Boolean.TRUE.equals(configService.getConfig().getLocalIndexEnabled())) {
runBestEffort(ragSyncMutation);
diff --git a/golemcore/notion/src/main/java/me/golemcore/plugins/golemcore/notion/NotionVaultToolProvider.java b/golemcore/notion/src/main/java/me/golemcore/plugins/golemcore/notion/NotionVaultToolProvider.java
index 957db22..1cc9eb4 100644
--- a/golemcore/notion/src/main/java/me/golemcore/plugins/golemcore/notion/NotionVaultToolProvider.java
+++ b/golemcore/notion/src/main/java/me/golemcore/plugins/golemcore/notion/NotionVaultToolProvider.java
@@ -6,6 +6,7 @@
import me.golemcore.plugin.api.extension.spi.ToolProvider;
import org.springframework.stereotype.Component;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
@@ -16,6 +17,8 @@ public class NotionVaultToolProvider implements ToolProvider {
private static final String TYPE = "type";
private static final String TYPE_OBJECT = "object";
private static final String TYPE_STRING = "string";
+ private static final String TYPE_INTEGER = "integer";
+ private static final String TYPE_BOOLEAN = "boolean";
private static final String PROPERTIES = "properties";
private static final String REQUIRED = "required";
private static final String PARAM_OPERATION = "operation";
@@ -23,8 +26,33 @@ public class NotionVaultToolProvider implements ToolProvider {
private static final String PARAM_QUERY = "query";
private static final String PARAM_LIMIT = "limit";
private static final String PARAM_CONTENT = "content";
+ private static final String PARAM_CONTENT_JSON = "content_json";
private static final String PARAM_TARGET_PATH = "target_path";
private static final String PARAM_NEW_NAME = "new_name";
+ private static final String PARAM_PARENT_PATH = "parent_path";
+ private static final String PARAM_TITLE = "title";
+ private static final String PARAM_DATABASE_ID = "database_id";
+ private static final String PARAM_DATA_SOURCE_ID = "data_source_id";
+ private static final String PARAM_PAGE_ID = "page_id";
+ private static final String PARAM_DESCRIPTION = "description";
+ private static final String PARAM_PROPERTIES_JSON = "properties_json";
+ private static final String PARAM_FILTER_JSON = "filter_json";
+ private static final String PARAM_SORTS_JSON = "sorts_json";
+ private static final String PARAM_CURSOR = "cursor";
+ private static final String PARAM_INLINE = "inline";
+ private static final String PARAM_ICON_EMOJI = "icon_emoji";
+ private static final String PARAM_COVER_URL = "cover_url";
+ private static final String PARAM_MODE = "mode";
+ private static final String PARAM_FILENAME = "filename";
+ private static final String PARAM_CONTENT_TYPE = "content_type";
+ private static final String PARAM_NUMBER_OF_PARTS = "number_of_parts";
+ private static final String PARAM_EXTERNAL_URL = "external_url";
+ private static final String PARAM_LOCAL_PATH = "local_path";
+ private static final String PARAM_FILE_UPLOAD_ID = "file_upload_id";
+ private static final String PARAM_FILE_NAME = "file_name";
+ private static final String PARAM_CAPTION = "caption";
+ private static final String PARAM_BLOCK_TYPE = "block_type";
+ private static final String PARAM_STATUS = "status";
private final NotionVaultService service;
@@ -34,50 +62,107 @@ public NotionVaultToolProvider(NotionVaultService service) {
@Override
public ToolDefinition getDefinition() {
+ Map properties = new LinkedHashMap<>();
+ properties.put(PARAM_OPERATION, Map.of(
+ TYPE, TYPE_STRING,
+ "enum", List.of(
+ "list_directory",
+ "search_notes",
+ "read_note",
+ "create_note",
+ "update_note",
+ "delete_note",
+ "move_note",
+ "rename_note",
+ "create_database",
+ "read_database",
+ "update_database",
+ "create_data_source",
+ "read_data_source",
+ "update_data_source",
+ "query_database",
+ "create_database_entry",
+ "read_database_entry",
+ "update_database_entry",
+ "list_page_files",
+ "create_file_upload",
+ "upload_file_content",
+ "complete_file_upload",
+ "read_file_upload",
+ "list_file_uploads",
+ "attach_file_to_page")));
+ properties.put(PARAM_QUERY, stringProperty("Full-text query against the local Notion index."));
+ properties.put(PARAM_PATH, stringProperty("Pseudo-path to a Notion page or subtree."));
+ properties.put(PARAM_LIMIT, integerProperty("Maximum number of results to return."));
+ properties.put(PARAM_CONTENT, stringProperty("Markdown content for note or page creation."));
+ properties.put(PARAM_CONTENT_JSON, stringProperty("JSON array of block children for page creation."));
+ properties.put(PARAM_TARGET_PATH, stringProperty("Target pseudo-path for move_note."));
+ properties.put(PARAM_NEW_NAME, stringProperty("New leaf page title for rename_note."));
+ properties.put(PARAM_PARENT_PATH,
+ stringProperty("Pseudo-path of the parent page used when creating a database."));
+ properties.put(PARAM_TITLE, stringProperty("Human-readable title for a database or data source."));
+ properties.put(PARAM_DATABASE_ID, stringProperty("Notion database ID."));
+ properties.put(PARAM_DATA_SOURCE_ID, stringProperty("Notion data source ID."));
+ properties.put(PARAM_PAGE_ID, stringProperty("Notion page ID."));
+ properties.put(PARAM_DESCRIPTION, stringProperty("Optional description text."));
+ properties.put(PARAM_PROPERTIES_JSON,
+ stringProperty("JSON object with database schema or page property values."));
+ properties.put(PARAM_FILTER_JSON, stringProperty("JSON object with a Notion data source filter."));
+ properties.put(PARAM_SORTS_JSON, stringProperty("JSON array with Notion sort definitions."));
+ properties.put(PARAM_CURSOR, stringProperty("Pagination cursor for data source or file upload listing."));
+ properties.put(PARAM_INLINE, booleanProperty("Whether the new database should be inline."));
+ properties.put(PARAM_ICON_EMOJI, stringProperty("Optional emoji icon."));
+ properties.put(PARAM_COVER_URL, stringProperty("Optional external URL for a page or database cover."));
+ properties.put(PARAM_MODE, stringProperty("File upload mode: single_part, multi_part, or external_url."));
+ properties.put(PARAM_FILENAME, stringProperty("Upload filename."));
+ properties.put(PARAM_CONTENT_TYPE, stringProperty("MIME type of the uploaded file."));
+ properties.put(PARAM_NUMBER_OF_PARTS, integerProperty("Number of parts for multi-part uploads."));
+ properties.put(PARAM_EXTERNAL_URL, stringProperty("Public external URL for importing or attaching a file."));
+ properties.put(PARAM_LOCAL_PATH, stringProperty("Local filesystem path used by upload_file_content."));
+ properties.put(PARAM_FILE_UPLOAD_ID, stringProperty("Notion file upload ID."));
+ properties.put(PARAM_FILE_NAME, stringProperty("Display file name when attaching a file block."));
+ properties.put(PARAM_CAPTION, stringProperty("Optional file block caption."));
+ properties.put(PARAM_BLOCK_TYPE, stringProperty("File block type: file, image, pdf, audio, or video."));
+ properties.put(PARAM_STATUS, stringProperty("Filter file uploads by status."));
+
+ Map schema = new LinkedHashMap<>();
+ schema.put(TYPE, TYPE_OBJECT);
+ schema.put(PROPERTIES, properties);
+ schema.put(REQUIRED, List.of(PARAM_OPERATION));
+ schema.put("allOf", List.of(
+ requiredWhen("search_notes", List.of(PARAM_QUERY)),
+ requiredWhen("read_note", List.of(PARAM_PATH)),
+ requiredWhen("create_note", List.of(PARAM_PATH, PARAM_CONTENT)),
+ requiredWhen("update_note", List.of(PARAM_PATH, PARAM_CONTENT)),
+ requiredWhen("delete_note", List.of(PARAM_PATH)),
+ requiredWhen("move_note", List.of(PARAM_PATH, PARAM_TARGET_PATH)),
+ requiredWhen("rename_note", List.of(PARAM_PATH, PARAM_NEW_NAME)),
+ requiredWhen("create_database", List.of(PARAM_PARENT_PATH, PARAM_TITLE)),
+ requiredWhen("read_database", List.of(PARAM_DATABASE_ID)),
+ requiredWhen("update_database", List.of(PARAM_DATABASE_ID)),
+ requiredWhen("create_data_source", List.of(PARAM_DATABASE_ID, PARAM_PROPERTIES_JSON)),
+ requiredWhen("read_data_source", List.of(PARAM_DATA_SOURCE_ID)),
+ requiredWhen("update_data_source", List.of(PARAM_DATA_SOURCE_ID)),
+ requiredWhenAny("query_database", List.of(
+ List.of(PARAM_DATABASE_ID),
+ List.of(PARAM_DATA_SOURCE_ID))),
+ requiredWhenAny("create_database_entry", List.of(
+ List.of(PARAM_DATABASE_ID, PARAM_PROPERTIES_JSON),
+ List.of(PARAM_DATA_SOURCE_ID, PARAM_PROPERTIES_JSON))),
+ requiredWhen("read_database_entry", List.of(PARAM_PAGE_ID)),
+ requiredWhen("update_database_entry", List.of(PARAM_PAGE_ID)),
+ requiredWhen("list_page_files", List.of(PARAM_PAGE_ID)),
+ requiredWhen("upload_file_content", List.of(PARAM_FILE_UPLOAD_ID, PARAM_LOCAL_PATH)),
+ requiredWhen("complete_file_upload", List.of(PARAM_FILE_UPLOAD_ID)),
+ requiredWhen("read_file_upload", List.of(PARAM_FILE_UPLOAD_ID)),
+ requiredWhenAny("attach_file_to_page", List.of(
+ List.of(PARAM_PAGE_ID, PARAM_FILE_UPLOAD_ID),
+ List.of(PARAM_PAGE_ID, PARAM_EXTERNAL_URL)))));
+
return ToolDefinition.builder()
.name("notion_vault")
- .description("Use Notion pages as a vault through the official Notion HTTP API.")
- .inputSchema(Map.of(
- TYPE, TYPE_OBJECT,
- PROPERTIES, Map.of(
- PARAM_OPERATION, Map.of(
- TYPE, TYPE_STRING,
- "enum", List.of(
- "list_directory",
- "search_notes",
- "read_note",
- "create_note",
- "update_note",
- "delete_note",
- "move_note",
- "rename_note")),
- PARAM_QUERY, Map.of(
- TYPE, TYPE_STRING,
- "description", "Full-text query against the local Notion index."),
- PARAM_PATH, Map.of(
- TYPE, TYPE_STRING,
- "description", "Pseudo-path to a Notion page or subtree."),
- PARAM_LIMIT, Map.of(
- TYPE, "integer",
- "description", "Maximum number of search results to return."),
- PARAM_CONTENT, Map.of(
- TYPE, TYPE_STRING,
- "description", "Markdown content for create_note or update_note."),
- PARAM_TARGET_PATH, Map.of(
- TYPE, TYPE_STRING,
- "description", "Target pseudo-path for move_note."),
- PARAM_NEW_NAME, Map.of(
- TYPE, TYPE_STRING,
- "description", "New leaf page title for rename_note.")),
- REQUIRED, List.of(PARAM_OPERATION),
- "allOf", List.of(
- requiredWhen("search_notes", List.of(PARAM_QUERY)),
- requiredWhen("read_note", List.of(PARAM_PATH)),
- requiredWhen("create_note", List.of(PARAM_PATH, PARAM_CONTENT)),
- requiredWhen("update_note", List.of(PARAM_PATH, PARAM_CONTENT)),
- requiredWhen("delete_note", List.of(PARAM_PATH)),
- requiredWhen("move_note", List.of(PARAM_PATH, PARAM_TARGET_PATH)),
- requiredWhen("rename_note", List.of(PARAM_PATH, PARAM_NEW_NAME)))))
+ .description("Use Notion pages, databases, and files through the official Notion HTTP API.")
+ .inputSchema(schema)
.build();
}
@@ -112,11 +197,94 @@ private ToolResult executeOperation(Map parameters) {
case "rename_note" -> service.renameNote(
readString(parameters.get(PARAM_PATH)),
readString(parameters.get(PARAM_NEW_NAME)));
+ case "create_database" -> service.createDatabase(
+ readString(parameters.get(PARAM_PARENT_PATH)),
+ readString(parameters.get(PARAM_TITLE)),
+ readString(parameters.get(PARAM_DESCRIPTION)),
+ readString(parameters.get(PARAM_PROPERTIES_JSON)),
+ readBoolean(parameters.get(PARAM_INLINE)),
+ readString(parameters.get(PARAM_ICON_EMOJI)),
+ readString(parameters.get(PARAM_COVER_URL)));
+ case "read_database" -> service.readDatabase(readString(parameters.get(PARAM_DATABASE_ID)));
+ case "update_database" -> service.updateDatabase(
+ readString(parameters.get(PARAM_DATABASE_ID)),
+ readString(parameters.get(PARAM_TITLE)),
+ readString(parameters.get(PARAM_DESCRIPTION)),
+ readString(parameters.get(PARAM_ICON_EMOJI)),
+ readString(parameters.get(PARAM_COVER_URL)));
+ case "create_data_source" -> service.createDataSource(
+ readString(parameters.get(PARAM_DATABASE_ID)),
+ readString(parameters.get(PARAM_TITLE)),
+ readString(parameters.get(PARAM_PROPERTIES_JSON)),
+ readString(parameters.get(PARAM_ICON_EMOJI)));
+ case "read_data_source" -> service.readDataSource(readString(parameters.get(PARAM_DATA_SOURCE_ID)));
+ case "update_data_source" -> service.updateDataSource(
+ readString(parameters.get(PARAM_DATA_SOURCE_ID)),
+ readString(parameters.get(PARAM_TITLE)),
+ readString(parameters.get(PARAM_PROPERTIES_JSON)),
+ readString(parameters.get(PARAM_ICON_EMOJI)));
+ case "query_database" -> service.queryDatabase(
+ readString(parameters.get(PARAM_DATABASE_ID)),
+ readString(parameters.get(PARAM_DATA_SOURCE_ID)),
+ readString(parameters.get(PARAM_FILTER_JSON)),
+ readString(parameters.get(PARAM_SORTS_JSON)),
+ readInteger(parameters.get(PARAM_LIMIT)),
+ readString(parameters.get(PARAM_CURSOR)));
+ case "create_database_entry" -> service.createDatabaseEntry(
+ readString(parameters.get(PARAM_DATABASE_ID)),
+ readString(parameters.get(PARAM_DATA_SOURCE_ID)),
+ readString(parameters.get(PARAM_PROPERTIES_JSON)),
+ readString(parameters.get(PARAM_CONTENT)),
+ readString(parameters.get(PARAM_CONTENT_JSON)),
+ readString(parameters.get(PARAM_ICON_EMOJI)),
+ readString(parameters.get(PARAM_COVER_URL)));
+ case "read_database_entry" -> service.readDatabaseEntry(readString(parameters.get(PARAM_PAGE_ID)));
+ case "update_database_entry" -> service.updateDatabaseEntry(
+ readString(parameters.get(PARAM_PAGE_ID)),
+ readString(parameters.get(PARAM_PROPERTIES_JSON)),
+ readString(parameters.get(PARAM_ICON_EMOJI)),
+ readString(parameters.get(PARAM_COVER_URL)));
+ case "list_page_files" -> service.listPageFiles(readString(parameters.get(PARAM_PAGE_ID)));
+ case "create_file_upload" -> service.createFileUpload(
+ readString(parameters.get(PARAM_MODE)),
+ readString(parameters.get(PARAM_FILENAME)),
+ readString(parameters.get(PARAM_CONTENT_TYPE)),
+ readInteger(parameters.get(PARAM_NUMBER_OF_PARTS)),
+ readString(parameters.get(PARAM_EXTERNAL_URL)));
+ case "upload_file_content" -> service.uploadFileContent(
+ readString(parameters.get(PARAM_FILE_UPLOAD_ID)),
+ readString(parameters.get(PARAM_LOCAL_PATH)),
+ readString(parameters.get(PARAM_CONTENT_TYPE)));
+ case "complete_file_upload" -> service.completeFileUpload(readString(parameters.get(PARAM_FILE_UPLOAD_ID)));
+ case "read_file_upload" -> service.readFileUpload(readString(parameters.get(PARAM_FILE_UPLOAD_ID)));
+ case "list_file_uploads" -> service.listFileUploads(
+ readString(parameters.get(PARAM_STATUS)),
+ readInteger(parameters.get(PARAM_LIMIT)),
+ readString(parameters.get(PARAM_CURSOR)));
+ case "attach_file_to_page" -> service.attachFileToPage(
+ readString(parameters.get(PARAM_PAGE_ID)),
+ readString(parameters.get(PARAM_FILE_UPLOAD_ID)),
+ readString(parameters.get(PARAM_EXTERNAL_URL)),
+ readString(parameters.get(PARAM_FILE_NAME)),
+ readString(parameters.get(PARAM_CAPTION)),
+ readString(parameters.get(PARAM_BLOCK_TYPE)));
default -> ToolResult.failure(ToolFailureKind.EXECUTION_FAILED,
"Unsupported notion_vault operation: " + operation);
};
}
+ private Map stringProperty(String description) {
+ return Map.of(TYPE, TYPE_STRING, "description", description);
+ }
+
+ private Map integerProperty(String description) {
+ return Map.of(TYPE, TYPE_INTEGER, "description", description);
+ }
+
+ private Map booleanProperty(String description) {
+ return Map.of(TYPE, TYPE_BOOLEAN, "description", description);
+ }
+
private Map requiredWhen(String operation, List requiredFields) {
return Map.of(
"if", Map.of(
@@ -126,6 +294,17 @@ private Map requiredWhen(String operation, List required
REQUIRED, requiredFields));
}
+ private Map requiredWhenAny(String operation, List> alternatives) {
+ return Map.of(
+ "if", Map.of(
+ PROPERTIES, Map.of(
+ PARAM_OPERATION, Map.of("const", operation))),
+ "then", Map.of(
+ "anyOf", alternatives.stream()
+ .map(required -> Map.of(REQUIRED, required))
+ .toList()));
+ }
+
private String readString(Object value) {
if (value instanceof String text) {
return text;
@@ -146,4 +325,14 @@ private Integer readInteger(Object value) {
}
return null;
}
+
+ private boolean readBoolean(Object value) {
+ if (value instanceof Boolean bool) {
+ return bool;
+ }
+ if (value instanceof String text && !text.isBlank()) {
+ return Boolean.parseBoolean(text);
+ }
+ return false;
+ }
}
diff --git a/golemcore/notion/src/main/java/me/golemcore/plugins/golemcore/notion/support/NotionApiClient.java b/golemcore/notion/src/main/java/me/golemcore/plugins/golemcore/notion/support/NotionApiClient.java
index b084166..fc1fb5e 100644
--- a/golemcore/notion/src/main/java/me/golemcore/plugins/golemcore/notion/support/NotionApiClient.java
+++ b/golemcore/notion/src/main/java/me/golemcore/plugins/golemcore/notion/support/NotionApiClient.java
@@ -5,6 +5,7 @@
import me.golemcore.plugins.golemcore.notion.NotionPluginConfig;
import me.golemcore.plugins.golemcore.notion.NotionPluginConfigService;
import okhttp3.MediaType;
+import okhttp3.MultipartBody;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
@@ -15,8 +16,13 @@
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
import java.util.ArrayList;
+import java.util.Base64;
+import java.util.LinkedHashMap;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@@ -24,6 +30,7 @@
public class NotionApiClient {
private static final MediaType JSON = MediaType.get("application/json; charset=utf-8");
+ private static final MediaType OCTET_STREAM = MediaType.get("application/octet-stream");
private final NotionPluginConfigService configService;
private final ObjectMapper objectMapper = new ObjectMapper();
@@ -41,10 +48,17 @@ public String retrievePageTitle(String pageId) {
}
public List listChildPages(String parentPageId) {
+ return listChildItems(parentPageId).stream()
+ .filter(child -> "page".equals(child.kind()))
+ .map(child -> new NotionPageSummary(child.id(), child.title(), child.url()))
+ .toList();
+ }
+
+ public List listChildItems(String parentPageId) {
if (parentPageId == null || parentPageId.isBlank()) {
throw new IllegalArgumentException("parentPageId is required");
}
- List pages = new ArrayList<>();
+ List items = new ArrayList<>();
String nextCursor = "";
boolean hasMore;
do {
@@ -57,18 +71,27 @@ public List listChildPages(String parentPageId) {
}
JsonNode response = getJson(pathBuilder.toString());
for (JsonNode result : response.path("results")) {
- if (!"child_page".equals(result.path("type").asText())) {
+ String type = result.path("type").asText("");
+ if ("child_page".equals(type)) {
+ items.add(new NotionChildSummary(
+ result.path("id").asText(),
+ result.path("child_page").path("title").asText(""),
+ result.path("url").asText(""),
+ "page"));
continue;
}
- pages.add(new NotionPageSummary(
- result.path("id").asText(),
- result.path("child_page").path("title").asText(),
- result.path("url").asText("")));
+ if ("child_database".equals(type)) {
+ items.add(new NotionChildSummary(
+ result.path("id").asText(),
+ result.path("child_database").path("title").asText(""),
+ result.path("url").asText(""),
+ "database"));
+ }
}
hasMore = response.path("has_more").asBoolean(false);
nextCursor = hasMore ? response.path("next_cursor").asText("") : "";
} while (hasMore && !nextCursor.isBlank());
- return pages;
+ return List.copyOf(items);
}
public String retrievePageMarkdown(String pageId) {
@@ -87,7 +110,7 @@ public NotionPageSummary createChildPage(String parentPageId, String title, Stri
throw new IllegalArgumentException("title is required");
}
JsonNode response = sendJson("POST", "/v1/pages", Map.of(
- "parent", Map.of("page_id", parentPageId),
+ "parent", Map.of("type", "page_id", "page_id", parentPageId),
"properties", Map.of(
"title", Map.of(
"title", List.of(Map.of(
@@ -137,17 +160,359 @@ public void renamePage(String pageId, String title) {
"text", Map.of("content", title)))))));
}
+ public NotionDatabaseSummary createDatabase(
+ String parentPageId,
+ String title,
+ String description,
+ Map properties,
+ boolean inline,
+ String iconEmoji,
+ String coverExternalUrl) {
+ if (parentPageId == null || parentPageId.isBlank()) {
+ throw new IllegalArgumentException("parentPageId is required");
+ }
+ Map payload = new LinkedHashMap<>();
+ payload.put("parent", Map.of("type", "page_id", "page_id", parentPageId));
+ payload.put("title", richTextArray(title));
+ if (description != null && !description.isBlank()) {
+ payload.put("description", richTextArray(description));
+ }
+ payload.put("is_inline", inline);
+ if (properties != null && !properties.isEmpty()) {
+ payload.put("initial_data_source", Map.of("properties", properties));
+ }
+ if (iconEmoji != null && !iconEmoji.isBlank()) {
+ payload.put("icon", Map.of("type", "emoji", "emoji", iconEmoji));
+ }
+ if (coverExternalUrl != null && !coverExternalUrl.isBlank()) {
+ payload.put("cover", Map.of(
+ "type", "external",
+ "external", Map.of("url", coverExternalUrl)));
+ }
+ JsonNode response = sendJson("POST", "/v1/databases", payload);
+ return toDatabaseSummary(response);
+ }
+
+ public NotionDatabaseSummary retrieveDatabase(String databaseId) {
+ if (databaseId == null || databaseId.isBlank()) {
+ throw new IllegalArgumentException("databaseId is required");
+ }
+ return toDatabaseSummary(getJson("/v1/databases/" + databaseId));
+ }
+
+ public NotionDatabaseSummary updateDatabase(
+ String databaseId,
+ String title,
+ String description,
+ String iconEmoji,
+ String coverExternalUrl) {
+ if (databaseId == null || databaseId.isBlank()) {
+ throw new IllegalArgumentException("databaseId is required");
+ }
+ Map payload = new LinkedHashMap<>();
+ if (title != null) {
+ payload.put("title", richTextArray(title));
+ }
+ if (description != null) {
+ payload.put("description", richTextArray(description));
+ }
+ if (iconEmoji != null) {
+ payload.put("icon", iconEmoji.isBlank()
+ ? null
+ : Map.of("type", "emoji", "emoji", iconEmoji));
+ }
+ if (coverExternalUrl != null) {
+ payload.put("cover", coverExternalUrl.isBlank()
+ ? null
+ : Map.of("type", "external", "external", Map.of("url", coverExternalUrl)));
+ }
+ JsonNode response = sendJson("PATCH", "/v1/databases/" + databaseId, payload);
+ return toDatabaseSummary(response);
+ }
+
+ public NotionDataSourceSummary createDataSource(
+ String databaseId,
+ String title,
+ Map properties,
+ String iconEmoji) {
+ if (databaseId == null || databaseId.isBlank()) {
+ throw new IllegalArgumentException("databaseId is required");
+ }
+ if (properties == null || properties.isEmpty()) {
+ throw new IllegalArgumentException("properties are required");
+ }
+ Map payload = new LinkedHashMap<>();
+ payload.put("parent", Map.of("type", "database_id", "database_id", databaseId));
+ payload.put("properties", properties);
+ if (title != null && !title.isBlank()) {
+ payload.put("title", richTextArray(title));
+ }
+ if (iconEmoji != null && !iconEmoji.isBlank()) {
+ payload.put("icon", Map.of("type", "emoji", "emoji", iconEmoji));
+ }
+ JsonNode response = sendJson("POST", "/v1/data_sources", payload);
+ return toDataSourceSummary(response);
+ }
+
+ public NotionDataSourceSummary retrieveDataSource(String dataSourceId) {
+ if (dataSourceId == null || dataSourceId.isBlank()) {
+ throw new IllegalArgumentException("dataSourceId is required");
+ }
+ return toDataSourceSummary(getJson("/v1/data_sources/" + dataSourceId));
+ }
+
+ public NotionDataSourceSummary updateDataSource(
+ String dataSourceId,
+ String title,
+ Map properties,
+ String iconEmoji) {
+ if (dataSourceId == null || dataSourceId.isBlank()) {
+ throw new IllegalArgumentException("dataSourceId is required");
+ }
+ Map payload = new LinkedHashMap<>();
+ if (title != null) {
+ payload.put("title", richTextArray(title));
+ }
+ if (properties != null) {
+ payload.put("properties", properties);
+ }
+ if (iconEmoji != null) {
+ payload.put("icon", iconEmoji.isBlank()
+ ? null
+ : Map.of("type", "emoji", "emoji", iconEmoji));
+ }
+ JsonNode response = sendJson("PATCH", "/v1/data_sources/" + dataSourceId, payload);
+ return toDataSourceSummary(response);
+ }
+
+ public NotionDataSourceQueryResult queryDataSource(
+ String dataSourceId,
+ String filterJson,
+ String sortsJson,
+ Integer limit,
+ String cursor) {
+ if (dataSourceId == null || dataSourceId.isBlank()) {
+ throw new IllegalArgumentException("dataSourceId is required");
+ }
+ Map payload = new LinkedHashMap<>();
+ if (filterJson != null && !filterJson.isBlank()) {
+ payload.put("filter", parseJsonObject(filterJson, "filterJson"));
+ }
+ if (sortsJson != null && !sortsJson.isBlank()) {
+ payload.put("sorts", parseJsonArray(sortsJson, "sortsJson"));
+ }
+ if (limit != null && limit > 0) {
+ payload.put("page_size", limit);
+ }
+ if (cursor != null && !cursor.isBlank()) {
+ payload.put("start_cursor", cursor);
+ }
+ JsonNode response = sendJson("POST", "/v1/data_sources/" + dataSourceId + "/query", payload);
+ List