diff --git a/docker-compose.yml b/docker-compose.yml index 5a6a8358..1f553ef9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -91,7 +91,9 @@ services: container_name: questiongeneratorservice-${teamname:-defaultASW} image: ghcr.io/arquisoft/wiq_es6c/questiongeneratorservice:latest profiles: ["dev", "prod"] - build: ./questionsservice/questiongeneratorservice + build: + context: ./questionsservice + dockerfile: ./questiongeneratorservice/Dockerfile depends_on: - mongodb_wiki - storequestionservice @@ -100,6 +102,7 @@ services: networks: - mynetwork environment: + DATAMODELS_URI: './questiondata-model' MONGODB_URI: mongodb://mongodb_wiki:27017/questions STORE_QUESTION_SERVICE_URL: http://storequestionservice:8004 restart: always @@ -108,7 +111,9 @@ services: container_name: wikidataextractorservice-${teamname:-defaultASW} image: ghcr.io/arquisoft/wiq_es6c/wikidataextractorservice:latest profiles: ["dev", "prod"] - build: ./questionsservice/wikidataExtractor + build: + context: ./questionsservice + dockerfile: ./wikidataExtractor/Dockerfile depends_on: - mongodb_wiki ports: @@ -116,6 +121,7 @@ services: networks: - mynetwork environment: + DATAMODELS_URI: './questiondata-model' MONGODB_URI: mongodb://mongodb_wiki:27017/questions restart: always diff --git a/questionsservice/questiondata-model.js b/questionsservice/questiondata-model.js new file mode 100644 index 00000000..7d1d8351 --- /dev/null +++ b/questionsservice/questiondata-model.js @@ -0,0 +1,82 @@ +const mongoose = require('mongoose'); + +const paisSchema = new mongoose.Schema({ + pais: { + type: String, + required: true + }, + capital: { + type: String, + required: false + }, + continente: { + type: String, + required: false + }, + lenguaje: { + type: String, + required: false + }, + bandera: { + type: String, + required: false + } +}, { timestamps: {} }); // Añade y gestiona automáticamente los campos createdAt y updatedAt + +const monumentSchema = new mongoose.Schema({ + monumento: { + type: String, + required: true + }, + pais: { + type: String, + required: false + } +}, {timestamps: {}}); + +const chemicalElementsSchema = new mongoose.Schema({ + elemento: { + type: String, + required: true + }, + simbolo: { + type: String, + required: false + } +}, {timestamps: {}}); + +const filmSchema = new mongoose.Schema({ + pelicula: { + type: String, + required: true + }, + director: { + type: String, + required: false + } +}, {timestamps: {}}); + +const songSchema = new mongoose.Schema({ + cancion: { + type: String, + required: true + }, + artista: { + type: String, + required: false + } +}, {timestamps: {}}); + +const Pais = mongoose.model('Pais', paisSchema); +const Monumento = mongoose.model('Monumento', monumentSchema); +const Elemento = mongoose.model('Elemento', chemicalElementsSchema); +const Pelicula = mongoose.model('Pelicula', filmSchema); +const Cancion = mongoose.model('Cancion', songSchema); + +module.exports = { + Pais, + Monumento, + Elemento, + Pelicula, + Cancion +}; \ No newline at end of file diff --git a/questionsservice/questiongeneratorservice/Dockerfile b/questionsservice/questiongeneratorservice/Dockerfile index d5f0af0b..1dac423d 100644 --- a/questionsservice/questiongeneratorservice/Dockerfile +++ b/questionsservice/questiongeneratorservice/Dockerfile @@ -5,13 +5,14 @@ FROM node:20 WORKDIR /usr/src/questionsservice/questiongeneratorservice # Copy package.json and package-lock.json to the working directory -COPY package*.json ./ +COPY ./questiongeneratorservice/package*.json ./ # Install app dependencies RUN npm install # Copy the app source code to the working directory -COPY . . +COPY ./questiongeneratorservice/ . +COPY questiondata-model.js . # Expose the port the app runs on EXPOSE 8007 diff --git a/questionsservice/questiongeneratorservice/package-lock.json b/questionsservice/questiongeneratorservice/package-lock.json index 2240421b..5467d7fe 100644 --- a/questionsservice/questiongeneratorservice/package-lock.json +++ b/questionsservice/questiongeneratorservice/package-lock.json @@ -106,12 +106,12 @@ } }, "node_modules/body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", "dependencies": { "bytes": "3.1.2", - "content-type": "~1.0.4", + "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", @@ -119,7 +119,7 @@ "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.11.0", - "raw-body": "2.5.1", + "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" }, @@ -245,9 +245,9 @@ } }, "node_modules/cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", "engines": { "node": ">= 0.6" } @@ -364,16 +364,16 @@ } }, "node_modules/express": { - "version": "4.18.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", - "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.1", + "body-parser": "1.20.2", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.5.0", + "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", @@ -434,9 +434,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", - "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", "funding": [ { "type": "individual", @@ -587,9 +587,9 @@ } }, "node_modules/hasown": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.1.tgz", - "integrity": "sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dependencies": { "function-bind": "^1.1.2" }, @@ -1086,9 +1086,9 @@ } }, "node_modules/raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", @@ -1193,16 +1193,16 @@ } }, "node_modules/set-function-length": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.1.tgz", - "integrity": "sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", "dependencies": { - "define-data-property": "^1.1.2", + "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.3", + "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.1" + "has-property-descriptors": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -1214,11 +1214,11 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" }, "node_modules/side-channel": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.5.tgz", - "integrity": "sha512-QcgiIWV4WV7qWExbN5llt6frQB/lBven9pqliLXfGPB+K9ZYXxDozp0wLkHS24kWCm+6YXH/f0HhnObZnZOBnQ==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", "dependencies": { - "call-bind": "^1.0.6", + "call-bind": "^1.0.7", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.4", "object-inspect": "^1.13.1" diff --git a/questionsservice/questiongeneratorservice/questiongenerator-model.js b/questionsservice/questiongeneratorservice/questiongenerator-model.js deleted file mode 100644 index c589bf36..00000000 --- a/questionsservice/questiongeneratorservice/questiongenerator-model.js +++ /dev/null @@ -1,27 +0,0 @@ -const mongoose = require('mongoose'); - -const paisSchema = new mongoose.Schema({ - pais: { - type: String, - required: true - }, - capital: { - type: String, - required: false - }, - lenguaje: { - type: String, - required: false - }, - bandera: { - type: String, - required: false - } -}, { timestamps: {} }); // Añade y gestiona automáticamente los campos createdAt y updatedAt - - -const Pais = mongoose.model('Pais', paisSchema); - -module.exports = { - Pais -}; \ No newline at end of file diff --git a/questionsservice/questiongeneratorservice/questiongenerator-service.js b/questionsservice/questiongeneratorservice/questiongenerator-service.js index 5e72f43d..89b68efa 100644 --- a/questionsservice/questiongeneratorservice/questiongenerator-service.js +++ b/questionsservice/questiongeneratorservice/questiongenerator-service.js @@ -63,7 +63,18 @@ app.get('/questions', async (req, res) => { } } catch (error) { console.error(`Bad Request: ${error.message}`); - res.status(400).json({ message: error.message }); + res.status(400).json({ error: error.message }); + } +}); + +// Route for getting topics for questions +app.get('/topics', async (req, res) => { + try { + const topics = QuestionGenerator.getAvailableTopics(); + res.send(topics); + } catch (error) { + console.error(`An error occurred: ${error.message}`); + res.status(500).json({ error: 'Internal Server Error' }); } }); diff --git a/questionsservice/questiongeneratorservice/questiongenerator.js b/questionsservice/questiongeneratorservice/questiongenerator.js index 07005323..6ce42012 100644 --- a/questionsservice/questiongeneratorservice/questiongenerator.js +++ b/questionsservice/questiongeneratorservice/questiongenerator.js @@ -1,34 +1,134 @@ -const { Pais } = require('./questiongenerator-model') +const modelUri = process.env.DATAMODELS_URI || '../questiondata-model'; +const { Pais, Monumento, Elemento, Pelicula, Cancion } = require(modelUri); class QuestionGenerator { static temas = new Map([ - ["paises", [0, 1, 2]], - ['capital', [0, 1]], - ["lenguaje", [2]] + ["Paises", [0, 1, 2, 3, 4, 5]], + ['Capitales', [0, 1]], + ['Continentes', [2, 3]], + ['Monumentos', [4, 5]], + ['Quimica', [6, 7]], + ['Peliculas', [8, 9]], + ['Canciones', [10, 11]] + // ["Lenguajes", []] ]); - ; static plantillas = [ - { + { // 0: Paises, Capitales + modelo: Pais, + generateMethod: (plantilla, respuestas) => this.generateQuestion1to1Relation(plantilla, respuestas), pregunta: (param) => `¿Cuál es la capital de ${param}?`, filtro: { pais: { $exists: true }, capital: { $exists: true } }, campo_pregunta: 'pais', campo_respuesta: 'capital' }, - { + { // 1: Paises, Capitales + modelo: Pais, + generateMethod: (plantilla, respuestas) => this.generateQuestion1to1Relation(plantilla, respuestas), pregunta: (param) => `¿De qué país es capital ${param}?`, filtro: { capital: { $exists: true }, pais: { $exists: true } }, campo_pregunta: 'capital', campo_respuesta: 'pais' }, - { - pregunta: (param) => `¿Qué lengua se habla en ${param}?`, - filtro: { pais: { $exists: true }, lenguaje: { $exists: true } }, + { // 2: Paises, Continentes - Meh, repite mucho los continentes + modelo: Pais, + generateMethod: (plantilla, respuestas) => this.generateQuestionNonDuplicatedAnswers(plantilla, respuestas), + pregunta: (param) => `¿En qué continente se situa ${param}?`, + filtro: { pais: { $exists: true }, continente: { $exists: true } }, + filtro_decoys: (answer) => { return { pais: { $exists: true }, continente: { $exists: true, $ne: answer.continente} }}, campo_pregunta: 'pais', - campo_respuesta: 'lenguaje' + campo_respuesta: 'continente' + }, + { // 3: Paises, Continentes + modelo: Pais, + generateMethod: (plantilla, respuestas) => this.generateQuestionNonDuplicatedAnswers(plantilla, respuestas), + pregunta: (param) => `¿Cual de los siguientes paises se situa en ${param}?`, + filtro: { pais: { $exists: true }, continente: { $exists: true } }, + filtro_decoys: (answer) => { return { pais: { $exists: true }, continente: { $exists: true, $ne: answer.continente} }}, + campo_pregunta: 'continente', + campo_respuesta: 'pais' + }, + { // 4: Paises, Monumentos + modelo: Monumento, + generateMethod: (plantilla, respuestas) => this.generateQuestionNonDuplicatedAnswers(plantilla, respuestas), + pregunta: (param) => `¿En qué país se situa la atracción turística "${param}"?`, + filtro: { pais: { $exists: true }, monumento: { $exists: true } }, + filtro_decoys: (answer) => { return { monumento: { $exists: true }, pais: { $exists: true, $ne: answer.pais} }}, + campo_pregunta: 'monumento', + campo_respuesta: 'pais' + }, + { // 5: Paises, Monumentos + modelo: Monumento, + generateMethod: (plantilla, respuestas) => this.generateQuestionNonDuplicatedAnswers(plantilla, respuestas), + pregunta: (param) => `¿Cuál de las siguientes atraccioones turísticas se encuentra en ${param}?`, + filtro: { pais: { $exists: true }, monumento: { $exists: true } }, + filtro_decoys: (answer) => { return { monumento: { $exists: true }, pais: { $exists: true, $ne: answer.pais} }}, + campo_pregunta: 'pais', + campo_respuesta: 'monumento' + }, + { // 6: Quimica + modelo: Elemento, + generateMethod: (plantilla, respuestas) => this.generateQuestion1to1Relation(plantilla, respuestas), + pregunta: (param) => `¿Cuál es el símbolo químico del ${param}?`, + filtro: { elemento: { $exists: true }, simbolo: { $exists: true } }, + campo_pregunta: 'elemento', + campo_respuesta: 'simbolo' + }, + { // 7: Quimica + modelo: Elemento, + generateMethod: (plantilla, respuestas) => this.generateQuestion1to1Relation(plantilla, respuestas), + pregunta: (param) => `¿Qué elemento químico representa el símbolo "${param}"?`, + filtro: { elemento: { $exists: true }, simbolo: { $exists: true } }, + campo_pregunta: 'simbolo', + campo_respuesta: 'elemento' + }, + { // 8: Peliculas + modelo: Pelicula, + generateMethod: (plantilla, respuestas) => this.generateQuestionNonDuplicatedAnswers(plantilla, respuestas), + pregunta: (param) => `¿Quién fue el director de la película "${param}"?`, + filtro: { pelicula: { $exists: true }, director: { $exists: true } }, + filtro_decoys: (answer) => { return { pelicula: { $exists: true }, director: { $exists: true, $ne: answer.director} }}, + campo_pregunta: 'pelicula', + campo_respuesta: 'director' + }, + { // 9: Peliculas + modelo: Pelicula, + generateMethod: (plantilla, respuestas) => this.generateQuestionNonDuplicatedAnswers(plantilla, respuestas), + pregunta: (param) => `¿Cuál de estas películas ha sido dirigida por "${param}"?`, + filtro: { pelicula: { $exists: true }, director: { $exists: true } }, + filtro_decoys: (answer) => { return { pelicula: { $exists: true }, director: { $exists: true, $ne: answer.director} }}, + campo_pregunta: 'director', + campo_respuesta: 'pelicula' + }, + { // 10: Canciones + modelo: Cancion, + generateMethod: (plantilla, respuestas) => this.generateQuestionNonDuplicatedAnswers(plantilla, respuestas), + pregunta: (param) => `¿Quién canta la canción "${param}"?`, + filtro: { cancion: { $exists: true }, artista: { $exists: true } }, + filtro_decoys: (answer) => { return { cancion: { $exists: true }, artista: { $exists: true, $ne: answer.artista} }}, + campo_pregunta: 'cancion', + campo_respuesta: 'artista' + }, + { // 11: Canciones + modelo: Cancion, + generateMethod: (plantilla, respuestas) => this.generateQuestionNonDuplicatedAnswers(plantilla, respuestas), + pregunta: (param) => `¿Cuál de las siguientes canciones es interpretada por "${param}"?`, + filtro: { cancion: { $exists: true }, artista: { $exists: true } }, + filtro_decoys: (answer) => { return { cancion: { $exists: true }, artista: { $exists: true, $ne: answer.artista} }}, + campo_pregunta: 'artista', + campo_respuesta: 'cancion' } // { + // modelo: Pais, + // generateMethod: (plantilla, respuestas) => this.generateQuestionNonDuplicatedAnswers(plantilla, respuestas), + // pregunta: (param) => `¿Qué lengua se habla en ${param}?`, + // filtro: { pais: { $exists: true }, lenguaje: { $exists: true } }, + // filtro_decoys: (answer) => { return { pais: { $exists: true }, lenguaje: { $exists: true, $ne: answer} }}, + // campo_pregunta: 'pais', + // campo_respuesta: 'lenguaje' + // }, + // { // pregunta: (param) => `¿Cuál es la bandera de ${param}?`, // filtro: { bandera: { $exists: true } }, // campo_pregunta: 'pais', @@ -40,11 +140,39 @@ class QuestionGenerator { return [ ...this.temas.keys() ]; } - static async generateQuestion(plantilla, respuestas) { - console.log("\nPlantilla:"); - console.log(plantilla); - const randomDocs = await Pais.aggregate([ + static async generateQuestionNonDuplicatedAnswers(plantilla, respuestas) { + const randomAnswer = await plantilla.modelo.aggregate([ + { $match: plantilla.filtro }, + { $sample: { size: 1 } } + ]); + if (randomAnswer.length < 1) { + console.error(`Not enought data found to generate a question`); + throw new Error(`Not enought data found to generate a question`); + } + let randomDecoys = []; + if (respuestas > 1){ + randomDecoys = await plantilla.modelo.aggregate([ + { $match: plantilla.filtro_decoys(randomAnswer[0]) }, + { $sample: { size: respuestas-1 } } + ]); + } + if (randomDecoys.length < respuestas-1) { + console.error(`Not enought data found to generate a question`); + throw new Error(`Not enought data found to generate a question`); + } + + const retQuestion = { + pregunta: plantilla.pregunta(randomAnswer[0][plantilla.campo_pregunta]), + respuesta_correcta: randomAnswer[0][plantilla.campo_respuesta], + respuestas_incorrectas: Array.from({ length: respuestas-1 }, (_, i) => randomDecoys[i][plantilla.campo_respuesta]) + }; + return retQuestion; + } + + static async generateQuestion1to1Relation(plantilla, respuestas) { + + const randomDocs = await plantilla.modelo.aggregate([ { $match: plantilla.filtro }, { $sample: { size: respuestas } } ]); @@ -53,54 +181,51 @@ class QuestionGenerator { throw new Error(`Not enought data found to generate a question`); } - console.log("\nFind:"); - console.log(randomDocs); - - var retQuestion = { + const retQuestion = { pregunta: plantilla.pregunta(randomDocs[0][plantilla.campo_pregunta]), respuesta_correcta: randomDocs[0][plantilla.campo_respuesta], respuestas_incorrectas: Array.from({ length: respuestas-1 }, (_, i) => randomDocs[i+1][plantilla.campo_respuesta]) }; - console.log("\nPregunta generada:"); - console.log(retQuestion); - return retQuestion; } static async generateQuestions(preguntas, respuestas, temas) { - console.log(temas); const plantillasDisponibles = this.getAvailableTemplates(temas); - console.log(plantillasDisponibles); - var retQuestions = []; + let retQuestions = []; for (let i = 0; i < preguntas; i++) { let index = Math.floor(Math.random() * plantillasDisponibles.length); - retQuestions.push(await this.generateQuestion(this.plantillas[plantillasDisponibles[index]], respuestas)); + let plantilla = this.plantillas[plantillasDisponibles[index]]; + retQuestions.push(await plantilla.generateMethod(plantilla, respuestas)); } + console.log("\nPreguntas generadas:"); + console.log(retQuestions); return retQuestions; } static getAvailableTemplates(temas) { if (temas.length == 0) { - return Array.from({ length: this.plantillas.length }, (_, i) => i); + let templates = Array.from({ length: this.plantillas.length }, (_, i) => i); + console.log("Temas a utilizar:\n\tTodos\nPlantillas a utilizar:"); + console.log(`\t${templates}`); + return templates; } - var templates = []; + let templates = []; + console.log("Temas a utilizar:"); temas.forEach(tema => { - console.log(tema); if (this.temas.has(tema)) { templates = templates.concat(this.temas.get(tema)); - console.log(this.temas.get(tema)); + console.log(`\t${tema}`); } else { - console.error(`The topic \'${tema}\' is not currently defined`); - throw new Error(`The topic \'${tema}\' is not currently defined`); + console.error(`\tThe topic '${tema}' is not currently defined`); } }); if (templates.length == 0) { console.error(`No correct topics were passed`); throw new Error(`No correct topics were passed`); } - console.log(templates); - console.log([...new Set(templates)]); + console.log("Plantillas a utilizar:"); + console.log(`\t${[...new Set(templates)]}`); return [...new Set(templates)]; } diff --git a/questionsservice/wikidataExtractor/Dockerfile b/questionsservice/wikidataExtractor/Dockerfile index 0fa41b7a..0be0f3ef 100644 --- a/questionsservice/wikidataExtractor/Dockerfile +++ b/questionsservice/wikidataExtractor/Dockerfile @@ -5,13 +5,14 @@ FROM node:20 WORKDIR /usr/src/questionsservice/wikidataExtractor # Copy package.json and package-lock.json to the working directory -COPY package*.json ./ +COPY ./wikidataExtractor/package*.json ./ # Install app dependencies RUN npm install # Copy the app source code to the working directory -COPY . . +COPY ./wikidataExtractor/ . +COPY questiondata-model.js . # Expose the port the app runs on EXPOSE 8008 diff --git a/questionsservice/wikidataExtractor/wikidataQueries.js b/questionsservice/wikidataExtractor/wikidataQueries.js index 9d54d8fc..18782279 100644 --- a/questionsservice/wikidataExtractor/wikidataQueries.js +++ b/questionsservice/wikidataExtractor/wikidataQueries.js @@ -2,7 +2,35 @@ const wikidata = require("./wikidataConnexion"); class WikiQueries { - static async obtenerPaisYCapital() { + static regExp = /^Q\d+$/; // Expresión regular para filtrar las etiquetas del tipo "Q1234" + + /* CIENCIA */ + + static async obtenerSimboloQuimico() { // En uso + console.log("Símbolos químicos"); + const query = ` + SELECT ?elementLabel ?symbol WHERE { + ?element wdt:P31 wd:Q11344. + ?element wdt:P246 ?symbol. + SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE],es". } + } + `; + + const results = await wikidata.consulta(query); + // console.log(results) + return results.filter(function(element) { + const elementOk = !WikiQueries.regExp.test(element.elementLabel); + const symbolOk = !WikiQueries.regExp.test(element.symbol); + return elementOk && symbolOk; + }); + + } + + + /* GEOGRAFÍA */ + + static async obtenerPaisYCapital() { // En uso + console.log("Países y Capitales"); const query = ` SELECT ?countryLabel ?capitalLabel WHERE { ?country wdt:P31 wd:Q6256. @@ -11,27 +39,107 @@ class WikiQueries { } `; + const results = await wikidata.consulta(query); + // console.log(results) + return results.filter(function(element) { + const countryOk = !WikiQueries.regExp.test(element.countryLabel); + const capitalOk = !WikiQueries.regExp.test(element.capitalLabel); + return countryOk && capitalOk; + }); + } + + static async obtenerPaisYContinente() { // En uso + console.log("Países y Continentes"); + const query = ` + SELECT ?countryLabel ?continentLabel WHERE { + ?country wdt:P31 wd:Q6256. + ?country wdt:P30 ?continent. + SERVICE wikibase:label { bd:serviceParam wikibase:language "es". } + } + `; + + const results = await wikidata.consulta(query); + // console.log(results) + return results.filter(function(element) { + const countryOk = !WikiQueries.regExp.test(element.countryLabel); + const continentOk = !WikiQueries.regExp.test(element.continentLabel); + return countryOk && continentOk; + }); + } + + static async obtenerPaisYBandera() { + const query = ` + SELECT ?flag ?flagLabel ?countryLabel WHERE { + ?country wdt:P31 wd:Q6256; + wdt:P41 ?flag. + SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE],es". } + } + LIMIT 200 + `; + const results = await wikidata.consulta(query); // console.log(results) return results; + } - static async obtenerPeliculasAñosYDirector() { + static async obtenerPaisYLenguaje() { const query = ` - SELECT ?peliculaLabel ?directorLabel ?fecha - WHERE { + SELECT ?countryLabel ?languageLabel WHERE { + ?country wdt:P31 wd:Q6256. + ?country wdt:P37 ?language. + SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE],es". } + } + LIMIT 500 + `; + + const results = await wikidata.consulta(query); + // console.log(results) + return results; + + } + + static async obtenerMonumentoYPais(){ // En uso + console.log("Países y Monumentos"); + const query = ` + SELECT ?monumentLabel ?countryLabel WHERE { + ?monument wdt:P31 wd:Q570116; wdt:P17 ?country. + SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE],es". } + } + LIMIT 2500 + `; + + const results = await wikidata.consulta(query); + // console.log(results) + return results.filter(function(element) { + const monumentOk = !WikiQueries.regExp.test(element.monumentLabel); + const countryOk = !WikiQueries.regExp.test(element.countryLabel); + return countryOk && monumentOk; + }); + + } + + + /* ENTRETENIMIENTO */ + + static async obtenerPeliculaYDirector() { // En uso + console.log("Películas y Directores"); + const query = ` + SELECT ?peliculaLabel ?directorLabel WHERE { ?pelicula wdt:P31 wd:Q11424. # Filtramos por instancias de películas - ?pelicula wdt:P577 ?fecha. # Obtenemos la fecha de publicación ?pelicula wdt:P57 ?director. # Obtenemos el director de la película - FILTER (YEAR(?fecha) > 2000). # Filtramos por películas posteriores al 2000 SERVICE wikibase:label { bd:serviceParam wikibase:language "es". } - } - LIMIT 2000 - `; + } + LIMIT 2500 + `; const results = await wikidata.consulta(query); // console.log(results) - return results; + return results.filter(function(element) { + const peliculaOk = !WikiQueries.regExp.test(element.peliculaLabel); + const directorOk = !WikiQueries.regExp.test(element.directorLabel); + return peliculaOk && directorOk; + }); } static async obtenerMangaYFecha() { @@ -51,6 +159,75 @@ class WikiQueries { return results; } + static async obtenerCancionYArtista() { // En uso + console.log("Canciones y Artistas"); + const query = ` + SELECT DISTINCT ?songLabel ?artistLabel WHERE { + ?song wdt:P31 wd:Q7366; # Instances of songs + wdt:P175 ?artist. # With property "performer" (artist) + + ?song wdt:P136 ?genre. # Filter by genre + VALUES ?genre { wd:Q202930 wd:Q188450 wd:Q11401 wd:Q20502 wd:Q58339 wd:Q211756 wd:Q474027 wd:Q484641 wd:Q547137 } # Specify genres + + OPTIONAL { ?song wdt:P175 ?secondArtist FILTER (?artist != ?secondArtist) } # Optional second performer + FILTER(!bound(?secondArtist)) # Filter out songs with a second performer + + SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE],es". } + } + `; + + const results = await wikidata.consulta(query); + // console.log(results) + return results.filter(function(element) { + const songOk = !WikiQueries.regExp.test(element.songLabel); + const artistOk = !WikiQueries.regExp.test(element.artistLabel); + return songOk && artistOk; + }); + + } + + static async obtenerAñoYGanadorF1(){ + const query = ` + SELECT ?year ?winnerLabel + WHERE { + wd:Q1968 wdt:P793 ?event. + ?event wdt:P585 ?date. + ?event wdt:P1346 ?winner. + ?winner wdt:P31 wd:Q5. + BIND(YEAR(?date) AS ?year) + SERVICE wikibase:label { bd:serviceParam wikibase:language "es". } + } + ORDER BY ?year + `; + + const results = await wikidata.consulta(query); + // console.log(results) + return results; + + } + + static async obtenerAñoYEquipoGanadorF1(){ + const query = ` + SELECT ?year ?winnerLabel + WHERE { + wd:Q1968 wdt:P793 ?event. + ?event wdt:P585 ?date. + ?event wdt:P1346 ?winner. + ?winner wdt:P31 wd:Q10497835. + BIND(YEAR(?date) AS ?year) + SERVICE wikibase:label { bd:serviceParam wikibase:language "es". } + } + ORDER BY ?year + `; + + const results = await wikidata.consulta(query); + // console.log(results) + return results; + + } + + + /* ARTE */ static async obtenerMonumentoYAñoDescubOAñoConst() { const query = ` @@ -71,16 +248,19 @@ class WikiQueries { } - // REVISAR // - static async obtenerPaisYLenguaje() { + /* DEPORTE */ + + static async obtenerJugadorYPais() { //País en el que juega const query = ` - SELECT ?countryLabel ?languageLabel WHERE { - ?country wdt:P31 wd:Q6256. - ?country wdt:P37 ?language. - SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE],es". } + SELECT ?playerLabel ?countryLabel + WHERE { + ?player wdt:P106 wd:Q3665646; + wdt:P54 ?team. + ?team wdt:P31 wd:Q13393265; + wdt:P17 ?country. + SERVICE wikibase:label {bd:serviceParam wikibase:language "[AUTO_LANGUAGE],en". } } - LIMIT 500 `; const results = await wikidata.consulta(query); @@ -89,14 +269,16 @@ class WikiQueries { } - static async obtenerPaisYBandera() { + static async obtenerJugadorYDeporte() { const query = ` - SELECT ?flag ?flagLabel ?countryLabel WHERE { - ?country wdt:P31 wd:Q6256; - wdt:P41 ?flag. - SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE],es". } + SELECT ?personLabel ?sportLabel + WHERE { + ?person wdt:P101 ?trabajo. + ?trabajo wdt:P31/wdt:P279* wd:Q31629. + ?person wdt:P106 ?sport. + + SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE],en". } } - LIMIT 200 `; const results = await wikidata.consulta(query); @@ -105,20 +287,17 @@ class WikiQueries { } - static async obtenerCantanteYCancion() { + static async obtenerEstadioYAñoFund() { const query = ` - SELECT ?song ?songLabel ?singer ?singerLabel + SELECT DISTINCT ?estadioLabel ?fundacion WHERE { - ?song wdt:P31 wd:Q7366; # Canción - wdt:P175 ?singer. # Cantante - ?singer wdt:P27 wd:Q29. # Español - MINUS { - ?song wdt:P175 ?anotherSinger. # Quitamos canciones con más de un cantante - FILTER (?anotherSinger != ?singer) - } - SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE],en". } + ?estadio wdt:P31 wd:Q483110 ; # Instancia de estadio deportivo + wdt:P17 wd:Q29 ; # Ubicado en España + wdt:P571 ?fechaInicio . # Fecha de inicio de la construcción + BIND(YEAR(?fechaInicio) AS ?fundacion) + SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE],en". } } - LIMIT 200 + ORDER BY ?fundacion `; const results = await wikidata.consulta(query); @@ -126,6 +305,7 @@ class WikiQueries { return results; } + } diff --git a/questionsservice/wikidataExtractor/wikidataextractor-model.js b/questionsservice/wikidataExtractor/wikidataextractor-model.js deleted file mode 100644 index 1b3f9971..00000000 --- a/questionsservice/wikidataExtractor/wikidataextractor-model.js +++ /dev/null @@ -1,27 +0,0 @@ -const mongoose = require('mongoose'); - -const paisSchema = new mongoose.Schema({ - pais: { - type: String, - required: true - }, - capital: { - type: String, - required: false - }, - lenguaje: { - type: String, - required: false - }, - bandera: { - type: String, - required: false - } -}, {timestamps: {}}); // Añade y gestiona automáticamente los campos createdAt y updatedAt - - -const Pais = mongoose.model('Pais', paisSchema); - -module.exports = { - Pais -}; \ No newline at end of file diff --git a/questionsservice/wikidataExtractor/wikidataextractor-service.js b/questionsservice/wikidataExtractor/wikidataextractor-service.js index ccd5137f..b508e4ed 100644 --- a/questionsservice/wikidataExtractor/wikidataextractor-service.js +++ b/questionsservice/wikidataExtractor/wikidataextractor-service.js @@ -2,7 +2,8 @@ const express = require('express'); const cron = require('node-cron'); const mongoose = require('mongoose'); const WikiQueries = require('./wikidataQueries'); -const { Pais } = require('./wikidataextractor-model'); +const modelUri = process.env.DATAMODELS_URI || '../questiondata-model'; +const { Pais, Monumento, Elemento, Pelicula, Cancion } = require(modelUri); const app = express(); const port = 8008; @@ -17,74 +18,90 @@ db.once('open', () => console.log("Connected to MongoDB: %s", mongoUri)); // Middleware to parse JSON in request body app.use(express.json()); -async function extractData() { - var data = await WikiQueries.obtenerPaisYLenguaje(); +const templates = [ + { + extractMethod: () => WikiQueries.obtenerPaisYCapital(), + filtro: (element) => { return { pais: String(element.countryLabel) }}, + campo_actualizar: (element) => { return { capital: element.capitalLabel }}, + saveMethod: (transactions) => Pais.bulkWrite(transactions) + }, + { + extractMethod: () => WikiQueries.obtenerPaisYContinente(), + filtro: (element) => { return { pais: String(element.countryLabel) }}, + campo_actualizar: (element) => { return { continente: element.continentLabel }}, + saveMethod: (transactions) => Pais.bulkWrite(transactions) + }, + { + extractMethod: () => WikiQueries.obtenerMonumentoYPais(), + filtro: (element) => { return { monumento: String(element.monumentLabel) }}, + campo_actualizar: (element) => { return { pais: element.countryLabel }}, + saveMethod: (transactions) => Monumento.bulkWrite(transactions) + }, + // { + // extractMethod: () => WikiQueries.obtenerPaisYLenguaje(), + // filtro: (element) => { return { pais: String(element.countryLabel) }}, + // campo_actualizar: (element) => { return { lenguaje: element.languageLabel }}, + // saveMethod: (transactions) => Pais.bulkWrite(transactions) + // }, + // { + // extractMethod: () => WikiQueries.obtenerPaisYBandera(), + // filtro: (element) => { return { pais: String(element.countryLabel) }}, + // campo_actualizar: (element) => { return { bandera: element.flagLabel }}, + // saveMethod: (transactions) => Pais.bulkWrite(transactions) + // }, + { + extractMethod: () => WikiQueries.obtenerSimboloQuimico(), + filtro: (element) => { return { elemento: String(element.elementLabel) }}, + campo_actualizar: (element) => { return { simbolo: element.symbol }}, + saveMethod: (transactions) => Elemento.bulkWrite(transactions) + }, + { + extractMethod: () => WikiQueries.obtenerPeliculaYDirector(), + filtro: (element) => { return { pelicula: String(element.peliculaLabel) }}, + campo_actualizar: (element) => { return { director: element.directorLabel }}, + saveMethod: (transactions) => Pelicula.bulkWrite(transactions) + }, + { + extractMethod: () => WikiQueries.obtenerCancionYArtista(), + filtro: (element) => { return { cancion: String(element.songLabel) }}, + campo_actualizar: (element) => { return { artista: element.artistLabel }}, + saveMethod: (transactions) => Cancion.bulkWrite(transactions) + } +]; + +async function extractData(template) { + console.log("Actualizando los datos sobre:") + const data = await template.extractMethod(); console.log(data); - var paises = data.map(function (element) { - var p = { + const transactions = data.map(function (element) { + let transaction = { updateOne: { - filter: { pais: String(element.countryLabel) }, - update: { - capital: element.capitalLabel, - lenguaje: element.languageLabel, - bandera: element.flagLabel - }, + filter: template.filtro(element), + update: template.campo_actualizar(element), upsert: true } }; - console.log(p); - return p; + return transaction; }); - await Pais.bulkWrite(paises); + await template.saveMethod(transactions); - return paises; + return transactions; } -var minutes = 30; +const minutes = 30; +const totalQueries = templates.length; +let query = 0; cron.schedule(`*/${minutes} * * * *`, () => { - console.log(`Running a task every ${minutes} minutes: ${Date()}`); - // Call function here - extractData(); + try { + console.log(`Running a task every ${minutes} minutes: ${Date()}`); + extractData(templates[query]); + query = (query+1)%totalQueries; + } catch (error) { + console.error(error.message) + } + }); - /* - ALL ROUTES ARE ONLY FOR DEVELOPING PURPOSES, THEY SHOULD GET DELETED IN PRODUCTION - THIS SERVICE SHOULD NOT BE ACCESSIBLE FROM OUTSIDE - */ - -// Route for extracting countries -// app.get('/extract', async (req, res) => { -// try { -// res.json(await extractData()); -// } catch (error) { -// res.status(500).json({ message: error.message }) -// // res.status(500).json({ error: 'Internal Server Error' }); -// } -// }); - -// // Route for geting countries -// app.get('/countries', async (req, res) => { -// try { -// const paises = await Pais.find({}) -// res.json(paises); -// } catch (error) { -// res.status(500).json({ message: error.message }) -// // res.status(500).json({ error: 'Internal Server Error' }); -// } -// }); - -// // Route for deleting countries -// app.delete('/countries', async (req, res) => { -// try { -// const paises = await Pais.deleteMany({}) -// res.json(paises); -// } catch (error) { -// res.status(500).json({ message: error.message }) -// // res.status(500).json({ error: 'Internal Server Error' }); -// } -// }); - - app.use((err, req, res, next) => { console.error(`An error occurred: ${err}`); res.status(500).send(`An error occurred: ${err.message}`); @@ -92,7 +109,7 @@ app.use((err, req, res, next) => { // Start the server const server = app.listen(port, () => { - console.log(`Questions Service listening at http://localhost:${port}`); + console.log(`Wikidata Extractor listening at http://localhost:${port}`); }); server.on('close', () => { diff --git a/sonar-project.properties b/sonar-project.properties index 4ae7bcc0..3a042d26 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -11,7 +11,7 @@ sonar.language=js sonar.projectName=wiq_es6c sonar.coverage.exclusions=**/*.test.js -sonar.sources=webapp,users/authservice,users/userservice,gatewayservice,storeQuestionService,userStatsService,apisgatewayservice,gameservice +sonar.sources=webapp,users/authservice,users/userservice,gatewayservice,storeQuestionService,userStatsService,apisgatewayservice,gameservice,questionsservice/questiongeneratorservice,questionsservice/wikidataExtractor sonar.sourceEncoding=UTF-8 sonar.exclusions=node_modules/** sonar.javascript.lcov.reportPaths=**/coverage/lcov.info