diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index bb71762235a1187989c66f732efe2aa222b128a2..8396a9ed144b0d4185a8bfd0dd085c5ceac7eade 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -46,6 +46,7 @@ test: - export GHOST_HOST_AND_PORT=http://localhost:2368 - export GHOST_ADMIN_API_KEY=60142bc9e33940000156bccc:6217742e2671e322612e89cac9bab61fcd01822709fe5d8f5e6a5b3e54d5e6bb - export SALT=$TEST_SALT + - export ELASTICSEARCH_NODE=http://localhost:9200 script: - npm i - npm run test:cov diff --git a/docker-compose.yml b/docker-compose.yml index e95e22ec421be34eadca84b2076cc2b029fd0cb0..2f38db773589f3cc656c801e7054a97b65b43286 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -67,6 +67,43 @@ services: volumes: - db-ghost + es01: + image: elasticsearch:7.6.1 + restart: unless-stopped + environment: + node.name: es01 + cluster.name: es-docker-cluster + discovery.type: single-node + xpack.security.enabled: ${ELASTIC_SECURITY} + ELASTIC_PASSWORD: ${ELASTICSEARCH_PASSWORD} + volumes: + - db-elastic + networks: + - elastic + ports: + - ${ELASTICSEARCH_PORT}:9200 + + kib01: + image: docker.elastic.co/kibana/kibana:7.6.1 + restart: unless-stopped + container_name: kib01 + ports: + - ${KIBANA_PORT}:5601 + environment: + ELASTICSEARCH_URL: http://es01:9200 + ELASTICSEARCH_HOSTS: '["http://es01:9200"]' + ELASTICSEARCH_USERNAME: elastic + ELASTICSEARCH_PASSWORD: ${ELASTICSEARCH_PASSWORD} + depends_on: + - es01 + networks: + - elastic + volumes: db-ram: db-ghost: + db-elastic: + +networks: + elastic: + driver: bridge diff --git a/package-lock.json b/package-lock.json index d787d4c561026f1861332126e9218bc649e0b94c..ec29f08e932c0f4fe96f84d2de0a17c7a942e318 100644 --- a/package-lock.json +++ b/package-lock.json @@ -636,6 +636,40 @@ "minimist": "^1.2.0" } }, + "@elastic/elasticsearch": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/@elastic/elasticsearch/-/elasticsearch-7.12.0.tgz", + "integrity": "sha512-GquUEytCijFRPEk3DKkkDdyhspB3qbucVQOwih9uNyz3iz804I+nGBUsFo2LwVvLQmQfEM0IY2+yoYfEz5wMug==", + "requires": { + "debug": "^4.3.1", + "hpagent": "^0.1.1", + "ms": "^2.1.3", + "pump": "^3.0.0", + "secure-json-parse": "^2.3.1" + }, + "dependencies": { + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "requires": { + "ms": "2.1.2" + }, + "dependencies": { + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + } + } + }, "@eslint/eslintrc": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.2.1.tgz", @@ -1356,6 +1390,26 @@ } } }, + "@nestjs/config": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-0.6.3.tgz", + "integrity": "sha512-JxvvUpmH0/WOrTB+zh8dEkxSUQXhB7V3d/qeQXyCnMiEFjaq89+fNFztpWjz4DlOfdS4/eYTzIEy9PH2uGnfzA==", + "requires": { + "dotenv": "8.2.0", + "dotenv-expand": "5.1.0", + "lodash.get": "4.4.2", + "lodash.has": "4.5.2", + "lodash.set": "4.3.2", + "uuid": "8.3.2" + }, + "dependencies": { + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" + } + } + }, "@nestjs/core": { "version": "7.5.1", "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-7.5.1.tgz", @@ -1370,6 +1424,11 @@ "uuid": "8.3.1" } }, + "@nestjs/elasticsearch": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@nestjs/elasticsearch/-/elasticsearch-7.1.0.tgz", + "integrity": "sha512-3ixmu9MkTh0DS+LKAKcWHLyf/1DPQTXoy+aVClVI14DJQU208oHR3V0e9klApC+GXCYW+BDhNReh4HRyekjTrw==" + }, "@nestjs/jwt": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-7.2.0.tgz", @@ -4921,6 +4980,11 @@ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz", "integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==" }, + "dotenv-expand": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz", + "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==" + }, "dotgitignore": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/dotgitignore/-/dotgitignore-2.1.0.tgz", @@ -5060,7 +5124,6 @@ "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dev": true, "requires": { "once": "^1.4.0" } @@ -6941,6 +7004,11 @@ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==" }, + "hpagent": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/hpagent/-/hpagent-0.1.1.tgz", + "integrity": "sha512-IxJWQiY0vmEjetHdoE9HZjD4Cx+mYTr25tR7JCxXaiI3QxW0YqYyM11KyZbHufoa/piWhMb2+D3FGpMgmA2cFQ==" + }, "html-encoding-sniffer": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz", @@ -8183,6 +8251,7 @@ "version": "12.0.0", "resolved": "https://registry.npmjs.org/jest-junit/-/jest-junit-12.0.0.tgz", "integrity": "sha512-+8K35LlboWiPuCnXSyiid7rFdxNlpCWWM20WEYe6IZH6psfUWKZmSpSRQ5tk0C0cBeDsvsnIzcef5mYhyJsbug==", + "dev": true, "requires": { "mkdirp": "^1.0.4", "strip-ansi": "^5.2.0", @@ -8193,17 +8262,20 @@ "ansi-regex": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true }, "mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true }, "strip-ansi": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, "requires": { "ansi-regex": "^4.1.0" } @@ -8211,7 +8283,8 @@ "uuid": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "dev": true } } }, @@ -9288,6 +9361,16 @@ "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", "integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=" }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=" + }, + "lodash.has": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/lodash.has/-/lodash.has-4.5.2.tgz", + "integrity": "sha1-0Z9NwQlQWMzL4rDN9O4P5Ko3yGI=" + }, "lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -9334,6 +9417,11 @@ "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" }, + "lodash.set": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz", + "integrity": "sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM=" + }, "lodash.sortby": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", @@ -11158,7 +11246,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, "requires": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -11806,6 +11893,11 @@ "ajv-keywords": "^3.4.1" } }, + "secure-json-parse": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.4.0.tgz", + "integrity": "sha512-Q5Z/97nbON5t/L/sH6mY2EacfjVGwrCcSi5D3btRO2GZ8pf1K1UN7Z9H5J57hjVU2Qzxr1xO+FmBhOvEkzCMmg==" + }, "semver": { "version": "7.3.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", @@ -14176,7 +14268,8 @@ "xml": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", - "integrity": "sha1-eLpyAgApxbyHuKgaPPzXS0ovweU=" + "integrity": "sha1-eLpyAgApxbyHuKgaPPzXS0ovweU=", + "dev": true }, "xml-name-validator": { "version": "3.0.0", diff --git a/package.json b/package.json index 79114f2ed061d6b141ac421bafe63a3df12d5d55..2f017979e39aebaecae276d4a32e1b44161f660c 100644 --- a/package.json +++ b/package.json @@ -25,8 +25,11 @@ "migrate:down": "migrate --migrations-dir=\"./src/migrations/scripts\" --compiler=\"ts:./src/migrations/migrations-utils/ts-compiler.js\" down" }, "dependencies": { + "@elastic/elasticsearch": "^7.12.0", "@nestjs/common": "^7.6.13", + "@nestjs/config": "^0.6.3", "@nestjs/core": "^7.5.1", + "@nestjs/elasticsearch": "^7.1.0", "@nestjs/jwt": "^7.2.0", "@nestjs/mongoose": "^7.1.0", "@nestjs/passport": "^7.1.5", diff --git a/scripts/init-ghost.js b/scripts/init-ghost.js index 350b0376602770cb6b9112cdb8afde728f6a13fe..345ed027d3792a0966ff63452cb05fddeb406fe1 100644 --- a/scripts/init-ghost.js +++ b/scripts/init-ghost.js @@ -81,8 +81,9 @@ function processImagesInHTML(html) { // Find images that Ghost Upload supports let imageRegex = /="([^"]*?(?:\.jpg|\.jpeg|\.gif|\.png|\.svg|\.sgvz))"/gim; let imagePromises = []; + let result; - while ((const result = imageRegex.exec(html)) !== null) { + while ((result = imageRegex.exec(html)) !== null) { let file = result[1]; // Upload the image, using the original matched filename as a reference imagePromises.push( diff --git a/src/admin/admin.controller.spec.ts b/src/admin/admin.controller.spec.ts index 153f13440355cc1802e1aadd5d47d174e8fd5866..dc0bdef307a30973ed3864fa428e46aca34c8dea 100644 --- a/src/admin/admin.controller.spec.ts +++ b/src/admin/admin.controller.spec.ts @@ -5,8 +5,10 @@ import { ConfigurationModule } from '../configuration/configuration.module'; import { MailerService } from '../mailer/mailer.service'; import { NewsletterSubscription } from '../newsletter/newsletter-subscription.schema'; import { NewsletterService } from '../newsletter/newsletter.service'; +import { SearchModule } from '../search/search.module'; import { Structure } from '../structures/schemas/structure.schema'; import { StructuresService } from '../structures/services/structures.service'; +import { StructuresSearchService } from '../structures/services/structures-search.service'; import { User } from '../users/schemas/user.schema'; import { UsersService } from '../users/users.service'; import { AdminController } from './admin.controller'; @@ -17,10 +19,11 @@ describe('AdminController', () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - imports: [ConfigurationModule, HttpModule], + imports: [ConfigurationModule, HttpModule, SearchModule], providers: [ UsersService, StructuresService, + StructuresSearchService, NewsletterService, MailerService, { diff --git a/src/search/search.module.ts b/src/search/search.module.ts new file mode 100644 index 0000000000000000000000000000000000000000..8e9b5e85b2226d75c157886fe06eabf64c265d7b --- /dev/null +++ b/src/search/search.module.ts @@ -0,0 +1,22 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { ElasticsearchModule } from '@nestjs/elasticsearch'; + +@Module({ + imports: [ + ConfigModule, + ElasticsearchModule.registerAsync({ + imports: [ConfigModule], + useFactory: async (configService: ConfigService) => ({ + node: configService.get('ELASTICSEARCH_NODE'), + auth: { + username: configService.get('ELASTICSEARCH_USERNAME'), + password: configService.get('ELASTICSEARCH_PASSWORD'), + }, + }), + inject: [ConfigService], + }), + ], + exports: [ElasticsearchModule], +}) +export class SearchModule {} diff --git a/src/structures/interfaces/structure-search-body.interface.ts b/src/structures/interfaces/structure-search-body.interface.ts new file mode 100644 index 0000000000000000000000000000000000000000..24d7591930d82643cd181ced7132665d8d2007d9 --- /dev/null +++ b/src/structures/interfaces/structure-search-body.interface.ts @@ -0,0 +1,8 @@ +import { Address } from '../schemas/address.schema'; + +export interface StructureSearchBody { + structureId: string; + structureName: string; + structureType: string; + address: Address; +} diff --git a/src/structures/interfaces/structure-search-response.interface.ts b/src/structures/interfaces/structure-search-response.interface.ts new file mode 100644 index 0000000000000000000000000000000000000000..7497343827756c4a16df4e15a861709604a00ce0 --- /dev/null +++ b/src/structures/interfaces/structure-search-response.interface.ts @@ -0,0 +1,10 @@ +import { StructureSearchBody } from './structure-search-body.interface'; + +export interface StructureSearchResult { + hits: { + total: number; + hits: Array<{ + _source: StructureSearchBody; + }>; + }; +} diff --git a/src/structures/services/structures-search.service.ts b/src/structures/services/structures-search.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..f334926a9fce19a953801764b1812f906692557d --- /dev/null +++ b/src/structures/services/structures-search.service.ts @@ -0,0 +1,91 @@ +import { Injectable } from '@nestjs/common'; +import { ElasticsearchService } from '@nestjs/elasticsearch'; +import { structureDto } from '../dto/structure.dto'; +import { StructureDocument } from '../schemas/structure.schema'; +import { StructureSearchBody } from '../interfaces/structure-search-body.interface'; +import { StructureSearchResult } from '../interfaces/structure-search-response.interface'; + +@Injectable() +export class StructuresSearchService { + private index = 'structures'; + + constructor(private readonly elasticsearchService: ElasticsearchService) {} + + public async indexStructure(structure: StructureDocument): Promise<StructureDocument> { + this.elasticsearchService.index<StructureSearchResult, StructureSearchBody>({ + index: this.index, + id: structure._id, + body: { + structureName: structure.structureName, + structureType: structure.structureType, + structureId: structure._id, + address: structure.address, + }, + }); + return structure; + } + + public async createStructureIndex(): Promise<any> { + return await this.elasticsearchService.indices.create({ + index: this.index, + }); + } + + public async dropIndex(): Promise<any> { + if ( + ( + await this.elasticsearchService.indices.exists({ + index: this.index, + }) + ).body + ) { + return this.elasticsearchService.indices.delete({ + index: this.index, + }); + } + } + + public async deleteIndexStructure(structure: StructureDocument): Promise<StructureDocument> { + this.elasticsearchService.delete<StructureSearchResult, StructureSearchBody>({ + index: this.index, + id: structure._id, + }); + return structure; + } + + public async search(searchString: string): Promise<StructureSearchBody[]> { + searchString = searchString ? searchString + '*' : '*'; + const { body } = await this.elasticsearchService.search<StructureSearchResult>({ + index: this.index, + body: { + from: 0, + size: 30, + query: { + query_string: { + analyze_wildcard: 'true', + query: searchString, + fields: ['structureName^5', 'structureType^5', 'address.street', 'address.commune^5'], + fuzziness: 'AUTO', + }, + }, + }, + }); + const hits = body.hits.hits; + return hits.map((item) => item._source); + } + + public async update(structure: structureDto, id: string): Promise<any> { + return this.elasticsearchService.update({ + index: this.index, + id: id, + body: { + doc: { + structureName: structure.structureName, + structureType: structure.structureType, + structureId: id, + address: structure.address, + }, + }, + }); + } +} diff --git a/src/structures/services/structures.service.ts b/src/structures/services/structures.service.ts index c6c02b27a8c0c0ba0fd1ccc298a288bc98263c0c..8b98603ab518083513dd8553994e794e897592a0 100644 --- a/src/structures/services/structures.service.ts +++ b/src/structures/services/structures.service.ts @@ -14,6 +14,7 @@ import { DateTime } from 'luxon'; import { IUser } from '../../users/interfaces/user.interface'; import * as _ from 'lodash'; import { OwnerDto } from '../../users/dto/owner.dto'; +import { StructuresSearchService } from './structures-search.service'; import { CategoriesFormationsModule } from '../../categories/schemas/categoriesFormationsModule.schema'; import { CategoriesModule } from '../../categories/categories.module'; import { CategoriesAccompagnement } from '../../categories/schemas/categoriesAccompagnement.schema'; @@ -26,9 +27,43 @@ export class StructuresService { private readonly httpService: HttpService, private readonly userService: UsersService, private readonly mailerService: MailerService, + private structuresSearchService: StructuresSearchService, @InjectModel(Structure.name) private structureModel: Model<StructureDocument> ) {} + async initiateStructureIndex(): Promise<StructureDocument[]> { + await this.structuresSearchService.dropIndex(); + await this.structuresSearchService.createStructureIndex(); + return this.populateES(); + } + + async searchForStructures(text: string, filters?: Array<any>): Promise<StructureDocument[]> { + const results = await this.structuresSearchService.search(text); + const ids = results.map((result) => result.structureId); + if (!ids.length) { + return []; + } + //we match ids from Elasticsearch with ids from mongoDB (and filters) and sort the result according to ElasticSearch order. + if (filters.length > 0) { + return ( + await this.structureModel + .find({ + _id: { $in: ids }, + $and: [...this.parseFilter(filters), { deletedAt: { $exists: false }, accountVerified: true }], + }) + .exec() + ).sort((a, b) => ids.indexOf(a.id) - ids.indexOf(b.id)); + } else { + return ( + await this.structureModel + .find({ + _id: { $in: ids }, + }) + .exec() + ).sort((a, b) => ids.indexOf(a.id) - ids.indexOf(b.id)); + } + } + public async create(idUser: string, structure: structureDto): Promise<Structure> { const user = await this.userService.findOne(idUser); if (!user) { @@ -37,10 +72,15 @@ export class StructuresService { const createdStructure = new this.structureModel(structure); createdStructure._id = Types.ObjectId(); await createdStructure.save(); - await this.getStructurePosition(createdStructure).then((postition: StructureDocument) => { - return this.structureModel - .findByIdAndUpdate(Types.ObjectId(createdStructure._id), { address: postition.address, coord: postition.coord }) - .exec(); + await this.getStructurePosition(createdStructure).then(async (postition: StructureDocument) => { + return this.structuresSearchService.indexStructure( + await this.structureModel + .findByIdAndUpdate(Types.ObjectId(createdStructure._id), { + address: postition.address, + coord: postition.coord, + }) + .exec() + ); }); user.structuresLink.push(createdStructure._id); user.save(); @@ -111,6 +151,16 @@ export class StructuresService { return this.structureModel.find({ deletedAt: { $exists: false }, accountVerified: true }).exec(); } + public async populateES(): Promise<StructureDocument[]> { + const structures = await this.structureModel.find({ deletedAt: { $exists: false } }).exec(); + await Promise.all( + structures.map((structure: StructureDocument) => { + this.structuresSearchService.indexStructure(structure); + }) + ); + return structures; + } + public async findAllFormated( formationCategories: CategoriesFormations[], accompagnementCategories: CategoriesAccompagnement[], @@ -213,6 +263,7 @@ export class StructuresService { if (!result) { throw new HttpException('Invalid structure id', HttpStatus.BAD_REQUEST); } else { + await this.structuresSearchService.update(structure, idStructure); this.sendAdminStructureNotification( result, this.mailerService.config.templates.structureModificationNotification.ejs, @@ -335,6 +386,7 @@ export class StructuresService { if (!structure) { throw new HttpException('Invalid structure id', HttpStatus.BAD_REQUEST); } + this.structuresSearchService.deleteIndexStructure(structure); structure.structureType = null; structure.deletedAt = DateTime.local().setZone('Europe/Paris').toString(); this.anonymizeStructure(structure).save(); diff --git a/src/structures/structures.controller.ts b/src/structures/structures.controller.ts index 9c6283a24c247d06648ad25c9cb883cf8189b84c..3121e59c81b2a83e1b6caf36e6df3ca36ffb9401 100644 --- a/src/structures/structures.controller.ts +++ b/src/structures/structures.controller.ts @@ -27,8 +27,9 @@ import { UsersService } from '../users/users.service'; import { CreateStructureDto } from './dto/create-structure.dto'; import { QueryStructure } from './dto/query-structure.dto'; import { structureDto } from './dto/structure.dto'; -import { Structure } from './schemas/structure.schema'; +import { Structure, StructureDocument } from './schemas/structure.schema'; import { StructuresService } from './services/structures.service'; +import { StructureSearchBody } from './interfaces/structure-search-body.interface'; @Controller('structures') export class StructuresController { @@ -65,7 +66,12 @@ export class StructuresController { @Post('search') public async search(@Query() query: QueryStructure, @Body() body): Promise<Structure[]> { - return this.structureService.search(query.query, body ? body.filters : null); + return await this.structureService.searchForStructures(query.query, body ? body.filters : null); + } + + @Post('resetSearchIndex') + public async resetES(): Promise<StructureDocument[]> { + return this.structureService.initiateStructureIndex(); } @Put('updateAfterOwnerVerify/:id') diff --git a/src/structures/structures.module.ts b/src/structures/structures.module.ts index 1445999d289ff826e1504e3754d814f10ed06322..650e3adfd9b7ef57e13d8e06090f1b9100f26e33 100644 --- a/src/structures/structures.module.ts +++ b/src/structures/structures.module.ts @@ -11,6 +11,8 @@ import { StructureTypeController } from './structure-type/structure-type.control import { StructureTypeService } from './structure-type/structure-type.service'; import { StructureType, StructureTypeSchema } from './structure-type/structure-type.schema'; import { CategoriesModule } from '../categories/categories.module'; +import { StructuresSearchService } from './services/structures-search.service'; +import { SearchModule } from '../search/search.module'; @Module({ imports: [ @@ -23,9 +25,10 @@ import { CategoriesModule } from '../categories/categories.module'; forwardRef(() => UsersModule), CategoriesModule, TempUserModule, + SearchModule, ], controllers: [StructuresController, StructureTypeController], exports: [StructuresService, StructureTypeService], - providers: [StructuresService, StructureTypeService, ApticStructuresService], + providers: [StructuresSearchService, StructuresService, StructureTypeService, ApticStructuresService], }) export class StructuresModule {} diff --git a/src/users/users.controller.spec.ts b/src/users/users.controller.spec.ts index e9c8b75b5ae7a6a0e434e1faca9ef95c71bbefb5..68da5450f95300fde9828f6a2ad2d1905b738753 100644 --- a/src/users/users.controller.spec.ts +++ b/src/users/users.controller.spec.ts @@ -4,8 +4,10 @@ import { Test, TestingModule } from '@nestjs/testing'; import { CategoriesModule } from '../categories/categories.module'; import { ConfigurationModule } from '../configuration/configuration.module'; import { MailerService } from '../mailer/mailer.service'; +import { SearchModule } from '../search/search.module'; import { Structure } from '../structures/schemas/structure.schema'; import { StructuresService } from '../structures/services/structures.service'; +import { StructuresSearchService } from '../structures/services/structures-search.service'; import { TempUser } from '../temp-user/temp-user.schema'; import { TempUserService } from '../temp-user/temp-user.service'; import { User } from './schemas/user.schema'; @@ -17,10 +19,11 @@ describe('UsersController', () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - imports: [ConfigurationModule, HttpModule], + imports: [ConfigurationModule, HttpModule, SearchModule], providers: [ UsersService, StructuresService, + StructuresSearchService, MailerService, TempUserService, { diff --git a/template.env b/template.env index 41cb576d15639e668ff90bbdadc2e508f70a9e03..027d8b6a62352ab2d9b95ae9c6d337d9e19d13c6 100644 --- a/template.env +++ b/template.env @@ -20,3 +20,10 @@ GHOST_CONTENT_API_KEY=<Ghost global api key, can be found in integration part of GHOST_ADMIN_API_KEY=<Ghost admin api key, can be found in integration part of ghost UI> GHOST_HOST_AND_PORT=<Ghost host and port, ex:http://localhost:2368> USER_PWD=<test user password, this password will be user by every test users> +ELASTICSEARCH_NODE=<elastic search container node> +ELASTICSEARCH_PATH=<elastic search container path> +ELASTICSEARCH_PORT=<elastic search port> +ELASTICSEARCH_USERNAME=<elastic search username> +ELASTICSEARCH_PASSWORD=<elastic search password> +ELASTIC_SECURITY=<elastic search security boolean (true = secure)> +KIBANA_PORT=<kibana port> \ No newline at end of file