gatsby-source-notion-churnotion 1.1.32 → 1.1.35

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 CHANGED
@@ -6,6 +6,33 @@ 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.35
10
+
11
+ - **Major Overhaul of Node Relationship Handling**:
12
+ - Completely rewrote the Book-Post relationship mechanism
13
+ - Added custom GraphQL resolver for childrenChurnotion field
14
+ - Improved recursive database traversal logic
15
+ - Optimized batch processing with smaller batches (20 pages at a time)
16
+ - Added detailed logging for better debuggability
17
+ - Simplified code structure with direct database page processing
18
+
19
+ ## What's New in v1.1.34
20
+
21
+ - **Fixed GraphQL Query Error**:
22
+ - Fixed "Cannot return null for non-nullable field Churnotion.rawText" error
23
+ - Changed rawText field to be nullable in schema definition
24
+ - Added fallback empty string for rawText in Post node creation
25
+ - Improved error handling for description field
26
+
27
+ ## What's New in v1.1.33
28
+
29
+ - **Fixed childrenChurnotion Field Issues**:
30
+ - Completely redesigned how Book-Post relationships are handled
31
+ - Added explicit node fields for childrenChurnotion
32
+ - Implemented robust node relationship creation in onPostBootstrap
33
+ - Fixed schema definition by using Fields type
34
+ - Resolved persistent "Field childrenChurnotion is not defined" error
35
+
9
36
  ## What's New in v1.1.32
10
37
 
11
38
  - **Fixed Gatsby Schema Relationship Bug**:
@@ -110,9 +137,4 @@ query MyQuery {
110
137
  }
111
138
  }
112
139
  }
