Skip to content
Open
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
35 changes: 28 additions & 7 deletions spring-batch-notion/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,24 +32,45 @@ implementation("org.springframework.batch.extensions:spring-batch-notion:${sprin

The `NotionDatabaseItemReader` is a restartable `ItemReader` that reads entries from a [Notion Database] via a paging technique.

### Basic Usage (Automatic Data Source Discovery)

A minimal configuration of the item reader is as follows:

```java
NotionDatabaseItemReader<Item> itemReader() {
String token = System.getenv("NOTION_TOKEN");
String databaseId = "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"; // UUID
String databaseId = "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"; // Database ID (UUID)
PropertyMapper<Item> propertyMapper = new CustomPropertyMapper();
return new NotionDatabaseItemReader<>(token, databaseId, propertyMapper);
}
```

The following constructor parameters should be provided:
When `dataSourceId` is not provided as shown above, the reader automatically discovers it from the database. The reader will query the database metadata to retrieve the first available data source.

### Advanced Usage (Manual Data Source Selection)

If the given Notion database has multiple data sources, you can specify the data source ID directly:

```java
NotionDatabaseItemReader<Item> itemReader() {
String token = System.getenv("NOTION_TOKEN");
String databaseId = "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"; // Database ID (UUID)
String dataSourceId = "YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY"; // Data Source ID (UUID)
PropertyMapper<Item> propertyMapper = new CustomPropertyMapper();
return new NotionDatabaseItemReader<>(token, databaseId, dataSourceId, propertyMapper);
}
```

### Constructor Parameters

The constructor accepts the following parameters:

| Property | Description |
|------------------|---------------------------------------------------------------------------------------------------------------------------|
| `token` | The Notion integration token. |
| `databaseId` | UUID of the database to read from. |
| `propertyMapper` | The `PropertyMapper` responsible for mapping properties of a Notion item into a Java object. |
| Property | Required | Description |
|------------------|----------|----------------------------------------------------------------------------------------------------------------------------|
| `token` | yes | The Notion integration token. |
| `databaseId` | yes | UUID of the database to read from. |
| `dataSourceId` | no | UUID of the data source to query. If not provided, the reader will automatically discover the first available data source. |
| `propertyMapper` | yes | The `PropertyMapper` responsible for mapping properties of a Notion item into a Java object. |

and the following configuration options are available:

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright 2024-2026 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.batch.extensions.notion;

import tools.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy;
import tools.jackson.databind.annotation.JsonNaming;

import java.util.List;

/**
* @author Stefano Cordio
*/
@JsonNaming(SnakeCaseStrategy.class)
record DatabaseInfo(String id, List<DataSource> dataSources) {

@JsonNaming(SnakeCaseStrategy.class)
record DataSource(String id, String name) {
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright 2024-2026 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.batch.extensions.notion;

import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.service.annotation.HttpExchange;
import org.springframework.web.service.annotation.PostExchange;

@HttpExchange(url = "/data_sources", version = "2025-09-03", accept = MediaType.APPLICATION_JSON_VALUE)
interface NotionDataSourceService {

@PostExchange("/{dataSourceId}/query")
QueryResult query(@PathVariable String dataSourceId, @RequestBody QueryRequest request);

}
Original file line number Diff line number Diff line change
Expand Up @@ -71,14 +71,21 @@ public class NotionDatabaseItemReader<T> extends AbstractPaginatedDataItemReader

private Sort[] sorts = new Sort[0];

private @Nullable NotionDatabaseService service;
private @Nullable NotionDatabaseService databaseService;

private @Nullable NotionDataSourceService dataSourceService;

private @Nullable String dataSourceId;

private boolean hasMore;

private @Nullable String nextCursor;

/**
* Create a new {@link NotionDatabaseItemReader}.
* <p>
* This constructor automatically selects the first available data source from the
* database.
* @param token the Notion integration token
* @param databaseId UUID of the database to read from
* @param propertyMapper the {@link PropertyMapper} responsible for mapping properties
Expand All @@ -91,6 +98,27 @@ public NotionDatabaseItemReader(String token, String databaseId, PropertyMapper<
this.pageSize = DEFAULT_PAGE_SIZE;
}

/**
* Create a new {@link NotionDatabaseItemReader} with a specific data source ID.
* <p>
* This constructor allows you to specify the data source ID directly, bypassing the
* automatic discovery. This is useful when working with databases that have multiple
* data sources.
* @param token the Notion integration token
* @param databaseId UUID of the database to read from
* @param dataSourceId UUID of the data source to read from
* @param propertyMapper the {@link PropertyMapper} responsible for mapping properties
* of a Notion item into a Java object
*/
public NotionDatabaseItemReader(String token, String databaseId, String dataSourceId,
PropertyMapper<T> propertyMapper) {
this.token = Objects.requireNonNull(token);
this.databaseId = Objects.requireNonNull(databaseId);
this.dataSourceId = Objects.requireNonNull(dataSourceId);
this.propertyMapper = Objects.requireNonNull(propertyMapper);
this.pageSize = DEFAULT_PAGE_SIZE;
}

/**
* The base URL of the Notion API.
* <p>
Expand Down Expand Up @@ -155,7 +183,14 @@ protected void doOpen() {

RestClientAdapter adapter = RestClientAdapter.create(restClient);
HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build();
service = factory.createClient(NotionDatabaseService.class);

databaseService = factory.createClient(NotionDatabaseService.class);
dataSourceService = factory.createClient(NotionDataSourceService.class);

if (dataSourceId == null) {
DatabaseInfo databaseInfo = databaseService.getDatabase(databaseId);
dataSourceId = databaseInfo.dataSources().get(0).id();
}

hasMore = true;
}
Expand All @@ -172,7 +207,7 @@ protected Iterator<T> doPageRead() {
QueryRequest request = new QueryRequest(pageSize, nextCursor, filter, sorts);

@SuppressWarnings("DataFlowIssue")
QueryResult result = service.query(databaseId, request);
QueryResult result = dataSourceService.query(dataSourceId, request);

hasMore = result.hasMore();
nextCursor = result.nextCursor();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,13 @@

import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.service.annotation.GetExchange;
import org.springframework.web.service.annotation.HttpExchange;
import org.springframework.web.service.annotation.PostExchange;

@HttpExchange(url = "/databases", version = "2022-06-28", accept = MediaType.APPLICATION_JSON_VALUE)
@HttpExchange(url = "/databases", version = "2025-09-03", accept = MediaType.APPLICATION_JSON_VALUE)
interface NotionDatabaseService {

@PostExchange("/{databaseId}/query")
QueryResult query(@PathVariable String databaseId, @RequestBody QueryRequest request);
@GetExchange("/{databaseId}")
DatabaseInfo getDatabase(@PathVariable String databaseId);

}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,6 @@ public class RequestHeaders {

public static final String NOTION_VERSION = "Notion-Version";

public static final String NOTION_VERSION_VALUE = "2022-06-28";
public static final String NOTION_VERSION_VALUE = "2025-09-03";

}
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,46 @@
*/
public class ResponseBodies {

public static String queryResponse(JSONObject... results) {
return queryResponse(null, results);
public static String databaseInfoResponse(UUID databaseId, UUID dataSourceId) {
try {
return new JSONObject() //
.put("object", "database")
.put("id", databaseId.toString())
.put("data_sources", new JSONArray() //
.put(new JSONObject() //
.put("id", dataSourceId.toString())
.put("name", "default")))
.toString();
}
catch (JSONException e) {
throw new RuntimeException(e);
}
}

public static String databaseInfoResponse(UUID databaseId, UUID firstDataSourceId, UUID secondDataSourceId) {
try {
return new JSONObject() //
.put("object", "database")
.put("id", databaseId.toString())
.put("data_sources", new JSONArray() //
.put(new JSONObject() //
.put("id", firstDataSourceId.toString())
.put("name", "default"))
.put(new JSONObject() //
.put("id", secondDataSourceId.toString())
.put("name", "secondary")))
.toString();
}
catch (JSONException e) {
throw new RuntimeException(e);
}
}

public static String datasourceQueryResponse(JSONObject... results) {
return datasourceQueryResponse(null, results);
}

public static String queryResponse(UUID nextCursor, JSONObject... results) {
public static String datasourceQueryResponse(UUID nextCursor, JSONObject... results) {
try {
return new JSONObject() //
.put("object", "list")
Expand All @@ -50,7 +85,7 @@ public static String queryResponse(UUID nextCursor, JSONObject... results) {
}
}

public static JSONObject result(UUID id, UUID databaseId, Map<?, ?> properties) {
public static JSONObject result(UUID id, UUID dataSourceId, Map<?, ?> properties) {
try {
Instant now = Instant.now();

Expand All @@ -62,8 +97,9 @@ public static JSONObject result(UUID id, UUID databaseId, Map<?, ?> properties)
.put("created_by", new JSONObject())
.put("last_edited_by", new JSONObject())
.put("parent", new JSONObject() //
.put("type", "database_id")
.put("database_id", databaseId.toString()))
.put("type", "data_source_id")
.put("data_source_id", dataSourceId.toString())
.put("database_id", randomUUID().toString()))
.put("archived", false)
.put("properties", new JSONObject(properties))
.put("url", "https://www.notion.so/" + randomUUID().toString().replace("-", ""));
Expand Down
Loading