Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions golemcore/notion/plugin.yaml
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion golemcore/notion/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<relativePath>../../pom.xml</relativePath>
</parent>

<version>1.0.1</version>
<version>1.1.0</version>
<artifactId>golemcore-notion-plugin</artifactId>
<name>golemcore/notion</name>
<description>Notion vault plugin for GolemCore</description>
Expand Down

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package me.golemcore.plugins.golemcore.notion.support;

public record NotionChildSummary(String id,String title,String url,String kind){}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package me.golemcore.plugins.golemcore.notion.support;

import java.util.List;
import java.util.Map;

public record NotionDataSourceQueryResult(String dataSourceId,int count,boolean hasMore,String nextCursor,List<Map<String,Object>>results){}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package me.golemcore.plugins.golemcore.notion.support;

import java.util.Map;

public record NotionDataSourceSummary(String id,String title,String url,Map<String,Object>properties){}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package me.golemcore.plugins.golemcore.notion.support;

import java.util.List;
import java.util.Map;

public record NotionDatabaseSummary(String id,String title,String url,List<Map<String,Object>>dataSources){}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package me.golemcore.plugins.golemcore.notion.support;

public record NotionFileAttachmentSummary(String name,String type,String url,String expiryTime,String sourceKind,String sourceName){}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package me.golemcore.plugins.golemcore.notion.support;

public record NotionFileUploadSummary(String id,String status,String filename,String contentType,Long contentLength,String uploadUrl,String expiryTime){}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package me.golemcore.plugins.golemcore.notion.support;

import java.util.List;
import java.util.Map;

public record NotionPageDetails(String id,String title,String url,List<NotionFileAttachmentSummary>files,Map<String,Object>rawProperties){}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import java.util.ArrayList;
Expand All @@ -15,13 +16,16 @@
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.NotionChildSummary;
import me.golemcore.plugins.golemcore.notion.support.NotionDatabaseSummary;
import me.golemcore.plugins.golemcore.notion.support.NotionFileUploadSummary;
import me.golemcore.plugins.golemcore.notion.support.NotionLocalIndexService;
import me.golemcore.plugins.golemcore.notion.support.NotionSearchHit;
import me.golemcore.plugins.golemcore.notion.support.NotionPageDetails;
import me.golemcore.plugins.golemcore.notion.support.NotionPageSummary;
import me.golemcore.plugins.golemcore.notion.support.NotionRagSyncService;
import me.golemcore.plugins.golemcore.notion.support.NotionSearchHit;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.mockito.Mockito.verify;