113
- ```
114
-
115
- This will return results in MDX format, as shown below:
116
-
117
-
118
- ![alt text](readme3.png)
140
+ ```
@@ -37,12 +37,12 @@ const getPages = async ({ databaseId, reporter, getCache, actions, createNode, c
37
37
  const result = await notionService.queryDatabase(databaseId);
38
38
  hasMore = false; // Notion API가 페이지네이션을 완전히 지원하지 않으므로 일단 한 번만 처리
39
39
  if (result?.results?.length) {
40
- reporter.info(`[SUCCESS] total pages > ${result.results.length}`);
40
+ reporter.info(`[SUCCESS] Database ${databaseId} has ${result.results.length} pages`);
41
41
  // 페이지 ID 목록 수집
42
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);
43
+ // 페이지 블록들을 병렬로 가져오기 - 최대 20개씩 배치 처리
44
+ for (let i = 0; i < pageIds.length; i += 20) {
45
+ const batch = pageIds.slice(i, i + 20);
46
46
  reporter.info(`[BATCH] Processing pages ${i + 1} to ${i + batch.length} of ${pageIds.length}`);
47
47
  const batchBlocks = await notionService.getMultiplePagesBlocks(batch);
48
48
  // 페이지 데이터와 블록 결합
@@ -59,106 +59,100 @@ const getPages = async ({ databaseId, reporter, getCache, actions, createNode, c
59
59
  }
60
60
  }
61
61
  }
62
- // 모든 페이지 병렬 처리
63
- await Promise.all(pagesToProcess.map(async ({ page, blocks }) => {
62
+ reporter.info(`[PROCESS] Processing ${pagesToProcess.length} pages from database ${databaseId}`);
63
+ // 모든 페이지 처리
64
+ for (const { page, blocks } of pagesToProcess) {
64
65
  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`);
66
+ // 번째 블록이 child_database인지 확인
67
+ if (blocks?.[0]?.type === `child_database`) {
68
+ // 카테고리 처리
69
+ const categoryJsonData = blocks[0];
70
+ const title = categoryJsonData.child_database?.title || `Unnamed Category`;
71
+ const slug = (0, slugify_1.slugify)(title) || `no-title-${categoryJsonData.id}`;
72
+ if (!title) {
73
+ reporter.warn(`[WARNING] Category without a title detected: ${categoryJsonData.id}`);
74
+ }
75
+ const nodeId = createNodeId(`${categoryJsonData.id}-category`);
76
+ const categoryUrl = `${parentCategoryUrl}/${slug}`;
77
+ const categoryNode = {
78
+ id: nodeId,
79
+ category_name: title,
80
+ parent: parentCategoryId,
81
+ slug: slug,
82
+ children: [],
83
+ internal: {
84
+ type: constants_1.NODE_TYPE.Category,
85
+ contentDigest: crypto_1.default
86
+ .createHash(`md5`)
87
+ .update(JSON.stringify(categoryJsonData))
88
+ .digest(`hex`),
89
+ },
90
+ url: `${constants_1.COMMON_URI}/${constants_1.CATEGORY_URI}${categoryUrl}`,
91
+ books: [],
92
+ };
93
+ await createNode(categoryNode);
94
+ // Book 관계 처리
95
+ const bookRelations = page.properties?.book?.relation || null;
96
+ if (bookRelations) {
97
+ bookRelations.forEach((relation) => {
98
+ const bookId = relation.id;
99
+ const bookNodeId = createNodeId(`${bookId}-book`);
100
+ const bookNode = getNode(bookNodeId);
101
+ if (bookNode) {
102
+ createParentChildLink({
103
+ parent: categoryNode,
104
+ child: bookNode,
105
+ });
106
+ const updatedBookNode = {
107
+ ...bookNode,
108
+ book_category: categoryNode.id,
109
+ internal: {
110
+ type: bookNode.internal.type,
111
+ contentDigest: crypto_1.default
112
+ .createHash(`md5`)
113
+ .update(JSON.stringify(bookNode))
114
+ .digest(`hex`),
115
+ },
116
+ };
117
+ createNode(updatedBookNode);
118
+ reporter.info(`[SUCCESS] Linked Category-Book: ${categoryNode.category_name} -> child: ${bookNode.book_name}`);
119
+ }
120
+ });
121
+ }
122
+ // 부모-자식 카테고리 관계 설정
123
+ if (parentCategoryId && categoryNode) {
124
+ const parentNode = getNode(parentCategoryId);
125
+ if (parentNode) {
126
+ createParentChildLink({
127
+ parent: parentNode,
128
+ child: categoryNode,
129
+ });
130
+ reporter.info(`[SUCCESS] Linked parent: ${parentNode.category_name} -> child: ${categoryNode.category_name}`);
131
+ }
132
+ else {
133
+ reporter.warn(`[WARNING] Parent node not found for ID: ${parentCategoryId}`);
134
+ }
135
+ }
136
+ const newCategoryPath = [...categoryPath, categoryNode];
137
+ // 해당 데이터베이스의 하위 페이지들을 처리
138
+ // 여기서 재귀적으로 자식 데이터베이스 처리
139
+ await processDatabase(categoryJsonData.id, nodeId, newCategoryPath, categoryUrl);
140
+ }
141
+ else {
142
+ // 일반 포스트 처리
143
+ await (0, timeLimit_1.timeLimit)(processPost(page, blocks, parentCategoryId, categoryPath, tagMap, parentCategoryUrl), 30000, // 30초 제한
144
+ `Processing post ${page.id} timed out after 30 seconds`);
145
+ }
68
146
  }
69
147
  catch (error) {
70
148
  reporter.warn(`[WARNING] Error processing page ${page.id}: ${error}`);
71
149
  }
72
- }));
150
+ }
73
151
  }
74
152
  catch (error) {
75
153
  reporter.error(`[ERROR] Processing database ${databaseId} failed: ${error}`);
76
154
  }
77
155
  };
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);
125
- if (bookNode) {
126
- createParentChildLink({
127
- parent: categoryNode,
128
- child: bookNode,
129
- });
130
- const updatedBookNode = {
131
- ...bookNode,
132
- book_category: categoryNode.id,
133
- internal: {
134
- type: bookNode.internal.type,
135
- contentDigest: crypto_1.default
136
- .createHash(`md5`)
137
- .update(JSON.stringify(bookNode))
138
- .digest(`hex`),
139
- },
140
- };
141
- createNode(updatedBookNode);
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
156
  /**
163
157
  * 포스트 처리 메서드
164
158
  */
@@ -224,7 +218,7 @@ const getPages = async ({ databaseId, reporter, getCache, actions, createNode, c
224
218
  update_date: (0, formatDate_1.useFormatDate)(page.last_edited_time),
225
219
  version: page.properties?.version?.number || null,
226
220
  description: page.properties?.description?.rich_text?.[0]?.plain_text ||
227
- rawText.substring(0, 400),
221
+ (rawText ? rawText.substring(0, 400) : ""),
228
222
  slug: slug,
229
223
  category_list: categoryPath,
230
224
  children: [],
@@ -240,8 +234,10 @@ const getPages = async ({ databaseId, reporter, getCache, actions, createNode, c
240
234
  url: `${constants_1.COMMON_URI}/${constants_1.POST_URI}/${slug}`,
241
235
  thumbnail: imageNode,
242
236
  parent: parentCategoryId,
237
+ rawText: rawText || "",
243
238
  };
244
239
  await createNode(postNode);
240
+ reporter.info(`[SUCCESS] Created post node: ${title} (${nodeId})`);
245
241
  if (parentCategoryId) {
246
242
  const parentNode = getNode(parentCategoryId);
247
243
  if (parentNode) {
@@ -2,9 +2,64 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createSchemaCustomization = void 0;
4
4
  const constants_1 = require("./constants");
5
- const createSchemaCustomization = ({ actions }) => {
5
+ const createSchemaCustomization = ({ actions, schema }) => {
6
6
  const { createTypes } = actions;
7
- createTypes(`
7
+ createTypes([
8
+ schema.buildObjectType({
9
+ name: constants_1.NODE_TYPE.Book,
10
+ interfaces: ["Node"],
11
+ fields: {
12
+ id: "ID!",
13
+ book_name: "String!",
14
+ create_date: {
15
+ type: "Date!",
16
+ extensions: {
17
+ dateformat: {},
18
+ },
19
+ },
20
+ update_date: {
21
+ type: "Date!",
22
+ extensions: {
23
+ dateformat: {},
24
+ },
25
+ },
26
+ children: {
27
+ type: `[${constants_1.NODE_TYPE.Post}]`,
28
+ extensions: {
29
+ link: { by: "book", from: "id" },
30
+ },
31
+ },
32
+ childrenChurnotion: {
33
+ type: `[${constants_1.NODE_TYPE.Post}]`,
34
+ resolve: (source, args, context) => {
35
+ return context.nodeModel.runQuery({
36
+ query: {
37
+ filter: {
38
+ book: { eq: source.id },
39
+ },
40
+ },
41
+ type: constants_1.NODE_TYPE.Post,
42
+ firstOnly: false,
43
+ });
44
+ },
45
+ },
46
+ url: "String!",
47
+ book_category: {
48
+ type: constants_1.NODE_TYPE.Category,
49
+ extensions: {
50
+ link: { by: "id", from: "book_category" },
51
+ },
52
+ },
53
+ book_image: {
54
+ type: "File",
55
+ extensions: {
56
+ link: { by: "id", from: "book_image" },
57
+ },
58
+ },
59
+ description: "String!",
60
+ },
61
+ }),
62
+ `
8
63
  type ${constants_1.NODE_TYPE.Post} implements Node {
9
64
  id: ID!
10
65
  category: ${constants_1.NODE_TYPE.Category}! @link(by: "id", from: "category")
@@ -22,7 +77,7 @@ const createSchemaCustomization = ({ actions }) => {
22
77
  category_list: [${constants_1.NODE_TYPE.Category}]
23
78
  url: String!
24
79
  thumbnail: File @link(by: "id", from: "thumbnail")
25
- rawText: String!
80
+ rawText: String
26
81
  }
27
82
 
28
83
  type ${constants_1.NODE_TYPE.Tag} implements Node {
@@ -46,17 +101,8 @@ const createSchemaCustomization = ({ actions }) => {
46
101
  childrenNBook: [${constants_1.NODE_TYPE.Book}] @link(by: "book_category", from: "id")
47
102
  }
48
103
 
49
- type ${constants_1.NODE_TYPE.Book} implements Node {
50
- id: ID!
51
- book_name: String!
52
- create_date: Date! @dateformat
53
- update_date: Date! @dateformat
54
- children: [${constants_1.NODE_TYPE.Post}] @link(by: "book", from: "id")
55
- childrenChurnotion: [${constants_1.NODE_TYPE.Post}] @link(by: "book", from: "id")
56
- url: String!
57
- book_category: ${constants_1.NODE_TYPE.Category} @link(by: "id", from: "book_category")
58
- book_image: File @link(by: "id", from: "book_image")
59
- description: String!
104
+ type Fields {
105
+ childrenChurnotion: [${constants_1.NODE_TYPE.Post}] @link(by: "id")
60
106
  }
61
107
 
62
108
  type ${constants_1.NODE_TYPE.Metadata} implements Node {
@@ -70,6 +116,7 @@ const createSchemaCustomization = ({ actions }) => {
70
116
  type ${constants_1.NODE_TYPE.RelatedPost} implements Node {
71
117
  posts: [${constants_1.NODE_TYPE.Post}] @link(by: "id")
72
118
  }
73
- `);
119
+ `,
120
+ ]);
74
121
  };
75
122
  exports.createSchemaCustomization = createSchemaCustomization;
@@ -1,2 +1,2 @@
1
1
  import { GatsbyNode } from "gatsby";
2
- export declare const onPostBootstrap: GatsbyNode[`onPostBootstrap`];
2
+ export declare const onPostBootstrap: GatsbyNode["onPostBootstrap"];
@@ -65,59 +65,34 @@ const getSpaceSeparatedDoc = (doc) => {
65
65
  const tokens = getTokens(doc);
66
66
  return tokens.join(" ");
67
67
  };
68
- const onPostBootstrap = async ({ actions, getNode, getNodesByType, createNodeId, reporter, cache }, options) => {
69
- const nodes = getNodesByType(constants_1.NODE_TYPE.Post);
70
- const docs = nodes
71
- .map((node) => ({ id: node.id, text: node.rawText }))
72
- .filter((doc) => doc.text?.trim() !== "");
73
- const tfidf = new natural_1.TfIdf();
74
- // tfidf
75
- docs.map(async (doc) => {
76
- if (doc.text) {
77
- const key = `${md5(doc.text)}-doc`;
78
- const cached_ssd = await cache.get(key);
79
- if (cached_ssd !== undefined) {
80
- tfidf.addDocument(cached_ssd);
81
- }
82
- else {
83
- const ssd = await getSpaceSeparatedDoc(await getTextFromRawText(doc.text));
84
- tfidf.addDocument(ssd);
85
- await cache.set(key, ssd);
68
+ const onPostBootstrap = async ({ getNodesByType, actions, reporter, }) => {
69
+ const { createNodeField } = actions;
70
+ reporter.info(`Creating explicit relationships between nodes...`);
71
+ // Get all Book and Post nodes
72
+ const books = getNodesByType(constants_1.NODE_TYPE.Book);
73
+ const posts = getNodesByType(constants_1.NODE_TYPE.Post);
74
+ // Create a map of book ID to related posts
75
+ const bookPostMap = new Map();
76
+ // Populate the map
77
+ posts.forEach((post) => {
78
+ if (post.book) {
79
+ if (!bookPostMap.has(post.book)) {
80
+ bookPostMap.set(post.book, []);
86
81
  }
82
+ bookPostMap.get(post.book).push(post.id);
87
83
  }
88
84
  });
89
- const doc_terms = docs.map((_, i) => tfidf.listTerms(i)
90
- .map((x) => ({ ...x, tfidf: x.tf * x.idf }))
91
- .sort((x, y) => y.tfidf - x.tfidf));
92
- const all_keywords = new Set();
93
- const tfidf_map_for_each_doc = [];
94
- doc_terms.forEach((x, i) => {
95
- tfidf_map_for_each_doc[i] = new Map();
96
- x.slice(0, 30).forEach((x) => {
97
- all_keywords.add(x.term);
98
- tfidf_map_for_each_doc[i].set(x.term, x.tfidf);
99
- });
100
- });
101
- const bow_vectors = new Map();
102
- docs.forEach((x, i) => {
103
- if (bow_vectors === null)
104
- return;
105
- bow_vectors.set(x.id, Array.from(all_keywords)
106
- .map((x) => tfidf_map_for_each_doc[i].get(x))
107
- .map((x) => (x === undefined ? 0 : x)));
108
- });
109
- nodes.forEach((node) => {
110
- const relatedNodeIds = getRelatedPosts(node.id, bow_vectors);
111
- const digest = `${node.id} - ${constants_1.NODE_TYPE.RelatedPost}`;
112
- actions.createNode({
113
- id: createNodeId(digest),
114
- parent: node.id,
115
- internal: {
116
- type: constants_1.NODE_TYPE.RelatedPost,
117
- contentDigest: digest,
118
- },
119
- posts: relatedNodeIds,
85
+ // Create explicit fields for each book
86
+ books.forEach((book) => {
87
+ const relatedPostIds = bookPostMap.get(book.id) || [];
88
+ // Add childrenChurnotion field explicitly
89
+ createNodeField({
90
+ node: book,
91
+ name: "childrenChurnotion",
92
+ value: relatedPostIds,
120
93
  });
94
+ reporter.info(`Added ${relatedPostIds.length} posts to book: ${book.book_name}`);
121
95
  });
96
+ reporter.info(`Relationship creation completed`);
122
97
  };
123
98
  exports.onPostBootstrap = onPostBootstrap;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "gatsby-source-notion-churnotion",
3
3
  "description": "Gatsby plugin that can connect with One Notion Database RECURSIVELY using official API",
4
- "version": "1.1.32",
4
+ "version": "1.1.35",
5
5
  "skipLibCheck": true,
6
6
  "license": "0BSD",
7
7
  "main": "./dist/gatsby-node.js",