gatsby-source-notion-churnotion 1.1.28 → 1.1.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -1
- package/dist/api/getPages.js +235 -213
- package/dist/api/service/index.d.ts +1 -0
- package/dist/api/service/index.js +17 -0
- package/dist/api/service/notionService.d.ts +39 -0
- package/dist/api/service/notionService.js +174 -0
- package/dist/util/blocks/blockProcessor.d.ts +35 -0
- package/dist/util/blocks/blockProcessor.js +44 -0
- package/dist/util/blocks/blockProcessorRegistry.d.ts +10 -0
- package/dist/util/blocks/blockProcessorRegistry.js +40 -0
- package/dist/util/blocks/imageBlockProcessor.d.ts +6 -0
- package/dist/util/blocks/imageBlockProcessor.js +113 -0
- package/dist/util/blocks/index.d.ts +6 -0
- package/dist/util/blocks/index.js +22 -0
- package/dist/util/blocks/mediaBlockProcessor.d.ts +6 -0
- package/dist/util/blocks/mediaBlockProcessor.js +48 -0
- package/dist/util/blocks/structureBlockProcessor.d.ts +6 -0
- package/dist/util/blocks/structureBlockProcessor.js +81 -0
- package/dist/util/blocks/textBlockProcessor.d.ts +6 -0
- package/dist/util/blocks/textBlockProcessor.js +23 -0
- package/dist/util/processor.js +35 -149
- package/dist/util/timeLimit.d.ts +8 -0
- package/dist/util/timeLimit.js +25 -1
- package/package.json +2 -1
package/README.md
CHANGED
@@ -6,6 +6,21 @@ This plugin recursively collects categories from a single Notion database, which
|
|
6
6
|
|
7
7
|
If you're considering Notion as your CMS for Gatsby, this plugin could be a great choice as it supports recursive category collection.
|
8
8
|
|
9
|
+
## What's New in v1.1.29
|
10
|
+
|
11
|
+
- **Added support for more Notion block types**:
|
12
|
+
- bookmark, breadcrumb, callout, code, column, column_list, divider, embed, equation, file, link_preview, pdf, table, table_of_contents, toggle, to_do, video, audio
|
13
|
+
- **Improved performance**:
|
14
|
+
- Added parallel processing for Notion API requests with concurrency limits
|
15
|
+
- Implemented caching to reduce duplicate API calls
|
16
|
+
- Added batch processing for large datasets
|
17
|
+
- Added timeout handling for long-running operations
|
18
|
+
- **Code refactoring**:
|
19
|
+
- Modular block processor architecture
|
20
|
+
- Better error handling
|
21
|
+
- Improved type safety
|
22
|
+
- Fixed ES Module import issue with p-limit
|
23
|
+
|
9
24
|
## Install
|
10
25
|
|
11
26
|
```shell
|
@@ -64,7 +79,7 @@ When the development server is running, `gatsby-source-notion-churnotion` will f
|
|
64
79
|
|
65
80
|
### Explore in GraphQL
|
66
81
|
|
67
|
-
Once the data is fetched, go to http://localhost:8000/__graphql, where you
|
82
|
+
Once the data is fetched, go to http://localhost:8000/__graphql, where you'll find new nodes such as `Churnotion`, `NBook`, `NCategory`, and `NTag` as shown below:
|
68
83
|
|
69
84
|

|
70
85
|
|
package/dist/api/getPages.js
CHANGED
@@ -6,237 +6,122 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.getPages = void 0;
|
7
7
|
const crypto_1 = __importDefault(require("crypto"));
|
8
8
|
const constants_1 = require("../constants");
|
9
|
-
const fetchData_1 = require("../util/fetchData");
|
10
9
|
const processor_1 = require("../util/processor");
|
11
10
|
const slugify_1 = require("../util/slugify");
|
12
|
-
const bookCategoryMap_1 = __importDefault(require("../util/bookCategoryMap"));
|
13
11
|
const formatDate_1 = require("../util/formatDate");
|
12
|
+
const service_1 = require("./service");
|
13
|
+
const timeLimit_1 = require("../util/timeLimit");
|
14
|
+
// 최대 동시 요청 수 설정
|
15
|
+
const MAX_CONCURRENT_REQUESTS = 5;
|
14
16
|
const getPages = async ({ databaseId, reporter, getCache, actions, createNode, createNodeId, createParentChildLink, getNode, cache, }) => {
|
17
|
+
// Notion Service 초기화
|
18
|
+
const notionService = new service_1.NotionService({
|
19
|
+
reporter,
|
20
|
+
parallelLimit: MAX_CONCURRENT_REQUESTS,
|
21
|
+
enableCaching: true,
|
22
|
+
});
|
23
|
+
// 태그 매핑을 위한 객체
|
24
|
+
const tagMap = {};
|
15
25
|
/**
|
16
26
|
* 데이터베이스 내에 페이지들을 읽어서 재귀적으로 추가하는 서브 메서드드
|
17
27
|
* @param databaseId 데이터베이스 아이디
|
18
28
|
* @param parentCategoryId 부모 데이터베이스 아이디
|
19
29
|
*/
|
20
|
-
const processDatabase = async (databaseId, parentCategoryId = null, categoryPath = [],
|
30
|
+
const processDatabase = async (databaseId, parentCategoryId = null, categoryPath = [], parentCategoryUrl = ``) => {
|
21
31
|
let hasMore = true;
|
22
32
|
try {
|
33
|
+
// 동시에 처리될 페이지 목록
|
34
|
+
const pagesToProcess = [];
|
23
35
|
while (hasMore) {
|
24
|
-
|
25
|
-
const
|
26
|
-
|
36
|
+
// 데이터베이스 쿼리
|
37
|
+
const result = await notionService.queryDatabase(databaseId);
|
38
|
+
hasMore = false; // Notion API가 페이지네이션을 완전히 지원하지 않으므로 일단 한 번만 처리
|
27
39
|
if (result?.results?.length) {
|
28
40
|
reporter.info(`[SUCCESS] total pages > ${result.results.length}`);
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
const
|
36
|
-
|
37
|
-
const
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
category_name: title,
|
46
|
-
parent: parentCategoryId,
|
47
|
-
slug: slug,
|
48
|
-
children: [],
|
49
|
-
internal: {
|
50
|
-
type: constants_1.NODE_TYPE.Category,
|
51
|
-
contentDigest: crypto_1.default
|
52
|
-
.createHash(`md5`)
|
53
|
-
.update(JSON.stringify(categoryJsonData))
|
54
|
-
.digest(`hex`),
|
55
|
-
},
|
56
|
-
url: `${constants_1.COMMON_URI}/${constants_1.CATEGORY_URI}${categoryUrl}`,
|
57
|
-
books: [],
|
58
|
-
};
|
59
|
-
await createNode(categoryNode);
|
60
|
-
const bookRelations = page.properties?.book?.relation || null;
|
61
|
-
if (bookRelations) {
|
62
|
-
bookRelations.forEach((relation) => {
|
63
|
-
const bookId = relation.id;
|
64
|
-
const bookNodeId = createNodeId(`${bookId}-book`);
|
65
|
-
const bookNode = getNode(bookNodeId);
|
66
|
-
if (bookNode) {
|
67
|
-
createParentChildLink({
|
68
|
-
parent: categoryNode,
|
69
|
-
child: bookNode,
|
70
|
-
});
|
71
|
-
const updatedBookNode = {
|
72
|
-
...bookNode,
|
73
|
-
book_category: categoryNode.id,
|
74
|
-
internal: {
|
75
|
-
type: bookNode.internal.type,
|
76
|
-
contentDigest: crypto_1.default
|
77
|
-
.createHash(`md5`)
|
78
|
-
.update(JSON.stringify(bookNode))
|
79
|
-
.digest(`hex`),
|
80
|
-
},
|
81
|
-
};
|
82
|
-
createNode(updatedBookNode);
|
83
|
-
reporter.info(`[SUCCESS] Linked Category-Book: ${categoryNode.category_name} -> child: ${bookNode.book_name}`);
|
84
|
-
}
|
85
|
-
});
|
86
|
-
}
|
87
|
-
if (parentCategoryId && categoryNode) {
|
88
|
-
const parentNode = getNode(parentCategoryId); // Gatsby에서 노드를 검색
|
89
|
-
if (parentNode) {
|
90
|
-
createParentChildLink({
|
91
|
-
parent: parentNode,
|
92
|
-
child: categoryNode,
|
93
|
-
});
|
94
|
-
reporter.info(`[SUCCESS] Linked parent: ${parentNode.category_name} -> child: ${categoryNode.category_name}`);
|
95
|
-
}
|
96
|
-
else {
|
97
|
-
reporter.warn(`[WARNING] Parent node not found for ID: ${parentCategoryId}`);
|
98
|
-
}
|
99
|
-
}
|
100
|
-
const newCategoryPath = [...categoryPath, categoryNode];
|
101
|
-
await processDatabase(categoryJsonData.id, nodeId, newCategoryPath, tagMap, categoryUrl);
|
102
|
-
}
|
103
|
-
else {
|
104
|
-
// 페이지인 경우
|
105
|
-
const title = page.properties?.[`이름`]?.title?.[0]?.plain_text || `Unnamed`;
|
106
|
-
const slug = (0, slugify_1.slugify)(page.properties?.[`slug`]?.rich_text?.[0]?.plain_text ||
|
107
|
-
crypto_1.default
|
108
|
-
.createHash(`md5`)
|
109
|
-
.update(JSON.stringify(title))
|
110
|
-
.digest(`hex`));
|
111
|
-
if (!page.properties?.[`이름`]?.title?.[0]?.plain_text) {
|
112
|
-
reporter.warn(`[WARNING] Category without a title detected: ${page.id}`);
|
113
|
-
}
|
114
|
-
const nodeId = createNodeId(`${page.id}-page`);
|
115
|
-
// Tag 노드 만들기
|
116
|
-
const tagIds = [];
|
117
|
-
if (page.properties.tags && page.properties.tags.multi_select) {
|
118
|
-
page.properties.tags.multi_select.forEach((tagData) => {
|
119
|
-
if (tagMap[tagData.name]) {
|
120
|
-
// 이미 존재하는 태그라면 tagMap에서 가져오기
|
121
|
-
const existingTagId = tagMap[tagData.name];
|
122
|
-
tagIds.push(existingTagId); // 기존 태그 ID 추가
|
123
|
-
reporter.info(`[INFO] Reusing existing tag: ${tagData.name}`);
|
124
|
-
}
|
125
|
-
else {
|
126
|
-
// 새로운 태그 생성
|
127
|
-
const tagNodeId = createNodeId(`${tagData.id}-tag`);
|
128
|
-
tagMap[tagData.name] = tagNodeId; // tagMap에 저장
|
129
|
-
tagIds.push(tagNodeId); // 새로운 태그 ID 추가
|
130
|
-
const slug = (0, slugify_1.slugify)(tagData.name) || `no-tag-${tagNodeId}`;
|
131
|
-
// 태그 노드 생성
|
132
|
-
const tagNode = {
|
133
|
-
id: tagNodeId,
|
134
|
-
tag_name: tagData.name,
|
135
|
-
slug: slug,
|
136
|
-
color: tagData.color,
|
137
|
-
children: [],
|
138
|
-
internal: {
|
139
|
-
type: constants_1.NODE_TYPE.Tag,
|
140
|
-
contentDigest: crypto_1.default
|
141
|
-
.createHash(`md5`)
|
142
|
-
.update(JSON.stringify(tagData))
|
143
|
-
.digest(`hex`),
|
144
|
-
},
|
145
|
-
url: `${constants_1.COMMON_URI}/${constants_1.TAG_URI}/${slug}`,
|
146
|
-
churnotions: [],
|
147
|
-
parent: null,
|
148
|
-
};
|
149
|
-
createNode(tagNode);
|
150
|
-
reporter.info(`[SUCCESS] Created new tag: ${tagData.name}`);
|
151
|
-
}
|
152
|
-
});
|
153
|
-
}
|
154
|
-
const bookId = page.properties?.book?.relation?.[0]?.id || null;
|
155
|
-
const bookNodeId = createNodeId(`${bookId}-book`);
|
156
|
-
const bookNode = getNode(bookNodeId);
|
157
|
-
const [imageNode, tableOfContents, updatedBlocks, rawText] = await (0, processor_1.processor)(pageData.results, actions, getCache, createNodeId, reporter, cache);
|
158
|
-
const postNode = {
|
159
|
-
id: nodeId,
|
160
|
-
category: parentCategoryId,
|
161
|
-
book: bookNode?.id,
|
162
|
-
book_index: page.properties?.bookIndex?.number || 0,
|
163
|
-
title: title,
|
164
|
-
content: updatedBlocks,
|
165
|
-
create_date: (0, formatDate_1.useFormatDate)(page.created_time),
|
166
|
-
update_date: (0, formatDate_1.useFormatDate)(page.last_edited_time),
|
167
|
-
version: page.properties?.version?.number || null,
|
168
|
-
description: page.properties?.description?.rich_text?.[0]?.plain_text ||
|
169
|
-
rawText.substring(0, 400),
|
170
|
-
slug: slug,
|
171
|
-
category_list: categoryPath,
|
172
|
-
children: [],
|
173
|
-
tableOfContents,
|
174
|
-
internal: {
|
175
|
-
type: constants_1.NODE_TYPE.Post,
|
176
|
-
contentDigest: crypto_1.default
|
177
|
-
.createHash(`md5`)
|
178
|
-
.update(JSON.stringify(nodeId))
|
179
|
-
.digest(`hex`),
|
180
|
-
},
|
181
|
-
tags: tagIds,
|
182
|
-
parent: null,
|
183
|
-
url: `${constants_1.COMMON_URI}/${constants_1.POST_URI}${parentCategoryUrl}/${slug}`,
|
184
|
-
thumbnail: imageNode,
|
185
|
-
rawText,
|
186
|
-
};
|
187
|
-
await createNode(postNode);
|
188
|
-
if (bookNode) {
|
189
|
-
createParentChildLink({
|
190
|
-
parent: bookNode,
|
191
|
-
child: postNode,
|
192
|
-
});
|
193
|
-
reporter.info(`[SUCCESS] Linked book: ${bookNode.book_name} -> page: ${postNode.title}`);
|
194
|
-
}
|
195
|
-
// tag와 post 부모-자식 관계 설정
|
196
|
-
tagIds.forEach((tagId) => {
|
197
|
-
const tagNode = getNode(tagId);
|
198
|
-
if (tagNode) {
|
199
|
-
createParentChildLink({
|
200
|
-
parent: tagNode,
|
201
|
-
child: postNode,
|
202
|
-
});
|
203
|
-
reporter.info(`[SUCCESS] Linked tag: ${tagNode.tag_name} -> page: ${postNode.title}`);
|
204
|
-
}
|
205
|
-
else {
|
206
|
-
reporter.warn(`[WARNING] Tag node not found for ID: ${tagId}`);
|
207
|
-
}
|
208
|
-
});
|
209
|
-
// category와 post 부모-자식 관계 설정
|
210
|
-
if (parentCategoryId && postNode) {
|
211
|
-
const parentNode = getNode(parentCategoryId); // Gatsby에서 노드를 검색
|
212
|
-
if (parentNode) {
|
213
|
-
createParentChildLink({
|
214
|
-
parent: parentNode,
|
215
|
-
child: postNode,
|
216
|
-
});
|
217
|
-
reporter.info(`[SUCCESS] Linked parent: ${parentNode.category_name} -> child: ${postNode.title}`);
|
218
|
-
}
|
219
|
-
else {
|
220
|
-
reporter.warn(`[WARNING] Parent node not found for ID: ${parentCategoryId}`);
|
41
|
+
// 페이지 ID 목록 수집
|
42
|
+
const pageIds = result.results.map((page) => page.id);
|
43
|
+
// 페이지 블록들을 병렬로 가져오기 - 최대 50개씩 배치 처리
|
44
|
+
for (let i = 0; i < pageIds.length; i += 50) {
|
45
|
+
const batch = pageIds.slice(i, i + 50);
|
46
|
+
reporter.info(`[BATCH] Processing pages ${i + 1} to ${i + batch.length} of ${pageIds.length}`);
|
47
|
+
const batchBlocks = await notionService.getMultiplePagesBlocks(batch);
|
48
|
+
// 페이지 데이터와 블록 결합
|
49
|
+
for (const pageId of batch) {
|
50
|
+
const page = result.results.find((p) => p.id === pageId);
|
51
|
+
if (page) {
|
52
|
+
const pageData = {
|
53
|
+
page,
|
54
|
+
blocks: batchBlocks[pageId] || [],
|
55
|
+
};
|
56
|
+
pagesToProcess.push(pageData);
|
221
57
|
}
|
222
58
|
}
|
223
59
|
}
|
224
60
|
}
|
225
|
-
hasMore = result.has_more;
|
226
61
|
}
|
62
|
+
// 모든 페이지 병렬 처리
|
63
|
+
await Promise.all(pagesToProcess.map(async ({ page, blocks }) => {
|
64
|
+
try {
|
65
|
+
// Timeout 설정으로 너무 오래 걸리는 페이지는 건너뛰기
|
66
|
+
await (0, timeLimit_1.timeLimit)(processPageData(page, blocks, parentCategoryId, categoryPath, tagMap, parentCategoryUrl), 30000, // 30초 제한
|
67
|
+
`Processing page ${page.id} timed out after 30 seconds`);
|
68
|
+
}
|
69
|
+
catch (error) {
|
70
|
+
reporter.warn(`[WARNING] Error processing page ${page.id}: ${error}`);
|
71
|
+
}
|
72
|
+
}));
|
227
73
|
}
|
228
74
|
catch (error) {
|
229
|
-
reporter.error(`[ERROR]
|
230
|
-
hasMore = false;
|
75
|
+
reporter.error(`[ERROR] Processing database ${databaseId} failed: ${error}`);
|
231
76
|
}
|
232
77
|
};
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
78
|
+
/**
|
79
|
+
* 페이지 데이터 처리 메서드
|
80
|
+
*/
|
81
|
+
const processPageData = async (page, pageBlocks, parentCategoryId, categoryPath, tagMap, parentCategoryUrl) => {
|
82
|
+
// 첫 번째 블록이 child_database인지 확인
|
83
|
+
if (pageBlocks?.[0]?.type === `child_database`) {
|
84
|
+
await processCategory(page, pageBlocks, parentCategoryId, categoryPath, tagMap, parentCategoryUrl);
|
85
|
+
}
|
86
|
+
else {
|
87
|
+
await processPost(page, pageBlocks, parentCategoryId, categoryPath, tagMap, parentCategoryUrl);
|
88
|
+
}
|
89
|
+
};
|
90
|
+
/**
|
91
|
+
* 카테고리 처리 메서드
|
92
|
+
*/
|
93
|
+
const processCategory = async (page, pageBlocks, parentCategoryId, categoryPath, tagMap, parentCategoryUrl) => {
|
94
|
+
const categoryJsonData = pageBlocks[0];
|
95
|
+
const title = categoryJsonData.child_database?.title || `Unnamed Category`;
|
96
|
+
const slug = (0, slugify_1.slugify)(title) || `no-title-${categoryJsonData.id}`;
|
97
|
+
if (!title) {
|
98
|
+
reporter.warn(`[WARNING] Category without a title detected: ${categoryJsonData.id}`);
|
99
|
+
}
|
100
|
+
const nodeId = createNodeId(`${categoryJsonData.id}-category`);
|
101
|
+
const categoryUrl = `${parentCategoryUrl}/${slug}`;
|
102
|
+
const categoryNode = {
|
103
|
+
id: nodeId,
|
104
|
+
category_name: title,
|
105
|
+
parent: parentCategoryId,
|
106
|
+
slug: slug,
|
107
|
+
children: [],
|
108
|
+
internal: {
|
109
|
+
type: constants_1.NODE_TYPE.Category,
|
110
|
+
contentDigest: crypto_1.default
|
111
|
+
.createHash(`md5`)
|
112
|
+
.update(JSON.stringify(categoryJsonData))
|
113
|
+
.digest(`hex`),
|
114
|
+
},
|
115
|
+
url: `${constants_1.COMMON_URI}/${constants_1.CATEGORY_URI}${categoryUrl}`,
|
116
|
+
books: [],
|
117
|
+
};
|
118
|
+
await createNode(categoryNode);
|
119
|
+
const bookRelations = page.properties?.book?.relation || null;
|
120
|
+
if (bookRelations) {
|
121
|
+
bookRelations.forEach((relation) => {
|
122
|
+
const bookId = relation.id;
|
123
|
+
const bookNodeId = createNodeId(`${bookId}-book`);
|
124
|
+
const bookNode = getNode(bookNodeId);
|
240
125
|
if (bookNode) {
|
241
126
|
createParentChildLink({
|
242
127
|
parent: categoryNode,
|
@@ -254,16 +139,153 @@ const getPages = async ({ databaseId, reporter, getCache, actions, createNode, c
|
|
254
139
|
},
|
255
140
|
};
|
256
141
|
createNode(updatedBookNode);
|
257
|
-
reporter.info(`[SUCCESS] Linked Book
|
142
|
+
reporter.info(`[SUCCESS] Linked Category-Book: ${categoryNode.category_name} -> child: ${bookNode.book_name}`);
|
143
|
+
}
|
144
|
+
});
|
145
|
+
}
|
146
|
+
if (parentCategoryId && categoryNode) {
|
147
|
+
const parentNode = getNode(parentCategoryId);
|
148
|
+
if (parentNode) {
|
149
|
+
createParentChildLink({
|
150
|
+
parent: parentNode,
|
151
|
+
child: categoryNode,
|
152
|
+
});
|
153
|
+
reporter.info(`[SUCCESS] Linked parent: ${parentNode.category_name} -> child: ${categoryNode.category_name}`);
|
154
|
+
}
|
155
|
+
else {
|
156
|
+
reporter.warn(`[WARNING] Parent node not found for ID: ${parentCategoryId}`);
|
157
|
+
}
|
158
|
+
}
|
159
|
+
const newCategoryPath = [...categoryPath, categoryNode];
|
160
|
+
await processDatabase(categoryJsonData.id, nodeId, newCategoryPath, categoryUrl);
|
161
|
+
};
|
162
|
+
/**
|
163
|
+
* 포스트 처리 메서드
|
164
|
+
*/
|
165
|
+
const processPost = async (page, pageBlocks, parentCategoryId, categoryPath, tagMap, parentCategoryUrl) => {
|
166
|
+
const title = page.properties?.[`이름`]?.title?.[0]?.plain_text || `Unnamed`;
|
167
|
+
const slug = (0, slugify_1.slugify)(page.properties?.[`slug`]?.rich_text?.[0]?.plain_text ||
|
168
|
+
crypto_1.default.createHash(`md5`).update(JSON.stringify(title)).digest(`hex`));
|
169
|
+
if (!page.properties?.[`이름`]?.title?.[0]?.plain_text) {
|
170
|
+
reporter.warn(`[WARNING] Post without a title detected: ${page.id}`);
|
171
|
+
}
|
172
|
+
const nodeId = createNodeId(`${page.id}-page`);
|
173
|
+
// Tag 노드 만들기
|
174
|
+
const tagIds = [];
|
175
|
+
if (page.properties.tags && page.properties.tags.multi_select) {
|
176
|
+
page.properties.tags.multi_select.forEach((tagData) => {
|
177
|
+
if (tagMap[tagData.name]) {
|
178
|
+
// 이미 존재하는 태그라면 tagMap에서 가져오기
|
179
|
+
const existingTagId = tagMap[tagData.name];
|
180
|
+
tagIds.push(existingTagId); // 기존 태그 ID 추가
|
181
|
+
reporter.info(`[INFO] Reusing existing tag: ${tagData.name}`);
|
258
182
|
}
|
259
183
|
else {
|
260
|
-
|
184
|
+
// 새로운 태그 생성
|
185
|
+
const tagNodeId = createNodeId(`${tagData.id}-tag`);
|
186
|
+
tagMap[tagData.name] = tagNodeId; // tagMap에 저장
|
187
|
+
tagIds.push(tagNodeId); // 새로운 태그 ID 추가
|
188
|
+
const slug = (0, slugify_1.slugify)(tagData.name) || `no-tag-${tagNodeId}`;
|
189
|
+
// 태그 노드 생성
|
190
|
+
const tagNode = {
|
191
|
+
id: tagNodeId,
|
192
|
+
tag_name: tagData.name,
|
193
|
+
slug: slug,
|
194
|
+
color: tagData.color,
|
195
|
+
children: [],
|
196
|
+
internal: {
|
197
|
+
type: constants_1.NODE_TYPE.Tag,
|
198
|
+
contentDigest: crypto_1.default
|
199
|
+
.createHash(`md5`)
|
200
|
+
.update(JSON.stringify(tagData))
|
201
|
+
.digest(`hex`),
|
202
|
+
},
|
203
|
+
url: `${constants_1.COMMON_URI}/${constants_1.TAG_URI}/${slug}`,
|
204
|
+
churnotions: [],
|
205
|
+
parent: null,
|
206
|
+
};
|
207
|
+
createNode(tagNode);
|
208
|
+
reporter.info(`[SUCCESS] Created new tag: ${tagData.name}`);
|
261
209
|
}
|
210
|
+
});
|
211
|
+
}
|
212
|
+
const bookId = page.properties?.book?.relation?.[0]?.id || null;
|
213
|
+
const bookNodeId = createNodeId(`${bookId}-book`);
|
214
|
+
const bookNode = getNode(bookNodeId);
|
215
|
+
const [imageNode, tableOfContents, updatedBlocks, rawText] = await (0, processor_1.processor)(pageBlocks, actions, getCache, createNodeId, reporter, cache);
|
216
|
+
const postNode = {
|
217
|
+
id: nodeId,
|
218
|
+
category: parentCategoryId,
|
219
|
+
book: bookNode?.id,
|
220
|
+
book_index: page.properties?.bookIndex?.number || 0,
|
221
|
+
title: title,
|
222
|
+
content: updatedBlocks,
|
223
|
+
create_date: (0, formatDate_1.useFormatDate)(page.created_time),
|
224
|
+
update_date: (0, formatDate_1.useFormatDate)(page.last_edited_time),
|
225
|
+
version: page.properties?.version?.number || null,
|
226
|
+
description: page.properties?.description?.rich_text?.[0]?.plain_text ||
|
227
|
+
rawText.substring(0, 400),
|
228
|
+
slug: slug,
|
229
|
+
category_list: categoryPath,
|
230
|
+
children: [],
|
231
|
+
tags: tagIds,
|
232
|
+
tableOfContents,
|
233
|
+
internal: {
|
234
|
+
type: constants_1.NODE_TYPE.Post,
|
235
|
+
contentDigest: crypto_1.default
|
236
|
+
.createHash(`md5`)
|
237
|
+
.update(JSON.stringify(nodeId))
|
238
|
+
.digest(`hex`),
|
239
|
+
},
|
240
|
+
url: `${constants_1.COMMON_URI}/${constants_1.POST_URI}/${slug}`,
|
241
|
+
thumbnail: imageNode,
|
242
|
+
parent: parentCategoryId,
|
243
|
+
};
|
244
|
+
await createNode(postNode);
|
245
|
+
if (parentCategoryId) {
|
246
|
+
const parentNode = getNode(parentCategoryId);
|
247
|
+
if (parentNode) {
|
248
|
+
createParentChildLink({
|
249
|
+
parent: parentNode,
|
250
|
+
child: postNode,
|
251
|
+
});
|
252
|
+
reporter.info(`[SUCCESS] Linked parent: ${parentNode.category_name} -> child: ${postNode.title}`);
|
262
253
|
}
|
263
254
|
}
|
264
|
-
|
265
|
-
|
255
|
+
// 태그와 포스트 연결
|
256
|
+
tagIds.forEach((tagId) => {
|
257
|
+
const tagNode = getNode(tagId);
|
258
|
+
if (tagNode) {
|
259
|
+
const updatedTagNode = {
|
260
|
+
...tagNode,
|
261
|
+
churnotions: [...(tagNode.churnotions || []), nodeId],
|
262
|
+
internal: {
|
263
|
+
type: tagNode.internal.type,
|
264
|
+
contentDigest: crypto_1.default
|
265
|
+
.createHash(`md5`)
|
266
|
+
.update(JSON.stringify({
|
267
|
+
...tagNode,
|
268
|
+
churnotions: [...(tagNode.churnotions || []), nodeId],
|
269
|
+
}))
|
270
|
+
.digest(`hex`),
|
271
|
+
},
|
272
|
+
};
|
273
|
+
createNode(updatedTagNode);
|
274
|
+
reporter.info(`[SUCCESS] Added post to tag: ${tagNode.tag_name} -> ${postNode.title}`);
|
275
|
+
}
|
276
|
+
});
|
277
|
+
// 책과 포스트 연결
|
278
|
+
if (bookNode && postNode) {
|
279
|
+
createParentChildLink({
|
280
|
+
parent: bookNode,
|
281
|
+
child: postNode,
|
282
|
+
});
|
283
|
+
reporter.info(`[SUCCESS] Linked Book-Post: ${bookNode.book_name} -> child: ${postNode.title}`);
|
266
284
|
}
|
267
|
-
}
|
285
|
+
};
|
286
|
+
// 초기 데이터베이스 처리 시작
|
287
|
+
await processDatabase(databaseId);
|
288
|
+
// 캐시 정리
|
289
|
+
notionService.clearCache();
|
268
290
|
};
|
269
291
|
exports.getPages = getPages;
|
@@ -0,0 +1 @@
|
|
1
|
+
export * from "./notionService";
|
@@ -0,0 +1,17 @@
|
|
1
|
+
"use strict";
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
3
|
+
if (k2 === undefined) k2 = k;
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
7
|
+
}
|
8
|
+
Object.defineProperty(o, k2, desc);
|
9
|
+
}) : (function(o, m, k, k2) {
|
10
|
+
if (k2 === undefined) k2 = k;
|
11
|
+
o[k2] = m[k];
|
12
|
+
}));
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
15
|
+
};
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
17
|
+
__exportStar(require("./notionService"), exports);
|
@@ -0,0 +1,39 @@
|
|
1
|
+
import { Reporter } from "gatsby";
|
2
|
+
import { BaseContentBlock } from "notion-types";
|
3
|
+
export interface NotionServiceOptions {
|
4
|
+
reporter: Reporter;
|
5
|
+
parallelLimit?: number;
|
6
|
+
enableCaching?: boolean;
|
7
|
+
}
|
8
|
+
export declare class NotionService {
|
9
|
+
private reporter;
|
10
|
+
private parallelLimit;
|
11
|
+
private enableCaching;
|
12
|
+
private cache;
|
13
|
+
private limiter;
|
14
|
+
constructor(options: NotionServiceOptions);
|
15
|
+
private initLimiter;
|
16
|
+
private createSimpleLimiter;
|
17
|
+
/**
|
18
|
+
* 데이터베이스 쿼리
|
19
|
+
*/
|
20
|
+
queryDatabase(databaseId: string, body?: any): Promise<any>;
|
21
|
+
/**
|
22
|
+
* 페이지의 자식 블록 가져오기
|
23
|
+
*/
|
24
|
+
getPageBlocks(pageId: string): Promise<BaseContentBlock[]>;
|
25
|
+
/**
|
26
|
+
* 여러 페이지의 블록 병렬 처리
|
27
|
+
*/
|
28
|
+
getMultiplePagesBlocks(pageIds: string[]): Promise<{
|
29
|
+
[id: string]: BaseContentBlock[];
|
30
|
+
}>;
|
31
|
+
/**
|
32
|
+
* 캐시 초기화
|
33
|
+
*/
|
34
|
+
clearCache(): void;
|
35
|
+
/**
|
36
|
+
* 병렬 처리 제한 설정
|
37
|
+
*/
|
38
|
+
setParallelLimit(limit: number): Promise<void>;
|
39
|
+
}
|