@rmdes/indiekit-endpoint-microsub 1.0.63 → 1.0.64
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/lib/controllers/reader/channel.js +0 -1
- package/lib/polling/scheduler.js +4 -4
- package/lib/storage/items-read-state.js +30 -59
- package/lib/storage/items-retention.js +1 -3
- package/lib/utils/blogroll-notify.js +3 -3
- package/lib/utils/constants.js +7 -0
- package/lib/utils/jf2.js +0 -109
- package/lib/utils/sanitize.js +1 -2
- package/lib/webmention/processor.js +2 -2
- package/lib/websub/handler.js +2 -2
- package/locales/en.json +3 -27
- package/package.json +1 -1
- package/lib/search/indexer.js +0 -90
- package/lib/storage/items-search.js +0 -34
|
@@ -10,7 +10,6 @@ import {
|
|
|
10
10
|
updateChannelSettings,
|
|
11
11
|
deleteChannel,
|
|
12
12
|
} from "../../storage/channels.js";
|
|
13
|
-
import { getFeedsForChannel } from "../../storage/feeds.js";
|
|
14
13
|
import { getTimelineItems } from "../../storage/items.js";
|
|
15
14
|
import { countReadItems } from "../../storage/items-read-state.js";
|
|
16
15
|
import { getUserId } from "../../utils/auth.js";
|
package/lib/polling/scheduler.js
CHANGED
|
@@ -35,7 +35,7 @@ export function startScheduler(indiekit) {
|
|
|
35
35
|
// Run immediately on start
|
|
36
36
|
runSchedulerCycle();
|
|
37
37
|
|
|
38
|
-
console.
|
|
38
|
+
console.info("[Microsub] Feed polling scheduler started");
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
/**
|
|
@@ -47,7 +47,7 @@ export function stopScheduler() {
|
|
|
47
47
|
schedulerInterval = undefined;
|
|
48
48
|
}
|
|
49
49
|
indiekitInstance = undefined;
|
|
50
|
-
console.
|
|
50
|
+
console.info("[Microsub] Feed polling scheduler stopped");
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
/**
|
|
@@ -74,13 +74,13 @@ async function runSchedulerCycle() {
|
|
|
74
74
|
return;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
console.
|
|
77
|
+
console.info(`[Microsub] Processing ${feeds.length} feeds due for refresh`);
|
|
78
78
|
|
|
79
79
|
const result = await processFeedBatch(application, feeds, {
|
|
80
80
|
concurrency: BATCH_CONCURRENCY,
|
|
81
81
|
});
|
|
82
82
|
|
|
83
|
-
console.
|
|
83
|
+
console.info(
|
|
84
84
|
`[Microsub] Processed ${result.total} feeds: ${result.successful} successful, ` +
|
|
85
85
|
`${result.failed} failed, ${result.itemsAdded} new items`,
|
|
86
86
|
);
|
|
@@ -5,22 +5,17 @@
|
|
|
5
5
|
|
|
6
6
|
import { ObjectId } from "mongodb";
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
MAX_FULL_READ_ITEMS,
|
|
10
|
+
UNREAD_RETENTION_DAYS,
|
|
11
|
+
} from "../utils/constants.js";
|
|
9
12
|
import { getCollection } from "./items.js";
|
|
10
13
|
|
|
11
|
-
// Maximum number of full read items to keep per channel before stripping content.
|
|
12
|
-
// Items beyond this limit are converted to lightweight dedup skeletons (channelId,
|
|
13
|
-
// uid, readBy) so the poller doesn't re-ingest them as new unread entries.
|
|
14
|
-
const MAX_FULL_READ_ITEMS = 200;
|
|
15
|
-
|
|
16
14
|
/**
|
|
17
15
|
* Cleanup old read items by stripping content but preserving dedup skeletons.
|
|
18
|
-
*
|
|
16
|
+
* Prevents the vicious cycle where deleted read items get re-ingested as
|
|
19
17
|
* unread by the poller because the dedup record (channelId + uid) was destroyed.
|
|
20
18
|
*
|
|
21
|
-
* AP items (feedId: null) are hard-deleted instead of stripped, since no poller
|
|
22
|
-
* re-ingests them — they arrive via inbox push and don't need dedup skeletons.
|
|
23
|
-
*
|
|
24
19
|
* @param {object} collection - MongoDB collection
|
|
25
20
|
* @param {ObjectId} channelObjectId - Channel ObjectId
|
|
26
21
|
* @param {string} userId - User ID
|
|
@@ -32,7 +27,6 @@ async function cleanupOldReadItems(collection, channelObjectId, userId) {
|
|
|
32
27
|
});
|
|
33
28
|
|
|
34
29
|
if (readCount > MAX_FULL_READ_ITEMS) {
|
|
35
|
-
// Find old read items beyond the retention limit
|
|
36
30
|
const itemsToCleanup = await collection
|
|
37
31
|
.find({
|
|
38
32
|
channelId: channelObjectId,
|
|
@@ -41,59 +35,36 @@ async function cleanupOldReadItems(collection, channelObjectId, userId) {
|
|
|
41
35
|
})
|
|
42
36
|
.sort({ published: -1, _id: -1 })
|
|
43
37
|
.skip(MAX_FULL_READ_ITEMS)
|
|
44
|
-
.project({ _id: 1
|
|
38
|
+
.project({ _id: 1 })
|
|
45
39
|
.toArray();
|
|
46
40
|
|
|
47
41
|
if (itemsToCleanup.length === 0) return;
|
|
48
42
|
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Strip RSS items to dedup skeletons — poller would re-ingest if deleted
|
|
71
|
-
if (rssItemIds.length > 0) {
|
|
72
|
-
const stripped = await collection.updateMany(
|
|
73
|
-
{ _id: { $in: rssItemIds } },
|
|
74
|
-
{
|
|
75
|
-
$set: { _stripped: true },
|
|
76
|
-
$unset: {
|
|
77
|
-
name: "",
|
|
78
|
-
content: "",
|
|
79
|
-
summary: "",
|
|
80
|
-
author: "",
|
|
81
|
-
category: "",
|
|
82
|
-
photo: "",
|
|
83
|
-
video: "",
|
|
84
|
-
audio: "",
|
|
85
|
-
likeOf: "",
|
|
86
|
-
repostOf: "",
|
|
87
|
-
bookmarkOf: "",
|
|
88
|
-
inReplyTo: "",
|
|
89
|
-
source: "",
|
|
90
|
-
},
|
|
43
|
+
const ids = itemsToCleanup.map((item) => item._id);
|
|
44
|
+
const stripped = await collection.updateMany(
|
|
45
|
+
{ _id: { $in: ids } },
|
|
46
|
+
{
|
|
47
|
+
$set: { _stripped: true },
|
|
48
|
+
$unset: {
|
|
49
|
+
name: "",
|
|
50
|
+
content: "",
|
|
51
|
+
summary: "",
|
|
52
|
+
author: "",
|
|
53
|
+
category: "",
|
|
54
|
+
photo: "",
|
|
55
|
+
video: "",
|
|
56
|
+
audio: "",
|
|
57
|
+
likeOf: "",
|
|
58
|
+
repostOf: "",
|
|
59
|
+
bookmarkOf: "",
|
|
60
|
+
inReplyTo: "",
|
|
61
|
+
source: "",
|
|
91
62
|
},
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
)
|
|
96
|
-
|
|
63
|
+
},
|
|
64
|
+
);
|
|
65
|
+
console.info(
|
|
66
|
+
`[Microsub] Stripped ${stripped.modifiedCount} old read items (keeping ${MAX_FULL_READ_ITEMS} full)`,
|
|
67
|
+
);
|
|
97
68
|
}
|
|
98
69
|
}
|
|
99
70
|
|
|
@@ -3,11 +3,9 @@
|
|
|
3
3
|
* @module storage/items-retention
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { MAX_FULL_READ_ITEMS } from "../utils/constants.js";
|
|
6
7
|
import { getCollection } from "./items.js";
|
|
7
8
|
|
|
8
|
-
// Maximum number of full read items to keep per channel before stripping content.
|
|
9
|
-
const MAX_FULL_READ_ITEMS = 200;
|
|
10
|
-
|
|
11
9
|
// Global retention defaults. Each can be overridden per channel via
|
|
12
10
|
// channel.settings.{maxItems,maxItemsPerFeed,maxUnreadAgeDays}. The "notifications"
|
|
13
11
|
// channel is exempt from these caps entirely — webmentions are high-signal and
|
|
@@ -36,7 +36,7 @@ export async function notifyBlogroll(application, action, data) {
|
|
|
36
36
|
status: "deleted",
|
|
37
37
|
});
|
|
38
38
|
if (deleted) {
|
|
39
|
-
console.
|
|
39
|
+
console.info(
|
|
40
40
|
`[Microsub→Blogroll] Skipping follow for ${data.url} — previously deleted by user`,
|
|
41
41
|
);
|
|
42
42
|
return;
|
|
@@ -76,7 +76,7 @@ export async function notifyBlogroll(application, action, data) {
|
|
|
76
76
|
{ upsert: true },
|
|
77
77
|
);
|
|
78
78
|
|
|
79
|
-
console.
|
|
79
|
+
console.info(`[Microsub→Blogroll] Added/updated feed ${data.url}`);
|
|
80
80
|
} else if (action === "unfollow") {
|
|
81
81
|
// Soft-delete the blog entry if it came from microsub
|
|
82
82
|
const result = await collection.updateOne(
|
|
@@ -96,7 +96,7 @@ export async function notifyBlogroll(application, action, data) {
|
|
|
96
96
|
);
|
|
97
97
|
|
|
98
98
|
if (result.modifiedCount > 0) {
|
|
99
|
-
console.
|
|
99
|
+
console.info(`[Microsub→Blogroll] Soft-deleted feed ${data.url}`);
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
102
|
}
|
package/lib/utils/constants.js
CHANGED
|
@@ -5,3 +5,10 @@
|
|
|
5
5
|
|
|
6
6
|
/** Retention period for unread count queries (only count recent items) */
|
|
7
7
|
export const UNREAD_RETENTION_DAYS = 30;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Maximum number of full read items to keep per channel/user before stripping
|
|
11
|
+
* content. Items beyond this limit are converted to lightweight dedup skeletons
|
|
12
|
+
* (channelId, uid, readBy) so the poller doesn't re-ingest them as new unread.
|
|
13
|
+
*/
|
|
14
|
+
export const MAX_FULL_READ_ITEMS = 200;
|
package/lib/utils/jf2.js
CHANGED
|
@@ -29,115 +29,6 @@ export function generateChannelUid() {
|
|
|
29
29
|
return result;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
/**
|
|
33
|
-
* Create a jf2 Item from normalized feed data
|
|
34
|
-
* @param {object} data - Normalized item data
|
|
35
|
-
* @param {object} source - Feed source metadata
|
|
36
|
-
* @returns {object} jf2 Item object
|
|
37
|
-
*/
|
|
38
|
-
export function createJf2Item(data, source) {
|
|
39
|
-
return {
|
|
40
|
-
type: "entry",
|
|
41
|
-
uid: data.uid,
|
|
42
|
-
url: data.url,
|
|
43
|
-
name: data.name || undefined,
|
|
44
|
-
content: data.content || undefined,
|
|
45
|
-
summary: data.summary || undefined,
|
|
46
|
-
published: data.published,
|
|
47
|
-
updated: data.updated || undefined,
|
|
48
|
-
author: data.author || undefined,
|
|
49
|
-
category: data.category || [],
|
|
50
|
-
photo: data.photo || [],
|
|
51
|
-
video: data.video || [],
|
|
52
|
-
audio: data.audio || [],
|
|
53
|
-
// Interaction types
|
|
54
|
-
"like-of": data.likeOf || [],
|
|
55
|
-
"repost-of": data.repostOf || [],
|
|
56
|
-
"bookmark-of": data.bookmarkOf || [],
|
|
57
|
-
"in-reply-to": data.inReplyTo || [],
|
|
58
|
-
// Internal properties (prefixed with _)
|
|
59
|
-
_id: data._id,
|
|
60
|
-
_is_read: data._is_read || false,
|
|
61
|
-
_source: source,
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Create a jf2 Card (author/person)
|
|
67
|
-
* @param {object} data - Author data
|
|
68
|
-
* @returns {object} jf2 Card object
|
|
69
|
-
*/
|
|
70
|
-
export function createJf2Card(data) {
|
|
71
|
-
if (!data) return;
|
|
72
|
-
|
|
73
|
-
return {
|
|
74
|
-
type: "card",
|
|
75
|
-
name: data.name || undefined,
|
|
76
|
-
url: data.url || undefined,
|
|
77
|
-
photo: data.photo || undefined,
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Create a jf2 Content object
|
|
83
|
-
* @param {string} text - Plain text content
|
|
84
|
-
* @param {string} html - HTML content
|
|
85
|
-
* @returns {object|undefined} jf2 Content object
|
|
86
|
-
*/
|
|
87
|
-
export function createJf2Content(text, html) {
|
|
88
|
-
if (!text && !html) return;
|
|
89
|
-
|
|
90
|
-
return {
|
|
91
|
-
text: text || stripHtml(html),
|
|
92
|
-
html: html || undefined,
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Strip HTML tags from string
|
|
98
|
-
* @param {string} html - HTML string
|
|
99
|
-
* @returns {string} Plain text
|
|
100
|
-
*/
|
|
101
|
-
export function stripHtml(html) {
|
|
102
|
-
if (!html) return "";
|
|
103
|
-
return html.replaceAll(/<[^>]*>/g, "").trim();
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Create a jf2 Feed response
|
|
108
|
-
* @param {object} options - Feed options
|
|
109
|
-
* @param {Array} options.items - Array of jf2 items
|
|
110
|
-
* @param {object} options.paging - Pagination cursors
|
|
111
|
-
* @returns {object} jf2 Feed object
|
|
112
|
-
*/
|
|
113
|
-
export function createJf2Feed({ items, paging }) {
|
|
114
|
-
const feed = {
|
|
115
|
-
items: items || [],
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
if (paging) {
|
|
119
|
-
feed.paging = {};
|
|
120
|
-
if (paging.before) feed.paging.before = paging.before;
|
|
121
|
-
if (paging.after) feed.paging.after = paging.after;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
return feed;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Create a Channel response object
|
|
129
|
-
* @param {object} channel - Channel data
|
|
130
|
-
* @param {number} unreadCount - Number of unread items
|
|
131
|
-
* @returns {object} Channel object for API response
|
|
132
|
-
*/
|
|
133
|
-
export function createChannelResponse(channel, unreadCount = 0) {
|
|
134
|
-
return {
|
|
135
|
-
uid: channel.uid,
|
|
136
|
-
name: channel.name,
|
|
137
|
-
unread: unreadCount > 0 ? unreadCount : false,
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
|
|
141
32
|
/**
|
|
142
33
|
* Create a Feed response object
|
|
143
34
|
* @param {object} feed - Feed data
|
package/lib/utils/sanitize.js
CHANGED
|
@@ -30,7 +30,7 @@ export async function processWebmention(application, source, target, userId) {
|
|
|
30
30
|
const verification = await verifyWebmention(source, target);
|
|
31
31
|
|
|
32
32
|
if (!verification.verified) {
|
|
33
|
-
console.
|
|
33
|
+
console.info(
|
|
34
34
|
`[Microsub] Webmention verification failed: ${verification.error}`,
|
|
35
35
|
);
|
|
36
36
|
return {
|
|
@@ -87,7 +87,7 @@ export async function processWebmention(application, source, target, userId) {
|
|
|
87
87
|
});
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
console.
|
|
90
|
+
console.info(
|
|
91
91
|
`[Microsub] Webmention processed: ${verification.type} from ${source}`,
|
|
92
92
|
);
|
|
93
93
|
|
package/lib/websub/handler.js
CHANGED
|
@@ -64,7 +64,7 @@ export async function verify(request, response) {
|
|
|
64
64
|
});
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
console.
|
|
67
|
+
console.info(`[Microsub] WebSub subscription verified for ${feed.url}`);
|
|
68
68
|
|
|
69
69
|
// Return challenge to verify subscription
|
|
70
70
|
response.type("text/plain").send(challenge);
|
|
@@ -143,7 +143,7 @@ async function processWebsubContent(application, feed, contentType, body) {
|
|
|
143
143
|
// Parse the pushed content
|
|
144
144
|
const parsed = await parseFeed(content, feed.url, { contentType });
|
|
145
145
|
|
|
146
|
-
console.
|
|
146
|
+
console.info(
|
|
147
147
|
`[Microsub] Processing ${parsed.items.length} items from WebSub push for ${feed.url}`,
|
|
148
148
|
);
|
|
149
149
|
|
package/locales/en.json
CHANGED
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
"showRead": "Show read ({{count}})",
|
|
11
11
|
"hideRead": "Hide read items",
|
|
12
12
|
"allRead": "All caught up!",
|
|
13
|
-
"newer": "Newer",
|
|
14
13
|
"older": "Older"
|
|
15
14
|
},
|
|
16
15
|
"channels": {
|
|
@@ -18,34 +17,21 @@
|
|
|
18
17
|
"name": "Channel name",
|
|
19
18
|
"new": "New channel",
|
|
20
19
|
"create": "Create channel",
|
|
21
|
-
"delete": "Delete channel",
|
|
22
20
|
"settings": "Channel settings",
|
|
23
|
-
"empty": "No channels yet. Create one to get started."
|
|
24
|
-
"notifications": "Notifications"
|
|
21
|
+
"empty": "No channels yet. Create one to get started."
|
|
25
22
|
},
|
|
26
23
|
"timeline": {
|
|
27
|
-
"title": "Timeline",
|
|
28
24
|
"empty": "No items in this channel",
|
|
29
|
-
"markRead": "Mark as read"
|
|
30
|
-
"markUnread": "Mark as unread",
|
|
31
|
-
"remove": "Remove"
|
|
25
|
+
"markRead": "Mark as read"
|
|
32
26
|
},
|
|
33
27
|
"feeds": {
|
|
34
28
|
"title": "Feeds",
|
|
35
29
|
"follow": "Follow",
|
|
36
30
|
"subscribe": "Subscribe to a feed",
|
|
37
|
-
"unfollow": "Unfollow",
|
|
38
31
|
"empty": "No feeds followed in this channel",
|
|
39
32
|
"url": "Feed URL",
|
|
40
33
|
"urlPlaceholder": "https://example.com/feed.xml",
|
|
41
|
-
"edit": "Edit feed"
|
|
42
|
-
"rediscover": "Rediscover feed",
|
|
43
|
-
"refresh": "Refresh now",
|
|
44
|
-
"status": {
|
|
45
|
-
"active": "Active",
|
|
46
|
-
"error": "Error",
|
|
47
|
-
"stale": "Stale"
|
|
48
|
-
}
|
|
34
|
+
"edit": "Edit feed"
|
|
49
35
|
},
|
|
50
36
|
"item": {
|
|
51
37
|
"reply": "Reply",
|
|
@@ -101,20 +87,10 @@
|
|
|
101
87
|
"submit": "Search",
|
|
102
88
|
"noResults": "No results found"
|
|
103
89
|
},
|
|
104
|
-
"preview": {
|
|
105
|
-
"title": "Preview",
|
|
106
|
-
"subscribe": "Subscribe to this feed"
|
|
107
|
-
},
|
|
108
90
|
"views": {
|
|
109
91
|
"channels": "Channels",
|
|
110
92
|
"deck": "Deck",
|
|
111
93
|
"timeline": "Timeline"
|
|
112
|
-
},
|
|
113
|
-
"error": {
|
|
114
|
-
"channelNotFound": "Channel not found",
|
|
115
|
-
"feedNotFound": "Feed not found",
|
|
116
|
-
"invalidUrl": "Invalid URL",
|
|
117
|
-
"invalidAction": "Invalid action"
|
|
118
94
|
}
|
|
119
95
|
}
|
|
120
96
|
}
|
package/package.json
CHANGED
package/lib/search/indexer.js
DELETED
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Search indexer for MongoDB text search
|
|
3
|
-
* @module search/indexer
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Create text indexes for microsub items
|
|
8
|
-
* @param {object} application - Indiekit application
|
|
9
|
-
* @returns {Promise<void>}
|
|
10
|
-
*/
|
|
11
|
-
export async function createSearchIndexes(application) {
|
|
12
|
-
const itemsCollection = application.collections.get("microsub_items");
|
|
13
|
-
|
|
14
|
-
// Create compound text index for full-text search
|
|
15
|
-
await itemsCollection.createIndex(
|
|
16
|
-
{
|
|
17
|
-
name: "text",
|
|
18
|
-
"content.text": "text",
|
|
19
|
-
"content.html": "text",
|
|
20
|
-
summary: "text",
|
|
21
|
-
"author.name": "text",
|
|
22
|
-
},
|
|
23
|
-
{
|
|
24
|
-
name: "text_search",
|
|
25
|
-
weights: {
|
|
26
|
-
name: 10,
|
|
27
|
-
"content.text": 5,
|
|
28
|
-
summary: 3,
|
|
29
|
-
"author.name": 2,
|
|
30
|
-
},
|
|
31
|
-
default_language: "english",
|
|
32
|
-
background: true,
|
|
33
|
-
},
|
|
34
|
-
);
|
|
35
|
-
|
|
36
|
-
// Create index for channel + published for efficient timeline queries
|
|
37
|
-
await itemsCollection.createIndex(
|
|
38
|
-
{ channelId: 1, published: -1 },
|
|
39
|
-
{ name: "channel_timeline" },
|
|
40
|
-
);
|
|
41
|
-
|
|
42
|
-
// Create index for deduplication
|
|
43
|
-
await itemsCollection.createIndex(
|
|
44
|
-
{ channelId: 1, uid: 1 },
|
|
45
|
-
{ name: "channel_uid", unique: true },
|
|
46
|
-
);
|
|
47
|
-
|
|
48
|
-
// Create index for feed-based queries
|
|
49
|
-
await itemsCollection.createIndex({ feedId: 1 }, { name: "feed_items" });
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Rebuild search indexes (drops and recreates)
|
|
54
|
-
* @param {object} application - Indiekit application
|
|
55
|
-
* @returns {Promise<void>}
|
|
56
|
-
*/
|
|
57
|
-
export async function rebuildSearchIndexes(application) {
|
|
58
|
-
const itemsCollection = application.collections.get("microsub_items");
|
|
59
|
-
|
|
60
|
-
// Drop existing text index
|
|
61
|
-
try {
|
|
62
|
-
await itemsCollection.dropIndex("text_search");
|
|
63
|
-
} catch {
|
|
64
|
-
// Index may not exist
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Recreate indexes
|
|
68
|
-
await createSearchIndexes(application);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Get search index stats
|
|
73
|
-
* @param {object} application - Indiekit application
|
|
74
|
-
* @returns {Promise<object>} Index statistics
|
|
75
|
-
*/
|
|
76
|
-
export async function getSearchIndexStats(application) {
|
|
77
|
-
const itemsCollection = application.collections.get("microsub_items");
|
|
78
|
-
|
|
79
|
-
const indexes = await itemsCollection.indexes();
|
|
80
|
-
const stats = await itemsCollection.stats();
|
|
81
|
-
|
|
82
|
-
return {
|
|
83
|
-
indexes: indexes.map((index) => ({
|
|
84
|
-
name: index.name,
|
|
85
|
-
key: index.key,
|
|
86
|
-
})),
|
|
87
|
-
totalDocuments: stats.count,
|
|
88
|
-
size: stats.size,
|
|
89
|
-
};
|
|
90
|
-
}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Timeline item search
|
|
3
|
-
* @module storage/items-search
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { ObjectId } from "mongodb";
|
|
7
|
-
|
|
8
|
-
import { getCollection, transformToJf2 } from "./items.js";
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Search items by text
|
|
12
|
-
* @param {object} application - Indiekit application
|
|
13
|
-
* @param {ObjectId|string} channelId - Channel ObjectId
|
|
14
|
-
* @param {string} query - Search query
|
|
15
|
-
* @param {number} [limit] - Max results
|
|
16
|
-
* @returns {Promise<Array>} Array of matching items
|
|
17
|
-
*/
|
|
18
|
-
export async function searchItems(application, channelId, query, limit = 20) {
|
|
19
|
-
const collection = getCollection(application);
|
|
20
|
-
const objectId =
|
|
21
|
-
typeof channelId === "string" ? new ObjectId(channelId) : channelId;
|
|
22
|
-
|
|
23
|
-
// Use MongoDB text index for efficient full-text search
|
|
24
|
-
const items = await collection
|
|
25
|
-
.find({
|
|
26
|
-
channelId: objectId,
|
|
27
|
-
$text: { $search: query },
|
|
28
|
-
})
|
|
29
|
-
.sort({ score: { $meta: "textScore" } })
|
|
30
|
-
.limit(limit)
|
|
31
|
-
.toArray();
|
|
32
|
-
|
|
33
|
-
return items.map((item) => transformToJf2(item));
|
|
34
|
-
}
|