class NotionVaultServiceTest {

Expand Down Expand Up @@ -60,6 +64,12 @@ void shouldListRootChildrenUsingPseudoPaths() {
void shouldReadRootPageWhenPathIsBlankAndRespectMaxChars() {
when(configService.getConfig()).thenReturn(config(true, true, true, true, 5));
apiClient.pageMarkdown.put("root-page", "123456789");
apiClient.pageDetailsById.put("root-page", new NotionPageDetails(
"root-page",
"Root",
"https://notion.so/root-page",
List.of(),
Map.of()));

ToolResult result = service.readNote("");

Expand Down Expand Up @@ -159,6 +169,82 @@ void shouldMoveAndRenamePageWhenTargetPathChangesParentAndLeaf() {
"https://notion.so/todo-page");
}

@Test
void shouldCreateDatabaseUnderResolvedParentPage() {
apiClient.addChild("root-page", "projects-page", "Projects");
apiClient.createdDatabase = new NotionDatabaseSummary(
"db-1",
"Roadmap",
"https://notion.so/db-1",
List.of(Map.of("id", "ds-1", "name", "Roadmap")));

ToolResult result = service.createDatabase("Projects", "Roadmap", "desc", "{}", true, "🗺️", null);

assertTrue(result.isSuccess());
Map<?, ?> data = assertInstanceOf(Map.class, result.getData());
Map<?, ?> database = assertInstanceOf(Map.class, data.get("database"));
assertEquals("db-1", database.get("id"));
assertEquals("Projects", data.get("parent_path"));
assertEquals("Roadmap", apiClient.lastCreateDatabaseTitle);
assertEquals("projects-page", apiClient.lastCreateDatabaseParentId);
}

@Test
void shouldQueryDatabaseUsingSingleAvailableDataSource() {
apiClient.databaseById.put("db-1", new NotionDatabaseSummary(
"db-1",
"Roadmap",
"https://notion.so/db-1",
List.of(Map.of("id", "ds-1", "name", "Roadmap"))));
apiClient.queryResultsByDataSource.put("ds-1", List.of(Map.of(
"id", "page-1",
"title", "Item 1",
"url", "https://notion.so/page-1",
"properties", Map.of())));

ToolResult result = service.queryDatabase("db-1", null, null, null, 10, null);

assertTrue(result.isSuccess());
Map<?, ?> data = assertInstanceOf(Map.class, result.getData());
assertEquals("ds-1", data.get("data_source_id"));
assertEquals(1, data.get("count"));
}

@Test
void shouldAttachFileToPageUsingUploadId() {
apiClient.appendedBlock = new NotionPageSummary("block-1", "spec.pdf", "https://notion.so/block-1");

ToolResult result = service.attachFileToPage("page-1", "upload-1", null, "spec.pdf", "spec", "file");

assertTrue(result.isSuccess());
Map<?, ?> data = assertInstanceOf(Map.class, result.getData());
assertEquals("block-1", data.get("block_id"));
assertEquals("page-1", data.get("page_id"));
assertEquals("upload-1", apiClient.lastAppendFileUploadId);
}

@Test
void shouldListPageFiles() {
apiClient.pageDetailsById.put("page-1", new NotionPageDetails(
"page-1",
"Entry",
"https://notion.so/page-1",
List.of(new me.golemcore.plugins.golemcore.notion.support.NotionFileAttachmentSummary(
"spec.pdf",
"file",
"https://cdn.example/spec.pdf",
"2026-04-04T11:00:00Z",
"property",
"Files")),
Map.of()));

ToolResult result = service.listPageFiles("page-1");

assertTrue(result.isSuccess());
Map<?, ?> data = assertInstanceOf(Map.class, result.getData());
assertEquals(1, data.get("count"));
}

@Test
void shouldRejectDeletingConfiguredRootPage() {
ToolResult result = service.deleteNote("");
Expand Down Expand Up @@ -211,12 +297,20 @@ private static final class StubNotionApiClient extends NotionApiClient {
private final Map<String, List<NotionPageSummary>> childrenByParent = new LinkedHashMap<>();
private final Map<String, String> pageMarkdown = new LinkedHashMap<>();
private final Map<String, String> pageTitles = new LinkedHashMap<>();
private final Map<String, NotionDatabaseSummary> databaseById = new LinkedHashMap<>();
private final Map<String, NotionPageDetails> pageDetailsById = new LinkedHashMap<>();
private final Map<String, List<Map<String, Object>>> queryResultsByDataSource = new LinkedHashMap<>();
private final List<String> listChildCalls = new ArrayList<>();
private final List<String> readMarkdownCalls = new ArrayList<>();
private final List<CreateCall> createCalls = new ArrayList<>();
private final List<String> archiveCalls = new ArrayList<>();
private final List<MoveCall> moveCalls = new ArrayList<>();
private final List<RenameCall> renameCalls = new ArrayList<>();
private String lastCreateDatabaseParentId;
private String lastCreateDatabaseTitle;
private String lastAppendFileUploadId;
private NotionDatabaseSummary createdDatabase;
private NotionPageSummary appendedBlock;

private StubNotionApiClient(NotionPluginConfigService configService) {
super(configService);
Expand All @@ -234,6 +328,14 @@ public List<NotionPageSummary> listChildPages(String parentPageId) {
return childrenByParent.getOrDefault(parentPageId, List.of());
}

@Override
public List<NotionChildSummary> listChildItems(String parentPageId) {
listChildCalls.add(parentPageId);
return childrenByParent.getOrDefault(parentPageId, List.of()).stream()
.map(page -> new NotionChildSummary(page.id(), page.title(), page.url(), "page"))
.toList();
}

@Override
public String retrievePageMarkdown(String pageId) {
readMarkdownCalls.add(pageId);
Expand Down Expand Up @@ -261,6 +363,72 @@ public void renamePage(String pageId, String title) {
renameCalls.add(new RenameCall(pageId, title));
}

@Override
public NotionDatabaseSummary createDatabase(
String parentPageId,
String title,
String description,
Map<String, Object> properties,
boolean inline,
String iconEmoji,
String coverExternalUrl) {
lastCreateDatabaseParentId = parentPageId;
lastCreateDatabaseTitle = title;
return createdDatabase != null
? createdDatabase
: new NotionDatabaseSummary("db-1", title, "https://notion.so/db-1", List.of());
}

@Override
public NotionDatabaseSummary retrieveDatabase(String databaseId) {
return databaseById.get(databaseId);
}

@Override
public me.golemcore.plugins.golemcore.notion.support.NotionDataSourceQueryResult queryDataSource(
String dataSourceId,
String filterJson,
String sortsJson,
Integer limit,
String cursor) {
List<Map<String, Object>> results = queryResultsByDataSource.getOrDefault(dataSourceId, List.of());
return new me.golemcore.plugins.golemcore.notion.support.NotionDataSourceQueryResult(
dataSourceId,
results.size(),
false,
"",
results);
}

@Override
public NotionPageDetails retrievePageDetails(String pageId) {
return pageDetailsById.get(pageId);
}

@Override
public NotionPageSummary appendFileBlock(
String pageId,
String fileUploadId,
String externalUrl,
String fileName,
String caption,
String blockType) {
lastAppendFileUploadId = fileUploadId;
return appendedBlock != null
? appendedBlock
: new NotionPageSummary("block-1", fileName, "https://notion.so/block-1");
}

@Override
public NotionFileUploadSummary createFileUpload(
String mode,
String filename,
String contentType,
Integer numberOfParts,
String externalUrl) {
return new NotionFileUploadSummary("upload-1", "pending", filename, contentType, null, "", "");
}

private void addChild(String parentPageId, String pageId, String title) {
childrenByParent.computeIfAbsent(parentPageId, ignored -> new ArrayList<>())
.add(new NotionPageSummary(pageId, title, "https://notion.so/" + pageId));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,24 @@ void shouldExposeSupportedVaultOperations() {
"update_note",
"delete_note",
"move_note",
"rename_note"), operation.get("enum"));
"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"), operation.get("enum"));
}

@Test
Expand Down Expand Up @@ -156,6 +173,57 @@ void shouldDispatchRenameToVaultService() {
verify(service).renameNote("Projects/Todo", "Done");
}

@Test
void shouldDispatchCreateDatabaseToVaultService() {
when(service.createDatabase("Projects", "Roadmap", "desc", "{}", true, "🗺️", null))
.thenReturn(ToolResult.success("created-db"));

ToolResult result = provider.execute(Map.of(
"operation", "create_database",
"parent_path", "Projects",
"title", "Roadmap",
"description", "desc",
"properties_json", "{}",
"inline", true,
"icon_emoji", "🗺️")).join();

assertTrue(result.isSuccess());
verify(service).createDatabase("Projects", "Roadmap", "desc", "{}", true, "🗺️", null);
}

@Test
void shouldDefaultMissingInlineFlagToFalseWhenCreatingDatabase() {
when(service.createDatabase("Projects", "Roadmap", "desc", "{}", false, null, null))
.thenReturn(ToolResult.success("created-db"));

ToolResult result = provider.execute(Map.of(
"operation", "create_database",
"parent_path", "Projects",
"title", "Roadmap",
"description", "desc",
"properties_json", "{}")).join();

assertTrue(result.isSuccess());
verify(service).createDatabase("Projects", "Roadmap", "desc", "{}", false, null, null);
}

@Test
void shouldDispatchAttachFileToPageToVaultService() {
when(service.attachFileToPage("page-1", "upload-1", null, "spec.pdf", "spec", "file"))
.thenReturn(ToolResult.success("attached"));

ToolResult result = provider.execute(Map.of(
"operation", "attach_file_to_page",
"page_id", "page-1",
"file_upload_id", "upload-1",
"file_name", "spec.pdf",
"caption", "spec",
"block_type", "file")).join();

assertTrue(result.isSuccess());
verify(service).attachFileToPage("page-1", "upload-1", null, "spec.pdf", "spec", "file");
}

@Test
void shouldRejectUnsupportedOperation() {
ToolResult result = provider.execute(Map.of("operation", "unknown")).join();
Expand Down
Loading
Loading