Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • web-et-numerique/factory/pamn_plateforme-des-acteurs-de-la-mediation-numerique/pamn_server
1 result
Show changes
Commits on Source (7)
Showing
with 504 additions and 37 deletions
...@@ -37,11 +37,15 @@ deploy_dev: ...@@ -37,11 +37,15 @@ deploy_dev:
test: test:
stage: test stage: test
image: ${CI_DEPENDENCY_PROXY_DIRECT_GROUP_IMAGE_PREFIX}/node:14.15.4 image: ${CI_DEPENDENCY_PROXY_DIRECT_GROUP_IMAGE_PREFIX}/node:14.15.4
services:
- name: ${CI_DEPENDENCY_PROXY_DIRECT_GROUP_IMAGE_PREFIX}/elasticsearch:7.16.2
alias: elasticsearch
command: ['bin/elasticsearch', '-Expack.security.enabled=false', '-Ediscovery.type=single-node']
before_script: before_script:
- export GHOST_HOST_AND_PORT=http://localhost:2368 - export GHOST_HOST_AND_PORT=http://localhost:2368
- export GHOST_ADMIN_API_KEY=60142bc9e33940000156bccc:6217742e2671e322612e89cac9bab61fcd01822709fe5d8f5e6a5b3e54d5e6bb - export GHOST_ADMIN_API_KEY=60142bc9e33940000156bccc:6217742e2671e322612e89cac9bab61fcd01822709fe5d8f5e6a5b3e54d5e6bb
- export SALT=$TEST_SALT - export SALT=$TEST_SALT
- export ELASTICSEARCH_NODE=http://localhost:9200 - export ELASTICSEARCH_NODE=http://elasticsearch:9200
script: script:
- npm i - npm i
- npm run test:cov - npm run test:cov
......
...@@ -2,6 +2,15 @@ ...@@ -2,6 +2,15 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
## [1.13.0](https://forge.grandlyon.com/web-et-numerique/pamn_plateforme-des-acteurs-de-la-mediation-numerique/pamn_server/compare/v1.12.0...v1.13.0) (2022-02-07)
### Features
* **pages:** display pages from ghost (eg. : about page and accessibility page) ([e4096e8](https://forge.grandlyon.com/web-et-numerique/pamn_plateforme-des-acteurs-de-la-mediation-numerique/pamn_server/commit/e4096e89266fb1c206089f739c0d2bcb15ad69c6))
* **routing:** update global routing of api ([f7fd011](https://forge.grandlyon.com/web-et-numerique/pamn_plateforme-des-acteurs-de-la-mediation-numerique/pamn_server/commit/f7fd0113461879bc10fabb2ab51d07551342e906))
* **search:** search-improvement ([fbbf2fb](https://forge.grandlyon.com/web-et-numerique/pamn_plateforme-des-acteurs-de-la-mediation-numerique/pamn_server/commit/fbbf2fb6142d879bad296c104c1e4da42e93dc3a))
## [1.12.0](https://forge.grandlyon.com/web-et-numerique/pamn_plateforme-des-acteurs-de-la-mediation-numerique/pamn_server/compare/v1.11.0...v1.12.0) (2022-02-01) ## [1.12.0](https://forge.grandlyon.com/web-et-numerique/pamn_plateforme-des-acteurs-de-la-mediation-numerique/pamn_server/compare/v1.11.0...v1.12.0) (2022-02-01)
......
{ {
"name": "ram_server", "name": "ram_server",
"version": "1.12.0", "version": "1.13.0",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
......
{ {
"name": "ram_server", "name": "ram_server",
"private": true, "private": true,
"version": "1.12.0", "version": "1.13.0",
"description": "Nest TypeScript starter repository", "description": "Nest TypeScript starter repository",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
......
[
{
"slug": "qui-sommes-nous",
"id": "61efc44b26db9e00015c4f32",
"title": "Qui sommes-nous ?",
"html": "<p>La numérisation accélérée des différents services privés et publics ainsi que la crise sanitaire que nous traversons a renforcé une fracture numérique déjà forte pour un nombre important de citoyens.</p><p>Au printemps 2019, la Métropole de Lyon s'est saisie des enjeux autour de l'inclusion numérique en initiant la structuration d'un réseau des acteurs de la médiation numérique sur son territoire. Son objectif est de mettre en relation les acteurs qui œuvrent au quotidien pour limiter cette fracture numérique, nombreux sur le territoire de la Métropole : associations, centres sociaux, structures informations jeunesses, grands opérateurs de services publics, collectivités...</p><p>Des ateliers de travail ont été organisés en 2019 pour identifier les besoins de ces acteurs et 9 offres de services ont été identifiées :</p><h3 id=\"recenser-et-partager-des-ressources-existantes-optimisation\">Recenser et partager des ressources existantes (optimisation)</h3><figure class=\"kg-card kg-image-card\"><img src=\"http://localhost:4200/assets/img/about_illustration_1.jpg\" class=\"kg-image\" alt=\"illustration des besoins\" loading=\"lazy\"></figure><h3 id=\"co-construire-de-nouvelles-ressources-d%C3%A9veloppement\">Co-construire de nouvelles ressources (développement)</h3><figure class=\"kg-card kg-image-card\"><img src=\"http://localhost:4200/assets/img/about_illustration_2.jpg\" class=\"kg-image\" alt=\"illustration des besoins\" loading=\"lazy\"></figure><p>Cet espace vise à centraliser et mettre en commun les ressources développées dans le cadre du réseau par ses acteurs.</p><p>N'hésitez pas à contribuer à cet espace en partageant vos ressources</p><!--kg-card-begin: markdown--><div style=\"display:flex; margin:0;padding:5px;align-items:center;\"\n</div><!--kg-card-end: markdown--><figure class=\"kg-card kg-image-card\"><img src=\"http://localhost:2368/content/images/2022/01/logo_europe.png\" class=\"kg-image\" alt loading=\"lazy\"></figure><figure class=\"kg-card kg-image-card\"><img src=\"http://localhost:2368/content/images/2022/01/logo_region.png\" class=\"kg-image\" alt=\"logo de l'union européenne\" loading=\"lazy\" width=\"220\" height=\"98\"></figure>",
"feature_image": null,
"featured": false,
"visibility": "public",
"created_at": "2022-01-25T09:35:07.000+00:00",
"updated_at": "2022-01-25T09:35:07.000+00:00",
"published_at": "2022-01-25T09:35:07.000+00:00",
"status": "published",
"custom_excerpt": null,
"codeinjection_head": null,
"codeinjection_foot": null,
"custom_template": null,
"canonical_url": null,
"url": "http://localhost:2368/qui-sommes-nous/",
"excerpt": "La numérisation accélérée des différents services privés et publics ainsi que la\ncrise sanitaire que nous traversons a renforcé une fracture numérique déjà forte\npour un nombre important de citoyens.\n\nAu printemps 2019, la Métropole de Lyon s'est saisie des enjeux autour de\nl'inclusion numérique en initiant la structuration d'un réseau des acteurs de la\nmédiation numérique sur son territoire. Son objectif est de mettre en relation\nles acteurs qui œuvrent au quotidien pour limiter cette fracture ",
"page": true,
"og_image": null,
"og_title": null,
"og_description": null,
"twitter_image": null,
"twitter_title": null,
"twitter_description": null,
"meta_title": null,
"meta_description": null,
"frontmatter": null
},
{
"slug": "accessibilite",
"id": "61effea226db9e00015c4f41",
"title": "Accessibilité",
"html": "<p>Page accessibilité. Contenu en cours de rédaction. </p>",
"feature_image": null,
"featured": false,
"visibility": "public",
"created_at": "2022-01-25T13:44:02.000+00:00",
"updated_at": "2022-01-25T13:44:02.000+00:00",
"published_at": "2022-01-25T13:44:02.000+00:00",
"status": "published",
"custom_excerpt": null,
"codeinjection_head": null,
"codeinjection_foot": null,
"custom_template": null,
"canonical_url": null,
"url": "http://localhost:2368/accessibilite/",
"excerpt": "Page accessibilité. Contenu a venir.",
"og_image": null,
"og_title": null,
"og_description": null,
"twitter_image": null,
"twitter_title": null,
"twitter_description": null,
"meta_title": null,
"meta_description": null,
"frontmatter": null
}
]
...@@ -5,6 +5,8 @@ const tagsData = require('./ghost/migrations/init/tags.json'); ...@@ -5,6 +5,8 @@ const tagsData = require('./ghost/migrations/init/tags.json');
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
const postsData = require('./ghost/migrations/init/posts.json'); const postsData = require('./ghost/migrations/init/posts.json');
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
const pagesData = require('./ghost/migrations/init/pages.json');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const path = require('path'); const path = require('path');
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
const GhostAdminAPI = require('@tryghost/admin-api'); const GhostAdminAPI = require('@tryghost/admin-api');
...@@ -21,7 +23,7 @@ var api = new GhostAdminAPI({ ...@@ -21,7 +23,7 @@ var api = new GhostAdminAPI({
async function deleteTags(existingTags) { async function deleteTags(existingTags) {
return await Promise.all( return await Promise.all(
_.forEach(existingTags, async (tag) => { _.forEach(existingTags, async (tag) => {
api.tags await api.tags
.delete(_.pick(tag, ['id'])) .delete(_.pick(tag, ['id']))
.then((res) => { .then((res) => {
return null; return null;
...@@ -33,7 +35,7 @@ async function deleteTags(existingTags) { ...@@ -33,7 +35,7 @@ async function deleteTags(existingTags) {
async function deletePosts(existingPosts) { async function deletePosts(existingPosts) {
return await Promise.all( return await Promise.all(
_.forEach(existingPosts, async (tag) => { _.forEach(existingPosts, async (tag) => {
api.posts await api.posts
.delete(_.pick(tag, ['id'])) .delete(_.pick(tag, ['id']))
.then((res) => { .then((res) => {
return null; return null;
...@@ -42,6 +44,18 @@ async function deletePosts(existingPosts) { ...@@ -42,6 +44,18 @@ async function deletePosts(existingPosts) {
}) })
); );
} }
async function deletePages(existingPages) {
return await Promise.all(
_.forEach(existingPages, async (id) => {
await api.pages
.delete(_.pick(id, ['id']))
.then((res) => {
return null;
})
.catch((error) => console.error(error));
})
);
}
async function createTags(deleteOnly) { async function createTags(deleteOnly) {
// Get existing tags // Get existing tags
...@@ -100,22 +114,22 @@ function processImagesInHTML(html) { ...@@ -100,22 +114,22 @@ function processImagesInHTML(html) {
}); });
} }
async function uploadPostImage(imagePath) async function uploadPostImage(imagePath) {
{
let imagePromise = api.images.upload({ let imagePromise = api.images.upload({
ref: imagePath, ref: imagePath,
file: path.resolve(imagePath) file: path.resolve(imagePath),
}); });
return Promise.resolve(imagePromise).then((url) => { return Promise.resolve(imagePromise)
.then((url) => {
return url.url; return url.url;
}).catch((error) => {
console.error(error);
return null
}) })
.catch((error) => {
console.error(error);
return null;
});
} }
async function createPosts(deleteOnly) { async function createPosts(deleteOnly) {
api.posts api.posts
.browse({ limit: 'all' }) .browse({ limit: 'all' })
...@@ -152,10 +166,52 @@ async function createPosts(deleteOnly) { ...@@ -152,10 +166,52 @@ async function createPosts(deleteOnly) {
.catch((error) => console.error(error)); .catch((error) => console.error(error));
} }
async function createPages(deleteOnly) {
api.pages
.browse({ limit: 'all' })
.then(async (existingPages) => {
// remove 'meta' key
delete existingPages['meta'];
if (existingPages.length > 0) {
console.log(`-- Dropping ${existingPages.length} pages... --`);
// Delete existing pages
await deletePages(existingPages).then(() => {
console.log('-- Pages dropped --');
});
} else {
console.log('-- No pages to drop --');
}
// wait complete delete of pages, if not page slugs are appended by _2
await new Promise((r) => setTimeout(r, 1000));
// Creating new pages
if (!deleteOnly) {
console.log(`-- Creating ${pagesData.length} pages --`);
_.forEach(pagesData, async (page) => {
//upload de l'image en featured_image
if (page.feature_image) {
page.feature_image = await uploadPostImage(page.feature_image);
}
api.pages
.add(page, { source: 'html' })
.then((res) => {
console.log(`-- Page \`${res.title}\` created --`);
})
.catch((error) => console.error(error));
});
}
})
.catch((error) => console.error(error));
}
async function main(deleteOnly) { async function main(deleteOnly) {
createTags(deleteOnly).then(() => { createTags(deleteOnly)
createPosts(deleteOnly); .then(() => {
}); createPosts(deleteOnly);
})
.then(() => {
createPages(deleteOnly);
});
} }
var myArgs = process.argv.slice(2); var myArgs = process.argv.slice(2);
......
...@@ -11,6 +11,7 @@ import { MailerModule } from './mailer/mailer.module'; ...@@ -11,6 +11,7 @@ import { MailerModule } from './mailer/mailer.module';
import { TclModule } from './tcl/tcl.module'; import { TclModule } from './tcl/tcl.module';
import { AdminModule } from './admin/admin.module'; import { AdminModule } from './admin/admin.module';
import { PostsModule } from './posts/posts.module'; import { PostsModule } from './posts/posts.module';
import { PagesModule } from './pages/pages.module';
import { TempUserModule } from './temp-user/temp-user.module'; import { TempUserModule } from './temp-user/temp-user.module';
import { NewsletterModule } from './newsletter/newsletter.module'; import { NewsletterModule } from './newsletter/newsletter.module';
import { ContactModule } from './contact/contact.module'; import { ContactModule } from './contact/contact.module';
...@@ -29,6 +30,7 @@ import { ContactModule } from './contact/contact.module'; ...@@ -29,6 +30,7 @@ import { ContactModule } from './contact/contact.module';
TclModule, TclModule,
AdminModule, AdminModule,
PostsModule, PostsModule,
PagesModule,
TempUserModule, TempUserModule,
NewsletterModule, NewsletterModule,
ContactModule, ContactModule,
......
...@@ -7,13 +7,14 @@ async function bootstrap() { ...@@ -7,13 +7,14 @@ async function bootstrap() {
const app = await NestFactory.create(AppModule); const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe()); app.useGlobalPipes(new ValidationPipe());
const options = new DocumentBuilder() const options = new DocumentBuilder()
.setTitle('RAM') .setTitle(`Res'in`)
.setDescription('RAM API description') .setDescription(`Res'in API documentation`)
.setVersion('1.0') .setVersion('1.0')
.addBearerAuth({ type: 'http', scheme: 'bearer', bearerFormat: 'JWT' }, 'JWT') .addBearerAuth({ type: 'http', scheme: 'bearer', bearerFormat: 'JWT' }, 'JWT')
.build(); .build();
const document = SwaggerModule.createDocument(app, options); const document = SwaggerModule.createDocument(app, options);
SwaggerModule.setup('api', app, document); SwaggerModule.setup('doc', app, document);
app.setGlobalPrefix('api');
await app.listen(3000); await app.listen(3000);
} }
bootstrap(); bootstrap();
import { HttpModule, HttpService } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import { of } from 'rxjs';
import { ConfigurationModule } from '../configuration/configuration.module';
import { PagesController } from './pages.controller';
import { AxiosResponse } from 'axios';
describe('PagesController', () => {
let controller: PagesController;
const httpServiceMock = {
get: jest.fn(),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [ConfigurationModule, HttpModule],
providers: [
{
provide: HttpService,
useValue: httpServiceMock,
},
],
controllers: [PagesController],
}).compile();
controller = module.get<PagesController>(PagesController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
describe('getPagebySlug', () => {
it('should get page Hello by Slug hello', async () => {
const data = [
{
id: '61c4847b0ff4550001505090',
uuid: 'f4ee5a37-a343-4cad-8a32-3f6cf87f9569',
title: 'Hello',
slug: 'hello',
html: '<p>Test</p>',
comment_id: '61c4847b0ff4550001505090',
feature_image: 'http://localhost:2368/content/images/2021/12/dacc-4.png',
featured: false,
visibility: 'public',
created_at: '2022-01-25T09:35:07.000+00:00',
updated_at: '2022-01-26T15:24:24.000+00:00',
published_at: '2022-01-25T09:35:16.000+00:00',
custom_excerpt: null,
codeinjection_head: null,
codeinjection_foot: null,
custom_template: null,
canonical_url: null,
url: 'http://localhost:2368/hello/',
excerpt:
'« Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed non risus.\n' +
'Suspendisse lectus tortor, dignissim sit amet, adipiscing nec, ultricies sed,\n' +
'dolor. Cras elementum ultrices diam. Maecenas ligula massa, varius a, semper\n' +
'congue, euismod non, mi. Proin porttitor, orci nec nonummy molestie, enim est\n' +
'eleifend mi, non fermentum diam nisl sit amet erat. Duis semper. Duis arcu\n' +
'massa, scelerisque vitae, consequat in, pretium a, enim. Pellentesque congue. Ut\n' +
'in risus volutpat libero pharetra tem',
reading_time: 1,
page: true,
access: true,
og_image: null,
og_title: null,
og_description: null,
twitter_image: null,
twitter_title: null,
twitter_description: null,
meta_title: null,
meta_description: null,
frontmatter: null,
},
];
const axiosResult: AxiosResponse = {
data: {
pages: data,
},
status: 200,
statusText: 'OK',
headers: {},
config: {},
};
httpServiceMock.get.mockImplementationOnce(() => of(axiosResult));
const result = await (await controller.getPagebySlug('hello')).toPromise();
expect(result.pages).toStrictEqual(data);
});
});
});
import { Controller, Get, HttpService, HttpException, HttpStatus, Logger, Param } from '@nestjs/common';
import { ConfigurationService } from '../configuration/configuration.service';
import { Observable } from 'rxjs';
import { catchError, map } from 'rxjs/operators';
import { Page } from './schemas/page.schema';
import { rewriteGhostImgUrl } from '../shared/utils';
@Controller('pages')
export class PagesController {
private readonly logger = new Logger(PagesController.name);
constructor(private readonly httpService: HttpService, private readonly configService: ConfigurationService) {}
@Get(':slug')
public async getPagebySlug(@Param('slug') slug: string): Promise<Observable<{ pages: Page[] }>> {
return this.httpService
.get(`${process.env.GHOST_HOST_AND_PORT}/ghost/api/v3/content/pages/slug/` + slug, {
params: {
key: process.env.GHOST_CONTENT_API_KEY,
},
})
.pipe(
map((response) => {
return { pages: [rewriteGhostImgUrl(this.configService, response.data.pages[0])] };
}),
catchError((err) => {
this.logger.error(err);
throw new HttpException('Page not found:' + err, HttpStatus.NOT_FOUND);
})
);
}
}
import { HttpModule, Module } from '@nestjs/common';
import { PagesController } from './pages.controller';
@Module({
imports: [HttpModule],
controllers: [PagesController],
})
export class PagesModule {}
export class Page {
id: string;
uuid: string;
title: string;
slug: string;
html: string;
comment_id: string;
feature_image: string;
featured: false;
visibility: string;
email_recipient_filter: string;
created_at: string;
updated_at: string;
published_at: string;
url: string;
excerpt: string;
custom_excerpt: string;
reading_time: string;
access: boolean;
send_email_when_published: boolean;
og_image: string;
og_title: string;
og_description: string;
twitter_image: string;
twitter_title: string;
twitter_description: string;
meta_title: string;
meta_description: string;
email_subject: string;
codeinjection_head: string;
codeinjection_foot: string;
custom_template: string;
canonical_url: string;
tags: [];
authors: [];
primary_author: [];
primary_tag: [];
frontmatter: [];
}
...@@ -5,6 +5,7 @@ import { ConfigurationService } from '../configuration/configuration.service'; ...@@ -5,6 +5,7 @@ import { ConfigurationService } from '../configuration/configuration.service';
import { TagEnum } from './enums/tag.enum'; import { TagEnum } from './enums/tag.enum';
import { Post } from './schemas/post.schema'; import { Post } from './schemas/post.schema';
import { Tag } from './schemas/tag.schema'; import { Tag } from './schemas/tag.schema';
import { rewriteGhostImgUrl } from '../shared/utils';
@Injectable() @Injectable()
export class PostsService { export class PostsService {
...@@ -74,15 +75,7 @@ export class PostsService { ...@@ -74,15 +75,7 @@ export class PostsService {
} }
// Handle image display. Rewrite image URL to fit ghost infra issue. // Handle image display. Rewrite image URL to fit ghost infra issue.
if (!this.configService.isLocalConf()) { postData = rewriteGhostImgUrl(this.configService, postData);
if (postData.feature_image) {
postData.feature_image = `https://${this.configService.config.host}/blog/content${
postData.feature_image.split('/content')[1]
}`;
}
const regex = /(https?:\/\/ghost):(\d*)?/g;
postData.html = postData.html.replace(regex, `https://${this.configService.config.host}/blog`);
}
return postData; return postData;
} }
} }
import { ConfigurationService } from '../configuration/configuration.service';
import { Page } from '../pages/schemas/page.schema';
import { Post } from '../posts/schemas/post.schema';
export function rewriteGhostImgUrl(configService: ConfigurationService, itemData: Page | Post): Page | Post {
// Handle image display. Rewrite image URL to fit ghost infra issue.
if (!configService.isLocalConf()) {
if (itemData.feature_image) {
itemData.feature_image = `https://${configService.config.host}/blog/content${
itemData.feature_image.split('/content')[1]
}`;
}
const regex = /(https?:\/\/ghost):(\d*)?/g;
itemData.html = itemData.html.replace(regex, `https://${configService.config.host}/blog`);
}
return itemData;
}
...@@ -3,6 +3,7 @@ import { StructureSearchBody } from './structure-search-body.interface'; ...@@ -3,6 +3,7 @@ import { StructureSearchBody } from './structure-search-body.interface';
export interface StructureSearchResult { export interface StructureSearchResult {
hits: { hits: {
total: number; total: number;
max_score: number;
hits: Array<{ hits: Array<{
_score: number; _score: number;
_source: StructureSearchBody; _source: StructureSearchBody;
......
...@@ -44,6 +44,7 @@ describe('StructuresService', () => { ...@@ -44,6 +44,7 @@ describe('StructuresService', () => {
}).compile(); }).compile();
service = module.get<StructuresService>(StructuresService); service = module.get<StructuresService>(StructuresService);
service['structuresSearchService']['index'] = 'structures-unit-test';
}); });
it('should be defined', () => { it('should be defined', () => {
......
import { Logger } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { Test, TestingModule } from '@nestjs/testing';
import { StructuresForSearchServiceMock } from '../../../test/mock/services/structures-for-search.mock.service';
import { MailerModule } from '../../mailer/mailer.module';
import { SearchModule } from '../../search/search.module';
import { StructuresSearchService } from './structures-search.service';
import { StructuresService } from './structures.service';
describe('StructuresSearchService', () => {
let service: StructuresSearchService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [MailerModule, SearchModule, ConfigModule],
providers: [
StructuresSearchService,
{
provide: StructuresService,
useClass: StructuresForSearchServiceMock,
},
],
}).compile();
service = module.get<StructuresSearchService>(StructuresSearchService);
service['index'] = 'structures-unit-test';
});
it('should be defined', () => {
expect(service).toBeDefined();
});
it('should create index', async () => {
await service.dropIndex();
const res = await service.createStructureIndex();
expect(res).toBeTruthy();
});
it('should index structures', async () => {
const structuresForSearchService = new StructuresForSearchServiceMock();
const structures = structuresForSearchService.findAll();
const res = await Promise.all(
structures.map((structure: any) => {
service.indexStructure(structure);
})
);
expect(res).toBeTruthy();
// wait for the new structures to be indexed before search
await service.refreshIndexStructure();
// but we still need to wait the refresh to be done
await new Promise((r) => setTimeout(r, 1000));
});
it('should find maisons de la métropole', async () => {
const res = await service.search('maison de la');
//Logger.log(JSON.stringify(res));
expect(res[0].structureName).toContain('Maison de la Métropole');
expect(res[1].structureName).toContain('Maison de la Métropole');
});
it('should find metropole', async () => {
const res = await service.search('metropole');
expect(res[0].structureName).toContain('Métropole');
});
it('should find text in description', async () => {
const res = await service.search('liseuse');
expect(res.length).toBe(1);
expect(res[0].structureName).toContain("Médiathèque d'Ecully");
});
it('should drop index', async () => {
const res = await service.dropIndex();
expect(res).toBeTruthy();
});
});
...@@ -29,6 +29,20 @@ export class StructuresSearchService { ...@@ -29,6 +29,20 @@ export class StructuresSearchService {
public async createStructureIndex(): Promise<any> { public async createStructureIndex(): Promise<any> {
return this.elasticsearchService.indices.create({ return this.elasticsearchService.indices.create({
index: this.index, index: this.index,
body: {
settings: {
analysis: {
analyzer: {
default: {
type: 'french',
},
default_search: {
type: 'french',
},
},
},
},
},
}); });
} }
...@@ -54,6 +68,12 @@ export class StructuresSearchService { ...@@ -54,6 +68,12 @@ export class StructuresSearchService {
return structure; return structure;
} }
public async refreshIndexStructure(): Promise<any> {
return this.elasticsearchService.indices.refresh({
index: this.index,
});
}
public async search(searchString: string): Promise<StructureSearchBody[]> { public async search(searchString: string): Promise<StructureSearchBody[]> {
searchString = searchString ? searchString + '*' : '*'; searchString = searchString ? searchString + '*' : '*';
const { body } = await this.elasticsearchService.search<StructureSearchResult>({ const { body } = await this.elasticsearchService.search<StructureSearchResult>({
...@@ -71,14 +91,8 @@ export class StructuresSearchService { ...@@ -71,14 +91,8 @@ export class StructuresSearchService {
}, },
}, },
}); });
const maxScore = Math.max.apply(
Math,
body.hits.hits.map(function (hit) {
return hit._score;
})
);
const sortedHits = body.hits.hits.filter(function (elem) { const sortedHits = body.hits.hits.filter(function (elem) {
return elem._score >= maxScore / 1.5; return elem._score >= body.hits.max_score / 1.5;
}); });
return sortedHits.map((item) => item._source); return sortedHits.map((item) => item._source);
} }
......
export class StructuresForSearchServiceMock {
findAll() {
return [
{
_id: '607ef197225ffd001391edb9',
structureName: "Médiathèque d'Ecully",
structureType: 'mediatheque',
address: {
numero: '1',
street: 'Avenue Edouard Aynard',
commune: 'Écully',
},
description:
'Nous sommes une équipe de 6 personnes accompagnant les usagers dans leur démarche de découverte des outils numériques, mettant à disposition sous forme de prêt des liseuses et du livre numérique et organisant des ateliers individuels de prise en main des outils numériques et tentant de répondre aux questions des usages sur des sujets divers.',
},
{
_id: '60368194cda3ba42b8e621dd',
structureName: 'Maison des associations (Grézieu-la-Varenne)',
structureType: 'autre',
address: {
numero: null,
street: " Place de l'Abbe Launay",
commune: 'Grézieu-la-Varenne',
},
description: null,
},
{
_id: '60b4b0836a9d4500313b8661',
structureName: 'Mairie (La Tour de Salvagny)',
structureType: 'mairie',
address: {
numero: null,
street: 'Place de la Mairie',
commune: 'La Tour-de-Salvagny',
},
description: null,
},
{
_id: '604b84e914d486001790ee57',
structureName: 'Maison de la Métropole (Ecully)',
structureType: 'mdm',
address: {
numero: '10',
street: 'Chemin Jean-Marie Vianney',
commune: 'Écully',
},
description: null,
},
{
_id: '61977124eb90f20031137c35',
structureName: 'Maison de la Métropole (Oullins)',
structureType: 'mdm',
address: {
numero: '17',
street: 'Rue Tupin',
commune: 'Oullins',
},
description: null,
},
];
}
}