@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.30",
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/learning": "0.5.0",
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.18.0",
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/federation/timeline', {
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
- // Client-side filter by query since the timeline endpoint doesn't have search
89
- const q = query.value.toLowerCase();
90
- fedResults.value = data.items.filter((item: any) =>
91
- (item.title?.toLowerCase().includes(q)) ||
92
- (item.content?.toLowerCase().includes(q)) ||
93
- (item.summary?.toLowerCase().includes(q))
94
- );
95
- } catch { fedResults.value = []; }
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 { listContent } from '@commonpub/server';
2
- import type { PaginatedResponse, ContentListItem } from '@commonpub/server';
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 = contentFiltersSchema.extend({
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<PaginatedResponse<ContentListItem>> => {
18
+ export default defineEventHandler(async (event): Promise<{ items: ContentSearchResult[]; total: number }> => {
11
19
  const db = useDB();
12
- const filters = parseQueryParams(event, searchQuerySchema);
13
- const q = filters.q || filters.search;
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
- return listContent(db, {
20
- ...filters,
21
- status: 'published',
22
- search: q,
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
+ });