@commonpub/layer 0.3.30 → 0.3.32
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@commonpub/layer",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.32",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./nuxt.config.ts",
|
|
6
6
|
"files": [
|
|
@@ -45,13 +45,13 @@
|
|
|
45
45
|
"vue-router": "^4.3.0",
|
|
46
46
|
"zod": "^4.3.6",
|
|
47
47
|
"@commonpub/auth": "0.5.0",
|
|
48
|
-
"@commonpub/docs": "0.5.2",
|
|
49
48
|
"@commonpub/config": "0.7.1",
|
|
50
|
-
"@commonpub/
|
|
49
|
+
"@commonpub/docs": "0.5.2",
|
|
51
50
|
"@commonpub/editor": "0.5.0",
|
|
51
|
+
"@commonpub/learning": "0.5.0",
|
|
52
52
|
"@commonpub/protocol": "0.9.5",
|
|
53
53
|
"@commonpub/schema": "0.8.13",
|
|
54
|
-
"@commonpub/server": "2.
|
|
54
|
+
"@commonpub/server": "2.19.0",
|
|
55
55
|
"@commonpub/ui": "0.7.1"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
package/pages/search.vue
CHANGED
|
@@ -75,24 +75,31 @@ const { data: results, status } = await useFetch<PaginatedResponse<Serialized<Co
|
|
|
75
75
|
|
|
76
76
|
const resultCount = computed(() => results.value?.total ?? results.value?.items?.length ?? 0);
|
|
77
77
|
|
|
78
|
-
// Federated search
|
|
78
|
+
// Federated search — uses server-side Postgres FTS
|
|
79
79
|
const fedResults = ref<any[]>([]);
|
|
80
80
|
const fedLoading = ref(false);
|
|
81
81
|
watch([query, activeType], async () => {
|
|
82
82
|
if (activeType.value !== 'fediverse' || !query.value) { fedResults.value = []; return; }
|
|
83
83
|
fedLoading.value = true;
|
|
84
84
|
try {
|
|
85
|
-
const data = await $fetch<{ items: any[]; total: number }>('/api/
|
|
86
|
-
params: { limit: 20 },
|
|
85
|
+
const data = await $fetch<{ items: any[]; total: number }>('/api/search/federated', {
|
|
86
|
+
params: { q: query.value, limit: 20 },
|
|
87
87
|
});
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
(
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
88
|
+
fedResults.value = data.items;
|
|
89
|
+
} catch {
|
|
90
|
+
// Fallback: if federated search endpoint not available, try client-side
|
|
91
|
+
try {
|
|
92
|
+
const data = await $fetch<{ items: any[]; total: number }>('/api/federation/timeline', {
|
|
93
|
+
params: { limit: 40 },
|
|
94
|
+
});
|
|
95
|
+
const q = query.value.toLowerCase();
|
|
96
|
+
fedResults.value = data.items.filter((item: any) =>
|
|
97
|
+
(item.title?.toLowerCase().includes(q)) ||
|
|
98
|
+
(item.content?.toLowerCase().includes(q)) ||
|
|
99
|
+
(item.summary?.toLowerCase().includes(q))
|
|
100
|
+
);
|
|
101
|
+
} catch { fedResults.value = []; }
|
|
102
|
+
}
|
|
96
103
|
fedLoading.value = false;
|
|
97
104
|
}, { immediate: true });
|
|
98
105
|
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { searchFederatedContent } from '@commonpub/server';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
const schema = z.object({
|
|
5
|
+
q: z.string().max(200),
|
|
6
|
+
limit: z.coerce.number().int().positive().max(100).optional(),
|
|
7
|
+
offset: z.coerce.number().int().min(0).optional(),
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Server-side federated content search using Postgres FTS.
|
|
12
|
+
* Replaces the client-side filtering on /api/federation/timeline.
|
|
13
|
+
*/
|
|
14
|
+
export default defineEventHandler(async (event) => {
|
|
15
|
+
requireFeature('federation');
|
|
16
|
+
const db = useDB();
|
|
17
|
+
const { q, limit, offset } = parseQueryParams(event, schema);
|
|
18
|
+
|
|
19
|
+
return searchFederatedContent(db, q, { limit, offset });
|
|
20
|
+
});
|
|
@@ -1,24 +1,54 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type {
|
|
3
|
-
import { contentFiltersSchema } from '@commonpub/schema';
|
|
1
|
+
import { searchContent } from '@commonpub/server';
|
|
2
|
+
import type { ContentSearchResult, ContentSearchOptions } from '@commonpub/server';
|
|
4
3
|
import { z } from 'zod';
|
|
5
4
|
|
|
6
|
-
const searchQuerySchema =
|
|
5
|
+
const searchQuerySchema = z.object({
|
|
7
6
|
q: z.string().max(200).optional(),
|
|
7
|
+
type: z.string().optional(),
|
|
8
|
+
sort: z.enum(['relevance', 'recent', 'popular']).optional(),
|
|
9
|
+
difficulty: z.string().optional(),
|
|
10
|
+
tags: z.string().optional(),
|
|
11
|
+
author: z.string().optional(),
|
|
12
|
+
dateFrom: z.string().optional(),
|
|
13
|
+
dateTo: z.string().optional(),
|
|
14
|
+
limit: z.coerce.number().int().positive().max(100).optional(),
|
|
15
|
+
offset: z.coerce.number().int().min(0).optional(),
|
|
8
16
|
});
|
|
9
17
|
|
|
10
|
-
export default defineEventHandler(async (event): Promise<
|
|
18
|
+
export default defineEventHandler(async (event): Promise<{ items: ContentSearchResult[]; total: number }> => {
|
|
11
19
|
const db = useDB();
|
|
12
|
-
const
|
|
13
|
-
const q =
|
|
20
|
+
const params = parseQueryParams(event, searchQuerySchema);
|
|
21
|
+
const q = params.q?.trim();
|
|
14
22
|
|
|
15
23
|
if (!q) {
|
|
16
24
|
return { items: [], total: 0 };
|
|
17
25
|
}
|
|
18
26
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
27
|
+
// Get Meilisearch client if configured
|
|
28
|
+
let meiliClient = null;
|
|
29
|
+
try {
|
|
30
|
+
const meiliUrl = process.env.MEILI_URL;
|
|
31
|
+
const meiliKey = process.env.MEILI_MASTER_KEY;
|
|
32
|
+
if (meiliUrl) {
|
|
33
|
+
const { MeiliSearch } = await import('meilisearch');
|
|
34
|
+
meiliClient = new MeiliSearch({ host: meiliUrl, apiKey: meiliKey });
|
|
35
|
+
}
|
|
36
|
+
} catch {
|
|
37
|
+
// Meilisearch not available — will use Postgres fallback
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const opts: ContentSearchOptions = {
|
|
41
|
+
query: q,
|
|
42
|
+
type: params.type,
|
|
43
|
+
difficulty: params.difficulty,
|
|
44
|
+
tags: params.tags?.split(',').map(t => t.trim()).filter(Boolean),
|
|
45
|
+
authorUsername: params.author,
|
|
46
|
+
dateFrom: params.dateFrom,
|
|
47
|
+
dateTo: params.dateTo,
|
|
48
|
+
sort: (params.sort as ContentSearchOptions['sort']) ?? 'relevance',
|
|
49
|
+
limit: params.limit,
|
|
50
|
+
offset: params.offset,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
return searchContent(db, opts, meiliClient);
|
|
24
54
|
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Search indexing plugin.
|
|
3
|
+
* Keeps Meilisearch in sync with content changes via hooks.
|
|
4
|
+
* If Meilisearch is not configured, this plugin is a no-op.
|
|
5
|
+
*/
|
|
6
|
+
import { onHook, indexContent, removeFromIndex, configureContentIndex } from '@commonpub/server';
|
|
7
|
+
import type { MeiliClient } from '@commonpub/server';
|
|
8
|
+
|
|
9
|
+
export default defineNitroPlugin(async () => {
|
|
10
|
+
if (process.env.NODE_ENV === 'test') return;
|
|
11
|
+
|
|
12
|
+
const meiliUrl = process.env.MEILI_URL;
|
|
13
|
+
const meiliKey = process.env.MEILI_MASTER_KEY;
|
|
14
|
+
|
|
15
|
+
if (!meiliUrl) {
|
|
16
|
+
console.log('[search-index] Meilisearch not configured — search uses Postgres FTS fallback');
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let client: MeiliClient;
|
|
21
|
+
try {
|
|
22
|
+
const { MeiliSearch } = await import('meilisearch');
|
|
23
|
+
client = new MeiliSearch({ host: meiliUrl, apiKey: meiliKey }) as unknown as MeiliClient;
|
|
24
|
+
await configureContentIndex(client);
|
|
25
|
+
console.log('[search-index] Meilisearch content index configured');
|
|
26
|
+
} catch (err) {
|
|
27
|
+
console.warn('[search-index] Failed to connect to Meilisearch:', err instanceof Error ? err.message : err);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Index on publish
|
|
32
|
+
onHook('content:published', async ({ db, contentId }) => {
|
|
33
|
+
try {
|
|
34
|
+
await indexContent(db, contentId, client);
|
|
35
|
+
} catch (err) {
|
|
36
|
+
console.warn('[search-index] Failed to index content:', err instanceof Error ? err.message : err);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Re-index on update
|
|
41
|
+
onHook('content:updated', async ({ db, contentId }) => {
|
|
42
|
+
try {
|
|
43
|
+
await indexContent(db, contentId, client);
|
|
44
|
+
} catch (err) {
|
|
45
|
+
console.warn('[search-index] Failed to re-index content:', err instanceof Error ? err.message : err);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Remove from index on delete
|
|
50
|
+
onHook('content:deleted', async ({ contentId }) => {
|
|
51
|
+
try {
|
|
52
|
+
await removeFromIndex(contentId, client);
|
|
53
|
+
} catch (err) {
|
|
54
|
+
console.warn('[search-index] Failed to remove from index:', err instanceof Error ? err.message : err);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
});
|