Skip to content

Commit 58bd1f2

Browse files
authored
feat: update get liked collections with meilisearch (#1717)
* feat: update get liked collections with meilisearch * refactor: remove unused schema * refactor: add tests * refactor: remove unused tests * refactor: fix tests * refactor: remove debug code
1 parent 6148bfa commit 58bd1f2

11 files changed

Lines changed: 238 additions & 173 deletions

File tree

src/services/item/plugins/publication/published/index.ts

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import {
1616
getCollectionsForMember,
1717
getInformations,
1818
getManyInformations,
19-
getMostLikedItems,
2019
getRecentCollections,
2120
publishItem,
2221
unpublishItem,
@@ -64,17 +63,6 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => {
6463
},
6564
);
6665

67-
fastify.get(
68-
'/collections/liked',
69-
{
70-
preHandler: optionalIsAuthenticated,
71-
schema: getMostLikedItems,
72-
},
73-
async ({ user, query: { limit } }) => {
74-
return itemPublishedService.getLikedItems(user?.account, buildRepositories(), limit);
75-
},
76-
);
77-
7866
fastify.post(
7967
'/collections/:itemId/publish',
8068
{

src/services/item/plugins/publication/published/plugins/search/index.test.ts

Lines changed: 114 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { StatusCodes } from 'http-status-codes';
22
import { MultiSearchParams } from 'meilisearch';
3+
import { v4 } from 'uuid';
34
import waitForExpect from 'wait-for-expect';
45

56
import { FastifyInstance } from 'fastify';
@@ -87,7 +88,7 @@ describe('Collection Search endpoints', () => {
8788
queries: [
8889
{
8990
attributesToHighlight: ['*'],
90-
filter: 'isHidden = false',
91+
filter: 'isPublishedRoot = true AND isHidden = false',
9192
indexUid: MOCK_INDEX,
9293
q: undefined,
9394
},
@@ -144,7 +145,8 @@ describe('Collection Search endpoints', () => {
144145
{
145146
attributesToHighlight: ['*'],
146147
q: 'random query',
147-
filter: "discipline IN ['random filter'] AND isHidden = false",
148+
filter:
149+
"discipline IN ['random filter'] AND isPublishedRoot = true AND isHidden = false",
148150
indexUid: MOCK_INDEX,
149151
},
150152
],
@@ -173,7 +175,7 @@ describe('Collection Search endpoints', () => {
173175
{
174176
attributesToHighlight: ['*'],
175177
q: 'random query',
176-
filter: 'isHidden = false',
178+
filter: 'isPublishedRoot = true AND isHidden = false',
177179
indexUid: MOCK_INDEX,
178180
},
179181
],
@@ -450,4 +452,113 @@ describe('Collection Search endpoints', () => {
450452
expect(res.json()).toEqual(fakeResponse.results[0].facetDistribution.discipline);
451453
});
452454
});
455+
456+
describe('GET /collections/liked', () => {
457+
it('get most liked items', async () => {
458+
// Meilisearch is mocked so format of API doesn't matter, we just want it to proxy MultiSearchParams;
459+
const fakeResponse = {
460+
results: [
461+
{
462+
indexUid: 'index',
463+
hits: [
464+
{
465+
name: 'Geogebra',
466+
description: 'Interactive tools from geogebra for mathematics.',
467+
content: '',
468+
creator: {
469+
id: v4(),
470+
name: 'Graasper',
471+
},
472+
level: [],
473+
discipline: [],
474+
'resource-type': [],
475+
id: v4(),
476+
type: 'folder',
477+
isPublishedRoot: true,
478+
isHidden: false,
479+
createdAt: '2021-10-20T13:12:47.821Z',
480+
updatedAt: '2021-10-23T09:25:39.798Z',
481+
lang: 'en',
482+
likes: 9,
483+
_formatted: {
484+
name: 'Geogebra',
485+
description: 'Interactive tools from geogebra for mathematics.',
486+
content: '',
487+
creator: {
488+
id: v4(),
489+
name: 'Graasper',
490+
},
491+
level: [],
492+
discipline: [],
493+
'resource-type': [],
494+
id: v4(),
495+
type: 'folder',
496+
isPublishedRoot: true,
497+
isHidden: false,
498+
createdAt: '2021-10-20T13:12:47.821Z',
499+
updatedAt: '2021-10-23T09:25:39.798Z',
500+
lang: 'en',
501+
likes: 9,
502+
},
503+
},
504+
{
505+
name: 'PhET',
506+
content: '',
507+
description: '',
508+
creator: {
509+
id: v4(),
510+
name: 'Graasper',
511+
},
512+
level: [],
513+
discipline: [],
514+
'resource-type': [],
515+
id: v4(),
516+
type: 'folder',
517+
isPublishedRoot: true,
518+
isHidden: false,
519+
createdAt: '2021-10-20T13:03:42.712Z',
520+
updatedAt: '2021-11-10T10:49:39.296Z',
521+
lang: 'en',
522+
likes: 7,
523+
_formatted: {
524+
name: 'PhET',
525+
description: '',
526+
content: '',
527+
creator: {
528+
id: v4(),
529+
name: 'Graasper',
530+
},
531+
level: [],
532+
discipline: [],
533+
'resource-type': [],
534+
id: v4(),
535+
type: 'folder',
536+
isPublishedRoot: true,
537+
isHidden: false,
538+
createdAt: '2021-10-20T13:03:42.712Z',
539+
updatedAt: '2021-11-10T10:49:39.296Z',
540+
lang: 'en',
541+
likes: 7,
542+
},
543+
},
544+
] as never[],
545+
processingTimeMs: 123,
546+
query: '',
547+
},
548+
],
549+
};
550+
jest.spyOn(MeiliSearchWrapper.prototype, 'search').mockResolvedValue(fakeResponse);
551+
552+
const res = await app.inject({
553+
method: HttpMethod.Get,
554+
url: `${ITEMS_ROUTE_PREFIX}/collections/liked`,
555+
});
556+
expect(res.statusCode).toBe(StatusCodes.OK);
557+
558+
// should return the likes property
559+
res.json().hits.forEach(({ likes }) => {
560+
expect(likes).toBeGreaterThanOrEqual(0);
561+
});
562+
});
563+
});
453564
});

src/services/item/plugins/publication/published/plugins/search/index.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { MEILISEARCH_REBUILD_SECRET } from '../../../../../../../utils/config';
99
import { buildRepositories } from '../../../../../../../utils/repositories';
1010
import { ActionService } from '../../../../../../action/services/action';
1111
import { optionalIsAuthenticated } from '../../../../../../auth/plugins/passport';
12-
import { getFacets, search } from './schemas';
12+
import { getFacets, getMostLiked, search } from './schemas';
1313
import { SearchService } from './service';
1414

1515
export type SearchFields = {
@@ -53,6 +53,15 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => {
5353
},
5454
);
5555

56+
fastify.get(
57+
'/collections/liked',
58+
{ preHandler: optionalIsAuthenticated, schema: getMostLiked },
59+
async ({ query }) => {
60+
const searchResults = await searchService.getMostLiked(query.limit);
61+
return searchResults;
62+
},
63+
);
64+
5665
fastify.get('/collections/search/rebuild', async ({ headers }, reply) => {
5766
// TODO: in the future, lock this behind admin permission and maybe add a button to the frontend admin panel
5867
const headerRebuildSecret = headers['meilisearch-rebuild'];

src/services/item/plugins/publication/published/plugins/search/meilisearch.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ const SEARCHABLE_ATTRIBUTES: (keyof IndexItem)[] = [
4444
'creator',
4545
...Object.values(TagCategory),
4646
];
47-
const SORT_ATTRIBUTES: (keyof IndexItem)[] = ['name', 'updatedAt', 'createdAt'];
47+
const SORT_ATTRIBUTES: (keyof IndexItem)[] = ['name', 'updatedAt', 'createdAt', 'likes'];
4848
const DISPLAY_ATTRIBUTES: (keyof IndexItem)[] = [
4949
'id',
5050
'name',

src/services/item/plugins/publication/published/plugins/search/schemas.ts

Lines changed: 68 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,56 @@ import { ItemType, TagCategory } from '@graasp/sdk';
77

88
import { customType } from '../../../../../../../plugins/typebox';
99
import { errorSchemaRef } from '../../../../../../../schemas/global';
10+
import { GET_MOST_LIKED_ITEMS_MAXIMUM } from '../../../../../../../utils/config';
11+
12+
const meilisearchSearchResponseSchema = customType.StrictObject({
13+
totalHits: Type.Optional(Type.Number()),
14+
estimatedTotalHits: Type.Optional(Type.Number()),
15+
processingTimeMs: Type.Number(),
16+
query: Type.String(),
17+
hits: Type.Array(
18+
customType.StrictObject({
19+
name: Type.String(),
20+
description: Type.String(),
21+
content: Type.String(),
22+
creator: customType.StrictObject({
23+
id: customType.UUID(),
24+
name: Type.String(),
25+
}),
26+
level: Type.Array(Type.String()),
27+
discipline: Type.Array(Type.String()),
28+
'resource-type': Type.Array(Type.String()),
29+
id: customType.UUID(),
30+
type: Type.Enum(ItemType),
31+
isPublishedRoot: Type.Boolean(),
32+
isHidden: Type.Boolean(),
33+
createdAt: customType.DateTime(),
34+
updatedAt: customType.DateTime(),
35+
lang: Type.String(),
36+
likes: Type.Number(),
37+
_formatted: customType.StrictObject({
38+
name: Type.String(),
39+
description: Type.String(),
40+
content: Type.String(),
41+
creator: customType.StrictObject({
42+
id: customType.UUID(),
43+
name: Type.String(),
44+
}),
45+
level: Type.Array(Type.String()),
46+
discipline: Type.Array(Type.String()),
47+
'resource-type': Type.Array(Type.String()),
48+
id: customType.UUID(),
49+
type: Type.String(),
50+
isPublishedRoot: Type.Boolean(),
51+
isHidden: Type.Boolean(),
52+
createdAt: customType.DateTime(),
53+
updatedAt: customType.DateTime(),
54+
lang: Type.String(),
55+
likes: Type.Number(),
56+
}),
57+
}),
58+
),
59+
});
1060

1161
export const search = {
1262
operationId: 'collectionSearch',
@@ -33,54 +83,24 @@ export const search = {
3383
}),
3484
),
3585
response: {
36-
[StatusCodes.OK]: customType.StrictObject({
37-
totalHits: Type.Optional(Type.Number()),
38-
estimatedTotalHits: Type.Optional(Type.Number()),
39-
processingTimeMs: Type.Number(),
40-
query: Type.String(),
41-
hits: Type.Array(
42-
customType.StrictObject({
43-
name: Type.String(),
44-
description: Type.String(),
45-
content: Type.String(),
46-
creator: customType.StrictObject({
47-
id: customType.UUID(),
48-
name: Type.String(),
49-
}),
50-
level: Type.Array(Type.String()),
51-
discipline: Type.Array(Type.String()),
52-
'resource-type': Type.Array(Type.String()),
53-
id: customType.UUID(),
54-
type: Type.Enum(ItemType),
55-
isPublishedRoot: Type.Boolean(),
56-
isHidden: Type.Boolean(),
57-
createdAt: customType.DateTime(),
58-
updatedAt: customType.DateTime(),
59-
lang: Type.String(),
60-
likes: Type.Number(),
61-
_formatted: customType.StrictObject({
62-
name: Type.String(),
63-
description: Type.String(),
64-
content: Type.String(),
65-
creator: customType.StrictObject({
66-
id: customType.UUID(),
67-
name: Type.String(),
68-
}),
69-
level: Type.Array(Type.String()),
70-
discipline: Type.Array(Type.String()),
71-
'resource-type': Type.Array(Type.String()),
72-
id: customType.UUID(),
73-
type: Type.String(),
74-
isPublishedRoot: Type.Boolean(),
75-
isHidden: Type.Boolean(),
76-
createdAt: customType.DateTime(),
77-
updatedAt: customType.DateTime(),
78-
lang: Type.String(),
79-
likes: Type.Number(),
80-
}),
81-
}),
82-
),
83-
}),
86+
[StatusCodes.OK]: meilisearchSearchResponseSchema,
87+
'4xx': errorSchemaRef,
88+
},
89+
} as const satisfies FastifySchema;
90+
91+
export const getMostLiked = {
92+
operationId: 'getMostLikedCollections',
93+
tags: ['collection', 'like'],
94+
summary: 'Get most liked collections',
95+
description: 'Get most liked collections.',
96+
97+
querystring: customType.StrictObject({
98+
limit: Type.Optional(
99+
Type.Number({ minimum: 1, maximum: GET_MOST_LIKED_ITEMS_MAXIMUM, default: 12 }),
100+
),
101+
}),
102+
response: {
103+
[StatusCodes.OK]: meilisearchSearchResponseSchema,
84104
'4xx': errorSchemaRef,
85105
},
86106
} as const satisfies FastifySchema;

0 commit comments

Comments
 (0)