diff --git a/.dcignore b/.dcignore
new file mode 100644
index 0000000..d1d85ac
--- /dev/null
+++ b/.dcignore
@@ -0,0 +1,23 @@
+# Write glob rules for ignored files.
+
+# Check syntax on https://deepcode.freshdesk.com/support/solutions/articles/60000531055-how-can-i-ignore-files-or-directories-
+
+# Check examples on https://github.com/github/gitignore
+
+# Hidden directories
+
+.\*/
+
+# Node
+
+logs
+pids
+lib-cov
+coverage
+bower_components
+build/Release
+node_modules/
+jspm_packages/
+web_modules/
+out
+dist
diff --git a/.eslintrc.json b/.eslintrc.json
new file mode 100644
index 0000000..58b8ff4
--- /dev/null
+++ b/.eslintrc.json
@@ -0,0 +1,29 @@
+{
+ "env": {
+ "browser": true,
+ "node": true,
+ "commonjs": true,
+ "shared-node-browser": true,
+ "es2024": true
+ },
+ "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
+ "parser": "@typescript-eslint/parser",
+ "parserOptions": {
+ "ecmaVersion": "latest"
+ },
+ "plugins": ["@typescript-eslint"],
+ "rules": {
+ "linebreak-style": ["warn", "unix"],
+ "quotes": [
+ "warn",
+ "double",
+ {
+ "avoidEscape": true
+ }
+ ],
+ "semi": ["error", "always"],
+ "no-unused-vars": ["off"],
+ "@typescript-eslint/no-unused-vars": ["warn"],
+ "prefer-const": ["warn"]
+ }
+}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..3013bbd
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,139 @@
+# SQLite files
+punto.sqlite
+
+docs/
+dist/
+
+# Vscode
+.vscode/
+
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+.pnpm-debug.log*
+
+# Diagnostic reports (https://nodejs.org/api/report.html)
+report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+*.lcov
+
+# nyc test coverage
+.nyc_output
+
+# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+bower_components
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (https://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directories
+node_modules/
+jspm_packages/
+
+# Snowpack dependency directory (https://snowpack.dev/)
+web_modules/
+
+# TypeScript cache
+*.tsbuildinfo
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Optional stylelint cache
+.stylelintcache
+
+# Microbundle cache
+.rpt2_cache/
+.rts2_cache_cjs/
+.rts2_cache_es/
+.rts2_cache_umd/
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# dotenv environment variable files
+.env
+.env.development.local
+.env.test.local
+.env.production.local
+.env.local
+
+# parcel-bundler cache (https://parceljs.org/)
+.cache
+.parcel-cache
+
+# Next.js build output
+.next
+out
+
+# Nuxt.js build / generate output
+.nuxt
+XXXdist
+
+# Gatsby files
+.cache/
+# Comment in the public line in if your project uses Gatsby and not Next.js
+# https://nextjs.org/blog/next-9-1#public-directory-support
+# public
+
+# vuepress build output
+.vuepress/dist
+
+# vuepress v2.x temp and cache directory
+.temp
+.cache
+
+# Docusaurus cache and generated files
+.docusaurus
+
+# Serverless directories
+.serverless/
+
+# FuseBox cache
+.fusebox/
+
+# DynamoDB Local files
+.dynamodb/
+
+# TernJS port file
+.tern-port
+
+# Stores VSCode versions used for testing VSCode extensions
+.vscode-test
+
+# yarn v2
+.yarn/cache
+.yarn/unplugged
+.yarn/build-state.yml
+.yarn/install-state.gz
+.pnp.*
\ No newline at end of file
diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 0000000..0f3e7a3
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1,9 @@
+{
+ "tabWidth": 4,
+ "useTabs": false,
+ "semi": true,
+ "trailingComma": "all",
+ "endOfLine": "lf",
+ "singleQuote": false,
+ "bracketSpacing": false
+}
diff --git a/Projet 1 _ Punto.pdf b/Projet 1 _ Punto.pdf
new file mode 100644
index 0000000..eba241f
Binary files /dev/null and b/Projet 1 _ Punto.pdf differ
diff --git "a/Punto_r\303\250gles.pdf" "b/Punto_r\303\250gles.pdf"
new file mode 100644
index 0000000..420d4ba
Binary files /dev/null and "b/Punto_r\303\250gles.pdf" differ
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..c690dbd
--- /dev/null
+++ b/README.md
@@ -0,0 +1,110 @@
+# Repository pour le projet du Punto réalisé à l'IUT de Vannes
+
+### Le code source est le même que celui qui a été rendu (le commit initial)
+
+### Le fichier [Projet 1 \_ Punto.pdf](./Projet%201%20_%20Punto.pdf) contient le sujet du projet
+
+- Ce sujet à été modifié après le rendu, pour ajouter la base de donnée Neo4j.
+
+### Le fichier [Punto_règles.pdf](./Punto_règles.pdf) contient les règles du jeu de Punto
+
+---
+
+# PuntoDB : Le Jeu de Punto avec Gestion de Base de Données
+
+### Auteurs
+
+- Naexy
+
+### Introduction
+
+PuntoDB est un jeu de Punto implémenté avec une interface de terminal, permettant la liaison avec quatres bases de données différentes : MySQL, SQLite, MongoDB et Neo4j. Le programme offre une expérience interactive en ligne de commande, avec des options pour jouer, gérer les bases de données et générer des parties.
+
+### Prérequis
+
+- Assurez-vous d'avoir installé MySQL, SQLite, MongoDB et Neo4j sur votre machine.
+- Node.js doit être installé pour exécuter le programme.
+
+### Installation des Dépendances
+
+Pour installer les dépendances nécessaires, exécutez :
+
+```sh
+pnpm install
+```
+
+### Construction du Projet
+
+Pour construire le projet (si nécessaire, une version construite est déjà incluse dans le dossier `dist`) :
+
+```sh
+pnpm run build
+```
+
+### Exécution des Tests
+
+Pour exécuter les tests :
+
+```sh
+pnpm run test
+```
+
+### Lancement du Programme
+
+Pour démarrer le programme principal :
+
+```sh
+pnpm run start
+```
+
+### Nettoyage des Fichiers de Build
+
+Pour nettoyer les fichiers de build (si un nouveau build a été effectué) :
+
+```sh
+pnpm run clean
+```
+
+- Cette commande utilise la commande `del` de Windows pour supprimer `tsconfig.tsbuildinfo`.
+
+### Documentation
+
+Pour générer la documentation du projet (si nécessaire, une version générée est déjà incluse dans le dossier `docs`) :
+
+```sh
+pnpm run docs
+```
+
+### Utilisation du Programme
+
+Au lancement, le programme offre plusieurs options :
+
+- Tapez `game` pour lancer une partie.
+- Tapez `db` pour accéder aux commandes de base de données.
+
+#### Mode Base de Données
+
+Dans ce mode, vous pouvez :
+
+- Activer ou désactiver les bases de données pour la sauvegarde des parties.
+- Vider les bases de données actives.
+
+#### Mode Jeu
+
+- Jouez une partie de Punto ou générez plusieurs parties (`g100` pour 100 parties, par exemple).
+- Les résultats des parties seront automatiquement sauvegardés dans les bases de données activées.
+
+#### Commandes Globales
+
+Des commandes comme `exit`, `n`, `quit`, etc., sont disponibles à tout moment pour naviguer ou quitter le programme.
+Voici la liste complète des commandes de "refus" :
+
+- `bye`
+- `exit`
+- `false`
+- `n`
+- `no`
+- `q`
+- `quit`
+- `refuse`
+- `stop`
diff --git a/assets_demo/check_db_game_cards.png b/assets_demo/check_db_game_cards.png
new file mode 100644
index 0000000..a7d3e11
Binary files /dev/null and b/assets_demo/check_db_game_cards.png differ
diff --git a/assets_demo/check_db_game_players.png b/assets_demo/check_db_game_players.png
new file mode 100644
index 0000000..a05881f
Binary files /dev/null and b/assets_demo/check_db_game_players.png differ
diff --git a/assets_demo/check_db_game_punto.png b/assets_demo/check_db_game_punto.png
new file mode 100644
index 0000000..19165fc
Binary files /dev/null and b/assets_demo/check_db_game_punto.png differ
diff --git a/assets_demo/check_db_game_punto2.png b/assets_demo/check_db_game_punto2.png
new file mode 100644
index 0000000..7716795
Binary files /dev/null and b/assets_demo/check_db_game_punto2.png differ
diff --git a/assets_demo/check_db_users.png b/assets_demo/check_db_users.png
new file mode 100644
index 0000000..6978524
Binary files /dev/null and b/assets_demo/check_db_users.png differ
diff --git a/assets_demo/choose_db.png b/assets_demo/choose_db.png
new file mode 100644
index 0000000..7f2c9d3
Binary files /dev/null and b/assets_demo/choose_db.png differ
diff --git a/assets_demo/choose_sqlite.png b/assets_demo/choose_sqlite.png
new file mode 100644
index 0000000..f92df44
Binary files /dev/null and b/assets_demo/choose_sqlite.png differ
diff --git a/assets_demo/empty_db.png b/assets_demo/empty_db.png
new file mode 100644
index 0000000..630a678
Binary files /dev/null and b/assets_demo/empty_db.png differ
diff --git a/assets_demo/generate_games.png b/assets_demo/generate_games.png
new file mode 100644
index 0000000..e237778
Binary files /dev/null and b/assets_demo/generate_games.png differ
diff --git a/assets_demo/play_game.png b/assets_demo/play_game.png
new file mode 100644
index 0000000..392b1e3
Binary files /dev/null and b/assets_demo/play_game.png differ
diff --git a/assets_demo/play_game_1.png b/assets_demo/play_game_1.png
new file mode 100644
index 0000000..7a9a1b0
Binary files /dev/null and b/assets_demo/play_game_1.png differ
diff --git a/assets_demo/play_game_2.png b/assets_demo/play_game_2.png
new file mode 100644
index 0000000..807a6a2
Binary files /dev/null and b/assets_demo/play_game_2.png differ
diff --git a/assets_demo/play_game_3.png b/assets_demo/play_game_3.png
new file mode 100644
index 0000000..abecb9b
Binary files /dev/null and b/assets_demo/play_game_3.png differ
diff --git a/assets_demo/quit_db.png b/assets_demo/quit_db.png
new file mode 100644
index 0000000..5631782
Binary files /dev/null and b/assets_demo/quit_db.png differ
diff --git a/assets_demo/quit_game.png b/assets_demo/quit_game.png
new file mode 100644
index 0000000..c7f9e64
Binary files /dev/null and b/assets_demo/quit_game.png differ
diff --git a/assets_demo/start_game.png b/assets_demo/start_game.png
new file mode 100644
index 0000000..ef04602
Binary files /dev/null and b/assets_demo/start_game.png differ
diff --git a/assets_demo/start_program.png b/assets_demo/start_program.png
new file mode 100644
index 0000000..2d31700
Binary files /dev/null and b/assets_demo/start_program.png differ
diff --git a/demo.md b/demo.md
new file mode 100644
index 0000000..1d42fc6
--- /dev/null
+++ b/demo.md
@@ -0,0 +1,46 @@
+## PuntoDB : Le Jeu de Punto avec Gestion de Base de Données
+
+### Démonstration avec Captures d'Écran
+
+#### Lancement du Programme
+Commencez par lancer le programme principal :
+- Écran de démarrage : ![Écran de démarrage](./assets_demo/start_program.png)
+
+#### Sélection d'une Base de Données
+Après le démarrage, choisissez une base de données pour enregistrer les parties :
+- Menu de sélection de la base de données : ![Menu de sélection de la base de données](./assets_demo/choose_db.png)
+- Sélection de SQLite comme exemple : ![Sélection de SQLite](./assets_demo/choose_sqlite.png)
+
+#### Quitter le Mode Base de Données
+Quitter la gestion des bases de données pour commencer une partie :
+- Retour au menu principal : ![Retour au menu principal](./assets_demo/quit_db.png)
+
+#### Démarrage d'une Partie
+Lancer une partie de Punto :
+- Écran de lancement d'une partie : ![Lancement d'une partie](./assets_demo/start_game.png)
+
+#### Jouer une Partie
+Démonstration d'une partie avec deux joueurs, `NJ` et `GK`, avec `NJ` comme premier joueur :
+- Début de la partie : ![Début de la partie](./assets_demo/play_game.png)
+- Jouer quelques coups : ![Jouer quelques coups](./assets_demo/play_game_1.png)
+- Coup automatique : ![Coup automatique](./assets_demo/play_game_2.png)
+- Autres coups joués : ![Autres coups joués](./assets_demo/play_game_3.png)
+
+#### Quitter la Partie
+Un joueur quitte la partie :
+- Écran de sortie de la partie : ![Quitter la partie](./assets_demo/quit_game.png)
+
+#### Vérification de la Sauvegarde
+Confirmation de l'enregistrement de la partie dans la base de données :
+- Vérification dans la base de données (Utilisateurs) : ![Utilisateurs](./assets_demo/check_db_users.png)
+- Vérification dans la base de données (Jeu - Punto) : ![Jeu - Punto](./assets_demo/check_db_game_punto.png)
+- Vérification dans la base de données (Jeu - Cartes) : ![Jeu - Cartes](./assets_demo/check_db_game_cards.png)
+- Vérification dans la base de données (Jeu - Joueurs) : ![Jeu - Joueurs](./assets_demo/check_db_game_players.png)
+
+#### Vider la Base de Données et Générer des Parties
+- Vidage de la base de données : ![Vider la base de données](./assets_demo/empty_db.png)
+- Génération de 10 parties : ![Génération de 10 parties](./assets_demo/generate_games.png)
+
+#### Vérification Finale
+Confirmation des parties générées dans la base de données :
+- Vérification dans la base de données après génération : ![Vérification après génération](./assets_demo/check_db_game_punto2.png)
\ No newline at end of file
diff --git a/infos/db_schem_mongo.drawio b/infos/db_schem_mongo.drawio
new file mode 100644
index 0000000..696c9da
--- /dev/null
+++ b/infos/db_schem_mongo.drawio
@@ -0,0 +1 @@
+7ZtLc6M4EIB/jY+TAgTGHGM7M5uqmZrUJvO8bMkg29oAcgk5Nvn1K0CYR4vsZKeCXQsnS42QRH/drUbCE7SIjh843m0/sYCEE8sIjhO0nFiWaRmO/MkkaSGZetNCsOE0UI0qwT19JkpoKOmeBiRpNBSMhYLumkKfxTHxRUOGOWeHZrM1C5uj7vCGAMG9j0Mo/UYDsVVSc+pVF/4gdLNVQ88st7gQ4bKxepJkiwN2qInQzQQtOGOiKEXHBQkz5ZV6Ke5733H1NDFOYvErN9x/n3200c9n9vXrp/Xiy80dffzznV308oTDvXrgu30smJqxSEs1JAcahTiWtfkW+9s9Jx/wTl6ypWDHaCwIv3mSE8m0bUhZIjAXiqaFpECNQ7ggx84HME9qkfZEWEQET2UTdYODlCaVKdmqeqhxcZRsW0Nil7aElS1sTl1X6pIFpbFXaM8B2vsra38tZZ9Xf0tzvA2AJuXzi0yLIpJDLU1ZxCHdxLIcknV2JdMRlQZ4rcQRDYLs5jknCX3Gq7wjo9R7/kjOfOIss572giWF0s0cAmePZMFCxqUkZjm+NQ3DluiX2HSbTycws8kLQV4zDS7rrWiV06nhmrjLEVAFyJ0CQlavhExAyMc8SAqXWshipskBA7OawNxze5QFeO1CnBKuiH1JCB+J1YlZDkTm9YoMwSDoDDoItgghzTLVbxCEOVnhVLdjanGC1koFdZHQNHulNh396uXId26/smBykftV8LDnceFZ8T5ayRVrwNBafmXq/Kpfal0pxjwdo2FXNDTPHQ0tmGUUvnY7elqHp3lnZwbzjuMISwtLw8rtFRXcePILRVzniDiNNyOuyrfOvfNkweQwHV1Lz0qzdvXrWy5gpYojLx2vc29oWDPA60Djh3RHxmjYVH25weHAXV7T6BMZjIYrlu3t5rwm7qD3C1tb8hrvmvbqXR5gVapwDIe6Vy9NathrOCznUwOWCCz2yRgNtcCQJt/oNTeE6UZ+atImdAkH8KcT9xcOM7QryeytlAfX/oEdwLuvWUzO/RoE15IYRwNK014FS+NbvcJCcLsc0CFxcJ19YyVrq5D5jw9bGjdp1cOQmcUqeUu9mrA998kd4VTOmvD7HfYzM0DLmbwo790Q0XGxE7wc4T0NywmQIxXfZfmdcWUYjhL8yGziqqwtj8pE8kpaq5yGLmWx1GzWmyk7Q6Wg1ltWrbrLa2m91u6w0CgJwDdorZAstZ4r6qVMTeEplPZvMRNaYc3OdB9RlTJOQizoU3O+OuNTI9xlblcZuWN0LCFlF8ogirsqEwYdoWk7trU6UsbT7kjaK05rzVRY6J5wyy3L9LtrXqC912gvC8UMKsc8MfgNX7X+f75qNnzV9rzf81b7sry1POS/cG+dele207Rn9786rG1clUcMPfssan8ZexE+q/kQx5riSCb181Dk1mYUZ2aysEpfkRn5JM5t9lJzo5dfMmynBQO5Z06E4LmYORgawDUcD9Do9XsABI++hkMDtV/ANZ9n9EsDbuUOhwZYZoxz04B7ScOhAXxDsxPbLw24OQUWeE7WRD6gTBWHgqm9vJe52Rss77Ja/buqSNuq/6ihm38A
\ No newline at end of file
diff --git a/infos/db_schem_sql.drawio b/infos/db_schem_sql.drawio
new file mode 100644
index 0000000..5b08f1e
--- /dev/null
+++ b/infos/db_schem_sql.drawio
@@ -0,0 +1 @@
+7Zzdc6M2EMD/Gl46cx2EwB+P8VeattdJJ7n27qmDQbE1h8Ej5NjcX18Bwgat7NjnGDvGM3lACwhpfytpdyXHwP3Z6p658+nnyCeBYZn+ysADw7KQbVlG+mf6SS7pFIIJo758aCN4oj+IFJpSuqA+iSsP8igKOJ1XhV4UhsTjFZnLWLSsPvYSBdWvzt0JAYInzw2g9F/q86mUolZ3c+M3QidTXvSvnd+YucXDsifx1PWjZUmEhwbusyji+dVs1SdBqrxCL/l7oy131w1jJOT7vPCP93s7HA6/JA9Loa0R+vzQ7X1CsppXN1jIHn+JCZMt5kmhhnhJZ4EbilJv6nrTBSP37lzcsoVgHtGQEzZ8FQ1JtW0KWcxdxiVNCwsBbG3xZcI4WZVEsvX3JJoRzhLxiLyLbalJaUp2S5aXJTCmlE3LTKTMlbYwWVe9UZe4kBo7RHsW0N5/6Qt3QhYuZmONIkVfeapEPhNfGiBx6QZ0EorrgLykd1J9UGF/d1I8o76fvtxjJKY/3HFWkVmoPeuR0zOcQVrTgkdxrnOUMWDRd9KPgogJSRhl9F5oECgiiGa3qewNDFV5YYiro6FlnYwWBrRCd0ZyXEJZNJzccG1wOefG1QG4Hhchjy5ybnIU5em0h6xa56Zuk+amzqHAzjc3eW3mvHrk/q+Xu7DTN//oPVqjT9gBtJY0fE7mVzs9HUtMN8B0i//JkEHPqe8y/yJnp7bjVGcnjbkjW6e8k2nvSj2nnZbyASYn7VgtKi7BGkeprd9w7TkzdWtdSwCt1Y2UllQbkmrXScoGpJIbKS2p7plJQfdsHrgJ8Z8XLLwh0yJD+4ZAJ4MGI6AMGuvdRtkWZJphhlCdyGDMn4+zh9so24JMs4bVi8xpA2ZeroqPHrieBhiqz5vX84JjTF7eBpiOlyb6qtWd12wyADwk9O/SzS5RGgeR9/15SsMqrnJaAaW5B/FKuRhHC+aRR8KoaDVhT3PXS8ctHnTETfHuhPAtN8mK8q8Sbnr9Lb3+1ZGlwap0a5CUCuvqClkotPVVNjcrlGpKi5uqslJSLqmVbTVH0e0RDQqtQBuS5pKrw3g7Js5Vs+PBIg9L/MpGIzS/kn05GvsqZIwELqevpNJcndHJLzym421j3S1lE80yFbuVhpC/tTFdUFHbrFaEsVKRNBq1omwMrLt9xLCAYe4vB0xbHgkza7mIievwhB5WEnoax61WVxvBUBY1hkYLmxdGA4arhtVyZ3PR34BnnTbHJIjCSZxOYXBL7WpJOQopjfdcqzOGWoDU099/Uk6yrc7HLFwFdC5hSwHmqDVxCNaoEp1OlzAQWcSEPVxtnrp1KLS39xVq3kKDoUi2sXBDdrG7no4mEyqVeCOmI6ZZYWoNH4tDciVgYj3gi/ha8zPHAtM4b/W6BDBtDfCcNd6vRNBF9F9cHx39rwvfjFIq4JDYf6t3Uw7qd65HHy6ob1VN2O4qxrlvUI+7SkWqlW8J6oUxuknpMTnotzbYUZMHLXNnu8Dz1bPO4iJvwbtmGDTHHpqTYVAPW2PNYetaY9p1F5pIw+4oNDSbCPXSsAANRl6I6KKYYNOvjgyn3xw8loLn3MdJLZgcvWQfQrv2/2Tm/722I47wItr7ehHWRXkRbcWI1aPp+zoRSJ2sTuREgO/Yu50I9fnCqTitEwET481ZtlBXT+h8yxbMXjSHBlZWqbVTcTYaMPlddSLklsWErxXQAEzqNKXDVO9vU6DnfVnORLHir1f5dergOg4g7Drc+OGcjK6jWLf9c15GC71R0Tt5GR3rDF4DhuFVc7bTbfV33udep3CTj5p01HGm2YKqlwb0qMHhhvzQsLgYJ40BpZ4JOqHfIIqb/1uRz3mb//6Bh/8D
\ No newline at end of file
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..f308ef0
--- /dev/null
+++ b/package.json
@@ -0,0 +1,33 @@
+{
+ "name": "puntodb",
+ "version": "1.0.0",
+ "description": "A Punto game used with database",
+ "main": "dist/main.js",
+ "scripts": {
+ "build": "tsc --build ./tsconfig.json --verbose --incremental",
+ "test": "ts-node ./src/tests/Tests.test.ts",
+ "start": "node ./dist/main.js",
+ "clean": "tsc --build ./tsconfig.json --clean && del tsconfig.tsbuildinfo",
+ "docs": "typedoc --entryPointStrategy expand ./src --out docs --darkHighlightTheme dark-plus --includeVersion true --excludeInternal false"
+ },
+ "author": "Naexy",
+ "license": "",
+ "packageManager": "pnpm@8.15.4",
+ "devDependencies": {
+ "@types/node": "^20.8.10",
+ "@typescript-eslint/eslint-plugin": "^6.9.1",
+ "@typescript-eslint/parser": "^6.9.1",
+ "eslint": "^8.53.0",
+ "ts-node": "^10.9.2",
+ "typedoc": "^0.25.12",
+ "typescript": "^5.4.2"
+ },
+ "dependencies": {
+ "mongodb": "^5.9.1",
+ "mysql2": "^3.6.3",
+ "neogma": "^1.12.3",
+ "reflect-metadata": "^0.1.13",
+ "sqlite3": "^5.1.6",
+ "typeorm": "^0.3.17"
+ }
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
new file mode 100644
index 0000000..22b701e
--- /dev/null
+++ b/pnpm-lock.yaml
@@ -0,0 +1,2497 @@
+lockfileVersion: '6.0'
+
+settings:
+ autoInstallPeers: true
+ excludeLinksFromLockfile: false
+
+dependencies:
+ mongodb:
+ specifier: ^5.9.1
+ version: 5.9.2
+ mysql2:
+ specifier: ^3.6.3
+ version: 3.9.2
+ neogma:
+ specifier: ^1.12.3
+ version: 1.13.0
+ reflect-metadata:
+ specifier: ^0.1.13
+ version: 0.1.14
+ sqlite3:
+ specifier: ^5.1.6
+ version: 5.1.7
+ typeorm:
+ specifier: ^0.3.17
+ version: 0.3.20(mongodb@5.9.2)(mysql2@3.9.2)(sqlite3@5.1.7)(ts-node@10.9.2)
+
+devDependencies:
+ '@types/node':
+ specifier: ^20.8.10
+ version: 20.11.26
+ '@typescript-eslint/eslint-plugin':
+ specifier: ^6.9.1
+ version: 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.57.0)(typescript@5.4.2)
+ '@typescript-eslint/parser':
+ specifier: ^6.9.1
+ version: 6.21.0(eslint@8.57.0)(typescript@5.4.2)
+ eslint:
+ specifier: ^8.53.0
+ version: 8.57.0
+ ts-node:
+ specifier: ^10.9.2
+ version: 10.9.2(@types/node@20.11.26)(typescript@5.4.2)
+ typedoc:
+ specifier: ^0.25.12
+ version: 0.25.12(typescript@5.4.2)
+ typescript:
+ specifier: ^5.4.2
+ version: 5.4.2
+
+packages:
+
+ /@aashutoshrathi/word-wrap@1.2.6:
+ resolution: {integrity: sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==}
+ engines: {node: '>=0.10.0'}
+ dev: true
+
+ /@cspotcode/source-map-support@0.8.1:
+ resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
+ engines: {node: '>=12'}
+ dependencies:
+ '@jridgewell/trace-mapping': 0.3.9
+
+ /@eslint-community/eslint-utils@4.4.0(eslint@8.57.0):
+ resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+ peerDependencies:
+ eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
+ dependencies:
+ eslint: 8.57.0
+ eslint-visitor-keys: 3.4.3
+ dev: true
+
+ /@eslint-community/regexpp@4.10.0:
+ resolution: {integrity: sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==}
+ engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
+ dev: true
+
+ /@eslint/eslintrc@2.1.4:
+ resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+ dependencies:
+ ajv: 6.12.6
+ debug: 4.3.4
+ espree: 9.6.1
+ globals: 13.24.0
+ ignore: 5.3.1
+ import-fresh: 3.3.0
+ js-yaml: 4.1.0
+ minimatch: 3.1.2
+ strip-json-comments: 3.1.1
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /@eslint/js@8.57.0:
+ resolution: {integrity: sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+ dev: true
+
+ /@gar/promisify@1.1.3:
+ resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==}
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /@humanwhocodes/config-array@0.11.14:
+ resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==}
+ engines: {node: '>=10.10.0'}
+ dependencies:
+ '@humanwhocodes/object-schema': 2.0.2
+ debug: 4.3.4
+ minimatch: 3.1.2
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /@humanwhocodes/module-importer@1.0.1:
+ resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==}
+ engines: {node: '>=12.22'}
+ dev: true
+
+ /@humanwhocodes/object-schema@2.0.2:
+ resolution: {integrity: sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==}
+ dev: true
+
+ /@isaacs/cliui@8.0.2:
+ resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
+ engines: {node: '>=12'}
+ dependencies:
+ string-width: 5.1.2
+ string-width-cjs: /string-width@4.2.3
+ strip-ansi: 7.1.0
+ strip-ansi-cjs: /strip-ansi@6.0.1
+ wrap-ansi: 8.1.0
+ wrap-ansi-cjs: /wrap-ansi@7.0.0
+ dev: false
+
+ /@jridgewell/resolve-uri@3.1.2:
+ resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
+ engines: {node: '>=6.0.0'}
+
+ /@jridgewell/sourcemap-codec@1.4.15:
+ resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==}
+
+ /@jridgewell/trace-mapping@0.3.9:
+ resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
+ dependencies:
+ '@jridgewell/resolve-uri': 3.1.2
+ '@jridgewell/sourcemap-codec': 1.4.15
+
+ /@mongodb-js/saslprep@1.1.5:
+ resolution: {integrity: sha512-XLNOMH66KhJzUJNwT/qlMnS4WsNDWD5ASdyaSH3EtK+F4r/CFGa3jT4GNi4mfOitGvWXtdLgQJkQjxSVrio+jA==}
+ requiresBuild: true
+ dependencies:
+ sparse-bitfield: 3.0.3
+ dev: false
+ optional: true
+
+ /@nodelib/fs.scandir@2.1.5:
+ resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
+ engines: {node: '>= 8'}
+ dependencies:
+ '@nodelib/fs.stat': 2.0.5
+ run-parallel: 1.2.0
+ dev: true
+
+ /@nodelib/fs.stat@2.0.5:
+ resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==}
+ engines: {node: '>= 8'}
+ dev: true
+
+ /@nodelib/fs.walk@1.2.8:
+ resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
+ engines: {node: '>= 8'}
+ dependencies:
+ '@nodelib/fs.scandir': 2.1.5
+ fastq: 1.17.1
+ dev: true
+
+ /@npmcli/fs@1.1.1:
+ resolution: {integrity: sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==}
+ requiresBuild: true
+ dependencies:
+ '@gar/promisify': 1.1.3
+ semver: 7.6.0
+ dev: false
+ optional: true
+
+ /@npmcli/move-file@1.1.2:
+ resolution: {integrity: sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==}
+ engines: {node: '>=10'}
+ deprecated: This functionality has been moved to @npmcli/fs
+ requiresBuild: true
+ dependencies:
+ mkdirp: 1.0.4
+ rimraf: 3.0.2
+ dev: false
+ optional: true
+
+ /@pkgjs/parseargs@0.11.0:
+ resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
+ engines: {node: '>=14'}
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /@sqltools/formatter@1.2.5:
+ resolution: {integrity: sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==}
+ dev: false
+
+ /@tootallnate/once@1.1.2:
+ resolution: {integrity: sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==}
+ engines: {node: '>= 6'}
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /@tsconfig/node10@1.0.9:
+ resolution: {integrity: sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==}
+
+ /@tsconfig/node12@1.0.11:
+ resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==}
+
+ /@tsconfig/node14@1.0.3:
+ resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==}
+
+ /@tsconfig/node16@1.0.4:
+ resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==}
+
+ /@types/json-schema@7.0.15:
+ resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
+ dev: true
+
+ /@types/node@20.11.26:
+ resolution: {integrity: sha512-YwOMmyhNnAWijOBQweOJnQPl068Oqd4K3OFbTc6AHJwzweUwwWG3GIFY74OKks2PJUDkQPeddOQES9mLn1CTEQ==}
+ dependencies:
+ undici-types: 5.26.5
+
+ /@types/revalidator@0.3.12:
+ resolution: {integrity: sha512-DsA2jHfz73JaIROVoMDd/x7nVWXBmEdDSoXB4yQlDzv/NCBkFY2fMHkyE6DGrvooLDAFe5QI6l9Wq0TgdopMtg==}
+ dev: false
+
+ /@types/semver@7.5.8:
+ resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==}
+ dev: true
+
+ /@types/webidl-conversions@7.0.3:
+ resolution: {integrity: sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==}
+ dev: false
+
+ /@types/whatwg-url@8.2.2:
+ resolution: {integrity: sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA==}
+ dependencies:
+ '@types/node': 20.11.26
+ '@types/webidl-conversions': 7.0.3
+ dev: false
+
+ /@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.57.0)(typescript@5.4.2):
+ resolution: {integrity: sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==}
+ engines: {node: ^16.0.0 || >=18.0.0}
+ peerDependencies:
+ '@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha
+ eslint: ^7.0.0 || ^8.0.0
+ typescript: '*'
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+ dependencies:
+ '@eslint-community/regexpp': 4.10.0
+ '@typescript-eslint/parser': 6.21.0(eslint@8.57.0)(typescript@5.4.2)
+ '@typescript-eslint/scope-manager': 6.21.0
+ '@typescript-eslint/type-utils': 6.21.0(eslint@8.57.0)(typescript@5.4.2)
+ '@typescript-eslint/utils': 6.21.0(eslint@8.57.0)(typescript@5.4.2)
+ '@typescript-eslint/visitor-keys': 6.21.0
+ debug: 4.3.4
+ eslint: 8.57.0
+ graphemer: 1.4.0
+ ignore: 5.3.1
+ natural-compare: 1.4.0
+ semver: 7.6.0
+ ts-api-utils: 1.3.0(typescript@5.4.2)
+ typescript: 5.4.2
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.2):
+ resolution: {integrity: sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==}
+ engines: {node: ^16.0.0 || >=18.0.0}
+ peerDependencies:
+ eslint: ^7.0.0 || ^8.0.0
+ typescript: '*'
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+ dependencies:
+ '@typescript-eslint/scope-manager': 6.21.0
+ '@typescript-eslint/types': 6.21.0
+ '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.4.2)
+ '@typescript-eslint/visitor-keys': 6.21.0
+ debug: 4.3.4
+ eslint: 8.57.0
+ typescript: 5.4.2
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /@typescript-eslint/scope-manager@6.21.0:
+ resolution: {integrity: sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==}
+ engines: {node: ^16.0.0 || >=18.0.0}
+ dependencies:
+ '@typescript-eslint/types': 6.21.0
+ '@typescript-eslint/visitor-keys': 6.21.0
+ dev: true
+
+ /@typescript-eslint/type-utils@6.21.0(eslint@8.57.0)(typescript@5.4.2):
+ resolution: {integrity: sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==}
+ engines: {node: ^16.0.0 || >=18.0.0}
+ peerDependencies:
+ eslint: ^7.0.0 || ^8.0.0
+ typescript: '*'
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+ dependencies:
+ '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.4.2)
+ '@typescript-eslint/utils': 6.21.0(eslint@8.57.0)(typescript@5.4.2)
+ debug: 4.3.4
+ eslint: 8.57.0
+ ts-api-utils: 1.3.0(typescript@5.4.2)
+ typescript: 5.4.2
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /@typescript-eslint/types@6.21.0:
+ resolution: {integrity: sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==}
+ engines: {node: ^16.0.0 || >=18.0.0}
+ dev: true
+
+ /@typescript-eslint/typescript-estree@6.21.0(typescript@5.4.2):
+ resolution: {integrity: sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==}
+ engines: {node: ^16.0.0 || >=18.0.0}
+ peerDependencies:
+ typescript: '*'
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+ dependencies:
+ '@typescript-eslint/types': 6.21.0
+ '@typescript-eslint/visitor-keys': 6.21.0
+ debug: 4.3.4
+ globby: 11.1.0
+ is-glob: 4.0.3
+ minimatch: 9.0.3
+ semver: 7.6.0
+ ts-api-utils: 1.3.0(typescript@5.4.2)
+ typescript: 5.4.2
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /@typescript-eslint/utils@6.21.0(eslint@8.57.0)(typescript@5.4.2):
+ resolution: {integrity: sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==}
+ engines: {node: ^16.0.0 || >=18.0.0}
+ peerDependencies:
+ eslint: ^7.0.0 || ^8.0.0
+ dependencies:
+ '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0)
+ '@types/json-schema': 7.0.15
+ '@types/semver': 7.5.8
+ '@typescript-eslint/scope-manager': 6.21.0
+ '@typescript-eslint/types': 6.21.0
+ '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.4.2)
+ eslint: 8.57.0
+ semver: 7.6.0
+ transitivePeerDependencies:
+ - supports-color
+ - typescript
+ dev: true
+
+ /@typescript-eslint/visitor-keys@6.21.0:
+ resolution: {integrity: sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==}
+ engines: {node: ^16.0.0 || >=18.0.0}
+ dependencies:
+ '@typescript-eslint/types': 6.21.0
+ eslint-visitor-keys: 3.4.3
+ dev: true
+
+ /@ungap/structured-clone@1.2.0:
+ resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==}
+ dev: true
+
+ /abbrev@1.1.1:
+ resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==}
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /acorn-jsx@5.3.2(acorn@8.11.3):
+ resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
+ peerDependencies:
+ acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
+ dependencies:
+ acorn: 8.11.3
+ dev: true
+
+ /acorn-walk@8.3.2:
+ resolution: {integrity: sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==}
+ engines: {node: '>=0.4.0'}
+
+ /acorn@8.11.3:
+ resolution: {integrity: sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==}
+ engines: {node: '>=0.4.0'}
+ hasBin: true
+
+ /agent-base@6.0.2:
+ resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
+ engines: {node: '>= 6.0.0'}
+ requiresBuild: true
+ dependencies:
+ debug: 4.3.4
+ transitivePeerDependencies:
+ - supports-color
+ dev: false
+ optional: true
+
+ /agentkeepalive@4.5.0:
+ resolution: {integrity: sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==}
+ engines: {node: '>= 8.0.0'}
+ requiresBuild: true
+ dependencies:
+ humanize-ms: 1.2.1
+ dev: false
+ optional: true
+
+ /aggregate-error@3.1.0:
+ resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==}
+ engines: {node: '>=8'}
+ requiresBuild: true
+ dependencies:
+ clean-stack: 2.2.0
+ indent-string: 4.0.0
+ dev: false
+ optional: true
+
+ /ajv@6.12.6:
+ resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
+ dependencies:
+ fast-deep-equal: 3.1.3
+ fast-json-stable-stringify: 2.1.0
+ json-schema-traverse: 0.4.1
+ uri-js: 4.4.1
+ dev: true
+
+ /ansi-regex@5.0.1:
+ resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
+ engines: {node: '>=8'}
+
+ /ansi-regex@6.0.1:
+ resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==}
+ engines: {node: '>=12'}
+ dev: false
+
+ /ansi-sequence-parser@1.1.1:
+ resolution: {integrity: sha512-vJXt3yiaUL4UU546s3rPXlsry/RnM730G1+HkpKE012AN0sx1eOrxSu95oKDIonskeLTijMgqWZ3uDEe3NFvyg==}
+ dev: true
+
+ /ansi-styles@4.3.0:
+ resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
+ engines: {node: '>=8'}
+ dependencies:
+ color-convert: 2.0.1
+
+ /ansi-styles@6.2.1:
+ resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==}
+ engines: {node: '>=12'}
+ dev: false
+
+ /any-promise@1.3.0:
+ resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==}
+ dev: false
+
+ /app-root-path@3.1.0:
+ resolution: {integrity: sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA==}
+ engines: {node: '>= 6.0.0'}
+ dev: false
+
+ /aproba@2.0.0:
+ resolution: {integrity: sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==}
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /are-we-there-yet@3.0.1:
+ resolution: {integrity: sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==}
+ engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
+ requiresBuild: true
+ dependencies:
+ delegates: 1.0.0
+ readable-stream: 3.6.2
+ dev: false
+ optional: true
+
+ /arg@4.1.3:
+ resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==}
+
+ /argparse@2.0.1:
+ resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
+ dev: true
+
+ /array-union@2.1.0:
+ resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==}
+ engines: {node: '>=8'}
+ dev: true
+
+ /balanced-match@1.0.2:
+ resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
+
+ /base64-js@1.5.1:
+ resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
+ dev: false
+
+ /bindings@1.5.0:
+ resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==}
+ dependencies:
+ file-uri-to-path: 1.0.0
+ dev: false
+
+ /bl@4.1.0:
+ resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
+ dependencies:
+ buffer: 5.7.1
+ inherits: 2.0.4
+ readable-stream: 3.6.2
+ dev: false
+
+ /brace-expansion@1.1.11:
+ resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==}
+ dependencies:
+ balanced-match: 1.0.2
+ concat-map: 0.0.1
+
+ /brace-expansion@2.0.1:
+ resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==}
+ dependencies:
+ balanced-match: 1.0.2
+
+ /braces@3.0.2:
+ resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==}
+ engines: {node: '>=8'}
+ dependencies:
+ fill-range: 7.0.1
+ dev: true
+
+ /bson@5.5.1:
+ resolution: {integrity: sha512-ix0EwukN2EpC0SRWIj/7B5+A6uQMQy6KMREI9qQqvgpkV2frH63T0UDVd1SYedL6dNCmDBYB3QtXi4ISk9YT+g==}
+ engines: {node: '>=14.20.1'}
+ dev: false
+
+ /buffer@5.7.1:
+ resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
+ dependencies:
+ base64-js: 1.5.1
+ ieee754: 1.2.1
+ dev: false
+
+ /buffer@6.0.3:
+ resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
+ dependencies:
+ base64-js: 1.5.1
+ ieee754: 1.2.1
+ dev: false
+
+ /cacache@15.3.0:
+ resolution: {integrity: sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==}
+ engines: {node: '>= 10'}
+ requiresBuild: true
+ dependencies:
+ '@npmcli/fs': 1.1.1
+ '@npmcli/move-file': 1.1.2
+ chownr: 2.0.0
+ fs-minipass: 2.1.0
+ glob: 7.2.3
+ infer-owner: 1.0.4
+ lru-cache: 6.0.0
+ minipass: 3.3.6
+ minipass-collect: 1.0.2
+ minipass-flush: 1.0.5
+ minipass-pipeline: 1.2.4
+ mkdirp: 1.0.4
+ p-map: 4.0.0
+ promise-inflight: 1.0.1
+ rimraf: 3.0.2
+ ssri: 8.0.1
+ tar: 6.2.0
+ unique-filename: 1.1.1
+ transitivePeerDependencies:
+ - bluebird
+ dev: false
+ optional: true
+
+ /callsites@3.1.0:
+ resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
+ engines: {node: '>=6'}
+ dev: true
+
+ /chalk@4.1.2:
+ resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
+ engines: {node: '>=10'}
+ dependencies:
+ ansi-styles: 4.3.0
+ supports-color: 7.2.0
+
+ /chownr@1.1.4:
+ resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==}
+ dev: false
+
+ /chownr@2.0.0:
+ resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==}
+ engines: {node: '>=10'}
+ dev: false
+
+ /clean-stack@2.2.0:
+ resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==}
+ engines: {node: '>=6'}
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /cli-highlight@2.1.11:
+ resolution: {integrity: sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==}
+ engines: {node: '>=8.0.0', npm: '>=5.0.0'}
+ hasBin: true
+ dependencies:
+ chalk: 4.1.2
+ highlight.js: 10.7.3
+ mz: 2.7.0
+ parse5: 5.1.1
+ parse5-htmlparser2-tree-adapter: 6.0.1
+ yargs: 16.2.0
+ dev: false
+
+ /cliui@7.0.4:
+ resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==}
+ dependencies:
+ string-width: 4.2.3
+ strip-ansi: 6.0.1
+ wrap-ansi: 7.0.0
+ dev: false
+
+ /cliui@8.0.1:
+ resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
+ engines: {node: '>=12'}
+ dependencies:
+ string-width: 4.2.3
+ strip-ansi: 6.0.1
+ wrap-ansi: 7.0.0
+ dev: false
+
+ /clone@2.1.2:
+ resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==}
+ engines: {node: '>=0.8'}
+ dev: false
+
+ /color-convert@2.0.1:
+ resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
+ engines: {node: '>=7.0.0'}
+ dependencies:
+ color-name: 1.1.4
+
+ /color-name@1.1.4:
+ resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
+
+ /color-support@1.1.3:
+ resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==}
+ hasBin: true
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /concat-map@0.0.1:
+ resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
+
+ /console-control-strings@1.1.0:
+ resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==}
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /create-require@1.1.1:
+ resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==}
+
+ /cross-spawn@7.0.3:
+ resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
+ engines: {node: '>= 8'}
+ dependencies:
+ path-key: 3.1.1
+ shebang-command: 2.0.0
+ which: 2.0.2
+
+ /dayjs@1.11.10:
+ resolution: {integrity: sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==}
+ dev: false
+
+ /debug@4.3.4:
+ resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
+ engines: {node: '>=6.0'}
+ peerDependencies:
+ supports-color: '*'
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
+ dependencies:
+ ms: 2.1.2
+
+ /decompress-response@6.0.0:
+ resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==}
+ engines: {node: '>=10'}
+ dependencies:
+ mimic-response: 3.1.0
+ dev: false
+
+ /deep-extend@0.6.0:
+ resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==}
+ engines: {node: '>=4.0.0'}
+ dev: false
+
+ /deep-is@0.1.4:
+ resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
+ dev: true
+
+ /delegates@1.0.0:
+ resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==}
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /denque@2.1.0:
+ resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==}
+ engines: {node: '>=0.10'}
+ dev: false
+
+ /detect-libc@2.0.2:
+ resolution: {integrity: sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==}
+ engines: {node: '>=8'}
+ dev: false
+
+ /diff@4.0.2:
+ resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==}
+ engines: {node: '>=0.3.1'}
+
+ /dir-glob@3.0.1:
+ resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
+ engines: {node: '>=8'}
+ dependencies:
+ path-type: 4.0.0
+ dev: true
+
+ /doctrine@3.0.0:
+ resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==}
+ engines: {node: '>=6.0.0'}
+ dependencies:
+ esutils: 2.0.3
+ dev: true
+
+ /dotenv@16.4.5:
+ resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==}
+ engines: {node: '>=12'}
+ dev: false
+
+ /eastasianwidth@0.2.0:
+ resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
+ dev: false
+
+ /emoji-regex@8.0.0:
+ resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
+ requiresBuild: true
+ dev: false
+
+ /emoji-regex@9.2.2:
+ resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
+ dev: false
+
+ /encoding@0.1.13:
+ resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==}
+ requiresBuild: true
+ dependencies:
+ iconv-lite: 0.6.3
+ dev: false
+ optional: true
+
+ /end-of-stream@1.4.4:
+ resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==}
+ dependencies:
+ once: 1.4.0
+ dev: false
+
+ /env-paths@2.2.1:
+ resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==}
+ engines: {node: '>=6'}
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /err-code@2.0.3:
+ resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==}
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /escalade@3.1.2:
+ resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==}
+ engines: {node: '>=6'}
+ dev: false
+
+ /escape-string-regexp@4.0.0:
+ resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
+ engines: {node: '>=10'}
+ dev: true
+
+ /eslint-scope@7.2.2:
+ resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+ dependencies:
+ esrecurse: 4.3.0
+ estraverse: 5.3.0
+ dev: true
+
+ /eslint-visitor-keys@3.4.3:
+ resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+ dev: true
+
+ /eslint@8.57.0:
+ resolution: {integrity: sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+ hasBin: true
+ dependencies:
+ '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0)
+ '@eslint-community/regexpp': 4.10.0
+ '@eslint/eslintrc': 2.1.4
+ '@eslint/js': 8.57.0
+ '@humanwhocodes/config-array': 0.11.14
+ '@humanwhocodes/module-importer': 1.0.1
+ '@nodelib/fs.walk': 1.2.8
+ '@ungap/structured-clone': 1.2.0
+ ajv: 6.12.6
+ chalk: 4.1.2
+ cross-spawn: 7.0.3
+ debug: 4.3.4
+ doctrine: 3.0.0
+ escape-string-regexp: 4.0.0
+ eslint-scope: 7.2.2
+ eslint-visitor-keys: 3.4.3
+ espree: 9.6.1
+ esquery: 1.5.0
+ esutils: 2.0.3
+ fast-deep-equal: 3.1.3
+ file-entry-cache: 6.0.1
+ find-up: 5.0.0
+ glob-parent: 6.0.2
+ globals: 13.24.0
+ graphemer: 1.4.0
+ ignore: 5.3.1
+ imurmurhash: 0.1.4
+ is-glob: 4.0.3
+ is-path-inside: 3.0.3
+ js-yaml: 4.1.0
+ json-stable-stringify-without-jsonify: 1.0.1
+ levn: 0.4.1
+ lodash.merge: 4.6.2
+ minimatch: 3.1.2
+ natural-compare: 1.4.0
+ optionator: 0.9.3
+ strip-ansi: 6.0.1
+ text-table: 0.2.0
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /espree@9.6.1:
+ resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+ dependencies:
+ acorn: 8.11.3
+ acorn-jsx: 5.3.2(acorn@8.11.3)
+ eslint-visitor-keys: 3.4.3
+ dev: true
+
+ /esquery@1.5.0:
+ resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==}
+ engines: {node: '>=0.10'}
+ dependencies:
+ estraverse: 5.3.0
+ dev: true
+
+ /esrecurse@4.3.0:
+ resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==}
+ engines: {node: '>=4.0'}
+ dependencies:
+ estraverse: 5.3.0
+ dev: true
+
+ /estraverse@5.3.0:
+ resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==}
+ engines: {node: '>=4.0'}
+ dev: true
+
+ /esutils@2.0.3:
+ resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
+ engines: {node: '>=0.10.0'}
+ dev: true
+
+ /expand-template@2.0.3:
+ resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==}
+ engines: {node: '>=6'}
+ dev: false
+
+ /fast-deep-equal@3.1.3:
+ resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
+ dev: true
+
+ /fast-glob@3.3.2:
+ resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==}
+ engines: {node: '>=8.6.0'}
+ dependencies:
+ '@nodelib/fs.stat': 2.0.5
+ '@nodelib/fs.walk': 1.2.8
+ glob-parent: 5.1.2
+ merge2: 1.4.1
+ micromatch: 4.0.5
+ dev: true
+
+ /fast-json-stable-stringify@2.1.0:
+ resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
+ dev: true
+
+ /fast-levenshtein@2.0.6:
+ resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
+ dev: true
+
+ /fastq@1.17.1:
+ resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==}
+ dependencies:
+ reusify: 1.0.4
+ dev: true
+
+ /file-entry-cache@6.0.1:
+ resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==}
+ engines: {node: ^10.12.0 || >=12.0.0}
+ dependencies:
+ flat-cache: 3.2.0
+ dev: true
+
+ /file-uri-to-path@1.0.0:
+ resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==}
+ dev: false
+
+ /fill-range@7.0.1:
+ resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==}
+ engines: {node: '>=8'}
+ dependencies:
+ to-regex-range: 5.0.1
+ dev: true
+
+ /find-up@5.0.0:
+ resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
+ engines: {node: '>=10'}
+ dependencies:
+ locate-path: 6.0.0
+ path-exists: 4.0.0
+ dev: true
+
+ /flat-cache@3.2.0:
+ resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==}
+ engines: {node: ^10.12.0 || >=12.0.0}
+ dependencies:
+ flatted: 3.3.1
+ keyv: 4.5.4
+ rimraf: 3.0.2
+ dev: true
+
+ /flatted@3.3.1:
+ resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==}
+ dev: true
+
+ /foreground-child@3.1.1:
+ resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==}
+ engines: {node: '>=14'}
+ dependencies:
+ cross-spawn: 7.0.3
+ signal-exit: 4.1.0
+ dev: false
+
+ /fs-constants@1.0.0:
+ resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
+ dev: false
+
+ /fs-minipass@2.1.0:
+ resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==}
+ engines: {node: '>= 8'}
+ dependencies:
+ minipass: 3.3.6
+ dev: false
+
+ /fs.realpath@1.0.0:
+ resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
+ requiresBuild: true
+
+ /gauge@4.0.4:
+ resolution: {integrity: sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==}
+ engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
+ requiresBuild: true
+ dependencies:
+ aproba: 2.0.0
+ color-support: 1.1.3
+ console-control-strings: 1.1.0
+ has-unicode: 2.0.1
+ signal-exit: 3.0.7
+ string-width: 4.2.3
+ strip-ansi: 6.0.1
+ wide-align: 1.1.5
+ dev: false
+ optional: true
+
+ /generate-function@2.3.1:
+ resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==}
+ dependencies:
+ is-property: 1.0.2
+ dev: false
+
+ /get-caller-file@2.0.5:
+ resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
+ engines: {node: 6.* || 8.* || >= 10.*}
+ dev: false
+
+ /github-from-package@0.0.0:
+ resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==}
+ dev: false
+
+ /glob-parent@5.1.2:
+ resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
+ engines: {node: '>= 6'}
+ dependencies:
+ is-glob: 4.0.3
+ dev: true
+
+ /glob-parent@6.0.2:
+ resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
+ engines: {node: '>=10.13.0'}
+ dependencies:
+ is-glob: 4.0.3
+ dev: true
+
+ /glob@10.3.10:
+ resolution: {integrity: sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==}
+ engines: {node: '>=16 || 14 >=14.17'}
+ hasBin: true
+ dependencies:
+ foreground-child: 3.1.1
+ jackspeak: 2.3.6
+ minimatch: 9.0.3
+ minipass: 7.0.4
+ path-scurry: 1.10.1
+ dev: false
+
+ /glob@7.2.3:
+ resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
+ requiresBuild: true
+ dependencies:
+ fs.realpath: 1.0.0
+ inflight: 1.0.6
+ inherits: 2.0.4
+ minimatch: 3.1.2
+ once: 1.4.0
+ path-is-absolute: 1.0.1
+
+ /globals@13.24.0:
+ resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==}
+ engines: {node: '>=8'}
+ dependencies:
+ type-fest: 0.20.2
+ dev: true
+
+ /globby@11.1.0:
+ resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==}
+ engines: {node: '>=10'}
+ dependencies:
+ array-union: 2.1.0
+ dir-glob: 3.0.1
+ fast-glob: 3.3.2
+ ignore: 5.3.1
+ merge2: 1.4.1
+ slash: 3.0.0
+ dev: true
+
+ /graceful-fs@4.2.11:
+ resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /graphemer@1.4.0:
+ resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==}
+ dev: true
+
+ /has-flag@4.0.0:
+ resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
+ engines: {node: '>=8'}
+
+ /has-unicode@2.0.1:
+ resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==}
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /highlight.js@10.7.3:
+ resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==}
+ dev: false
+
+ /http-cache-semantics@4.1.1:
+ resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==}
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /http-proxy-agent@4.0.1:
+ resolution: {integrity: sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==}
+ engines: {node: '>= 6'}
+ requiresBuild: true
+ dependencies:
+ '@tootallnate/once': 1.1.2
+ agent-base: 6.0.2
+ debug: 4.3.4
+ transitivePeerDependencies:
+ - supports-color
+ dev: false
+ optional: true
+
+ /https-proxy-agent@5.0.1:
+ resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==}
+ engines: {node: '>= 6'}
+ requiresBuild: true
+ dependencies:
+ agent-base: 6.0.2
+ debug: 4.3.4
+ transitivePeerDependencies:
+ - supports-color
+ dev: false
+ optional: true
+
+ /humanize-ms@1.2.1:
+ resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==}
+ requiresBuild: true
+ dependencies:
+ ms: 2.1.3
+ dev: false
+ optional: true
+
+ /iconv-lite@0.6.3:
+ resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
+ engines: {node: '>=0.10.0'}
+ dependencies:
+ safer-buffer: 2.1.2
+ dev: false
+
+ /ieee754@1.2.1:
+ resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
+ dev: false
+
+ /ignore@5.3.1:
+ resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==}
+ engines: {node: '>= 4'}
+ dev: true
+
+ /import-fresh@3.3.0:
+ resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==}
+ engines: {node: '>=6'}
+ dependencies:
+ parent-module: 1.0.1
+ resolve-from: 4.0.0
+ dev: true
+
+ /imurmurhash@0.1.4:
+ resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
+ engines: {node: '>=0.8.19'}
+
+ /indent-string@4.0.0:
+ resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==}
+ engines: {node: '>=8'}
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /infer-owner@1.0.4:
+ resolution: {integrity: sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==}
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /inflight@1.0.6:
+ resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
+ requiresBuild: true
+ dependencies:
+ once: 1.4.0
+ wrappy: 1.0.2
+
+ /inherits@2.0.4:
+ resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
+
+ /ini@1.3.8:
+ resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==}
+ dev: false
+
+ /ip-address@9.0.5:
+ resolution: {integrity: sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==}
+ engines: {node: '>= 12'}
+ dependencies:
+ jsbn: 1.1.0
+ sprintf-js: 1.1.3
+ dev: false
+
+ /is-extglob@2.1.1:
+ resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
+ engines: {node: '>=0.10.0'}
+ dev: true
+
+ /is-fullwidth-code-point@3.0.0:
+ resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
+ engines: {node: '>=8'}
+ requiresBuild: true
+ dev: false
+
+ /is-glob@4.0.3:
+ resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
+ engines: {node: '>=0.10.0'}
+ dependencies:
+ is-extglob: 2.1.1
+ dev: true
+
+ /is-lambda@1.0.1:
+ resolution: {integrity: sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==}
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /is-number@7.0.0:
+ resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
+ engines: {node: '>=0.12.0'}
+ dev: true
+
+ /is-path-inside@3.0.3:
+ resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==}
+ engines: {node: '>=8'}
+ dev: true
+
+ /is-property@1.0.2:
+ resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==}
+ dev: false
+
+ /isexe@2.0.0:
+ resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
+ requiresBuild: true
+
+ /jackspeak@2.3.6:
+ resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==}
+ engines: {node: '>=14'}
+ dependencies:
+ '@isaacs/cliui': 8.0.2
+ optionalDependencies:
+ '@pkgjs/parseargs': 0.11.0
+ dev: false
+
+ /js-yaml@4.1.0:
+ resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
+ hasBin: true
+ dependencies:
+ argparse: 2.0.1
+ dev: true
+
+ /jsbn@1.1.0:
+ resolution: {integrity: sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==}
+ dev: false
+
+ /json-buffer@3.0.1:
+ resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
+ dev: true
+
+ /json-schema-traverse@0.4.1:
+ resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
+ dev: true
+
+ /json-stable-stringify-without-jsonify@1.0.1:
+ resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
+ dev: true
+
+ /jsonc-parser@3.2.1:
+ resolution: {integrity: sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==}
+ dev: true
+
+ /keyv@4.5.4:
+ resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
+ dependencies:
+ json-buffer: 3.0.1
+ dev: true
+
+ /levn@0.4.1:
+ resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
+ engines: {node: '>= 0.8.0'}
+ dependencies:
+ prelude-ls: 1.2.1
+ type-check: 0.4.0
+ dev: true
+
+ /locate-path@6.0.0:
+ resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
+ engines: {node: '>=10'}
+ dependencies:
+ p-locate: 5.0.0
+ dev: true
+
+ /lodash.merge@4.6.2:
+ resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
+ dev: true
+
+ /long@5.2.3:
+ resolution: {integrity: sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==}
+ dev: false
+
+ /lru-cache@10.2.0:
+ resolution: {integrity: sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==}
+ engines: {node: 14 || >=16.14}
+ dev: false
+
+ /lru-cache@6.0.0:
+ resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==}
+ engines: {node: '>=10'}
+ dependencies:
+ yallist: 4.0.0
+
+ /lru-cache@7.18.3:
+ resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==}
+ engines: {node: '>=12'}
+ dev: false
+
+ /lru-cache@8.0.5:
+ resolution: {integrity: sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==}
+ engines: {node: '>=16.14'}
+ dev: false
+
+ /lunr@2.3.9:
+ resolution: {integrity: sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==}
+ dev: true
+
+ /make-error@1.3.6:
+ resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==}
+
+ /make-fetch-happen@9.1.0:
+ resolution: {integrity: sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==}
+ engines: {node: '>= 10'}
+ requiresBuild: true
+ dependencies:
+ agentkeepalive: 4.5.0
+ cacache: 15.3.0
+ http-cache-semantics: 4.1.1
+ http-proxy-agent: 4.0.1
+ https-proxy-agent: 5.0.1
+ is-lambda: 1.0.1
+ lru-cache: 6.0.0
+ minipass: 3.3.6
+ minipass-collect: 1.0.2
+ minipass-fetch: 1.4.1
+ minipass-flush: 1.0.5
+ minipass-pipeline: 1.2.4
+ negotiator: 0.6.3
+ promise-retry: 2.0.1
+ socks-proxy-agent: 6.2.1
+ ssri: 8.0.1
+ transitivePeerDependencies:
+ - bluebird
+ - supports-color
+ dev: false
+ optional: true
+
+ /marked@4.3.0:
+ resolution: {integrity: sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==}
+ engines: {node: '>= 12'}
+ hasBin: true
+ dev: true
+
+ /memory-pager@1.5.0:
+ resolution: {integrity: sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==}
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /merge2@1.4.1:
+ resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
+ engines: {node: '>= 8'}
+ dev: true
+
+ /micromatch@4.0.5:
+ resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==}
+ engines: {node: '>=8.6'}
+ dependencies:
+ braces: 3.0.2
+ picomatch: 2.3.1
+ dev: true
+
+ /mimic-response@3.1.0:
+ resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==}
+ engines: {node: '>=10'}
+ dev: false
+
+ /minimatch@3.1.2:
+ resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
+ dependencies:
+ brace-expansion: 1.1.11
+
+ /minimatch@9.0.3:
+ resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==}
+ engines: {node: '>=16 || 14 >=14.17'}
+ dependencies:
+ brace-expansion: 2.0.1
+
+ /minimist@1.2.8:
+ resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
+ dev: false
+
+ /minipass-collect@1.0.2:
+ resolution: {integrity: sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==}
+ engines: {node: '>= 8'}
+ requiresBuild: true
+ dependencies:
+ minipass: 3.3.6
+ dev: false
+ optional: true
+
+ /minipass-fetch@1.4.1:
+ resolution: {integrity: sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==}
+ engines: {node: '>=8'}
+ requiresBuild: true
+ dependencies:
+ minipass: 3.3.6
+ minipass-sized: 1.0.3
+ minizlib: 2.1.2
+ optionalDependencies:
+ encoding: 0.1.13
+ dev: false
+ optional: true
+
+ /minipass-flush@1.0.5:
+ resolution: {integrity: sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==}
+ engines: {node: '>= 8'}
+ requiresBuild: true
+ dependencies:
+ minipass: 3.3.6
+ dev: false
+ optional: true
+
+ /minipass-pipeline@1.2.4:
+ resolution: {integrity: sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==}
+ engines: {node: '>=8'}
+ requiresBuild: true
+ dependencies:
+ minipass: 3.3.6
+ dev: false
+ optional: true
+
+ /minipass-sized@1.0.3:
+ resolution: {integrity: sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==}
+ engines: {node: '>=8'}
+ requiresBuild: true
+ dependencies:
+ minipass: 3.3.6
+ dev: false
+ optional: true
+
+ /minipass@3.3.6:
+ resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==}
+ engines: {node: '>=8'}
+ dependencies:
+ yallist: 4.0.0
+ dev: false
+
+ /minipass@5.0.0:
+ resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==}
+ engines: {node: '>=8'}
+ dev: false
+
+ /minipass@7.0.4:
+ resolution: {integrity: sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==}
+ engines: {node: '>=16 || 14 >=14.17'}
+ dev: false
+
+ /minizlib@2.1.2:
+ resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==}
+ engines: {node: '>= 8'}
+ dependencies:
+ minipass: 3.3.6
+ yallist: 4.0.0
+ dev: false
+
+ /mkdirp-classic@0.5.3:
+ resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==}
+ dev: false
+
+ /mkdirp@1.0.4:
+ resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==}
+ engines: {node: '>=10'}
+ hasBin: true
+ dev: false
+
+ /mkdirp@2.1.6:
+ resolution: {integrity: sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A==}
+ engines: {node: '>=10'}
+ hasBin: true
+ dev: false
+
+ /mongodb-connection-string-url@2.6.0:
+ resolution: {integrity: sha512-WvTZlI9ab0QYtTYnuMLgobULWhokRjtC7db9LtcVfJ+Hsnyr5eo6ZtNAt3Ly24XZScGMelOcGtm7lSn0332tPQ==}
+ dependencies:
+ '@types/whatwg-url': 8.2.2
+ whatwg-url: 11.0.0
+ dev: false
+
+ /mongodb@5.9.2:
+ resolution: {integrity: sha512-H60HecKO4Bc+7dhOv4sJlgvenK4fQNqqUIlXxZYQNbfEWSALGAwGoyJd/0Qwk4TttFXUOHJ2ZJQe/52ScaUwtQ==}
+ engines: {node: '>=14.20.1'}
+ peerDependencies:
+ '@aws-sdk/credential-providers': ^3.188.0
+ '@mongodb-js/zstd': ^1.0.0
+ kerberos: ^1.0.0 || ^2.0.0
+ mongodb-client-encryption: '>=2.3.0 <3'
+ snappy: ^7.2.2
+ peerDependenciesMeta:
+ '@aws-sdk/credential-providers':
+ optional: true
+ '@mongodb-js/zstd':
+ optional: true
+ kerberos:
+ optional: true
+ mongodb-client-encryption:
+ optional: true
+ snappy:
+ optional: true
+ dependencies:
+ bson: 5.5.1
+ mongodb-connection-string-url: 2.6.0
+ socks: 2.8.1
+ optionalDependencies:
+ '@mongodb-js/saslprep': 1.1.5
+ dev: false
+
+ /ms@2.1.2:
+ resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
+
+ /ms@2.1.3:
+ resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /mysql2@3.9.2:
+ resolution: {integrity: sha512-3Cwg/UuRkAv/wm6RhtPE5L7JlPB877vwSF6gfLAS68H+zhH+u5oa3AieqEd0D0/kC3W7qIhYbH419f7O9i/5nw==}
+ engines: {node: '>= 8.0'}
+ dependencies:
+ denque: 2.1.0
+ generate-function: 2.3.1
+ iconv-lite: 0.6.3
+ long: 5.2.3
+ lru-cache: 8.0.5
+ named-placeholders: 1.1.3
+ seq-queue: 0.0.5
+ sqlstring: 2.3.3
+ dev: false
+
+ /mz@2.7.0:
+ resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}
+ dependencies:
+ any-promise: 1.3.0
+ object-assign: 4.1.1
+ thenify-all: 1.6.0
+ dev: false
+
+ /named-placeholders@1.1.3:
+ resolution: {integrity: sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==}
+ engines: {node: '>=12.0.0'}
+ dependencies:
+ lru-cache: 7.18.3
+ dev: false
+
+ /napi-build-utils@1.0.2:
+ resolution: {integrity: sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==}
+ dev: false
+
+ /natural-compare@1.4.0:
+ resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
+ dev: true
+
+ /negotiator@0.6.3:
+ resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
+ engines: {node: '>= 0.6'}
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /neo4j-driver-bolt-connection@5.18.0:
+ resolution: {integrity: sha512-Oc0w4V1sFzy6b1Mvsojz2mcDI0Caqv/jsb61DY7s9i0BHMglkNfMR1GJHjRzC6HaeDU85EFw1RLNF0NJmCSzpg==}
+ dependencies:
+ buffer: 6.0.3
+ neo4j-driver-core: 5.18.0
+ string_decoder: 1.3.0
+ dev: false
+
+ /neo4j-driver-core@5.18.0:
+ resolution: {integrity: sha512-naq5zT5tYazh81CO28L5YrcL/Wy4NoppqawkE5zyfFFVEs3bhmGn7G170FL0Fs8h7Dab1aZ5hlOBwlXXVWSDng==}
+ dev: false
+
+ /neo4j-driver@5.18.0:
+ resolution: {integrity: sha512-lDDoj45SN/FNZIYa9cTesHTAVMrcPeWmgkrYiYGTVBfJ3yKk2m2G7+lpmfip6NN8H+A0C4NIqfmMHtcC4eflyw==}
+ dependencies:
+ neo4j-driver-bolt-connection: 5.18.0
+ neo4j-driver-core: 5.18.0
+ rxjs: 7.8.1
+ dev: false
+
+ /neogma@1.13.0:
+ resolution: {integrity: sha512-VPr5XUiQlsDaBJyCdfAWkEG6mN0Z6F5govb/H7h16FXSARC7pVubzZ8CioTmJ/jXCto5Jz9J1l6cifoZhi9/Aw==}
+ dependencies:
+ '@types/revalidator': 0.3.12
+ clone: 2.1.2
+ dotenv: 16.4.5
+ neo4j-driver: 5.18.0
+ revalidator: 0.3.1
+ uuid: 9.0.1
+ dev: false
+
+ /node-abi@3.56.0:
+ resolution: {integrity: sha512-fZjdhDOeRcaS+rcpve7XuwHBmktS1nS1gzgghwKUQQ8nTy2FdSDr6ZT8k6YhvlJeHmmQMYiT/IH9hfco5zeW2Q==}
+ engines: {node: '>=10'}
+ dependencies:
+ semver: 7.6.0
+ dev: false
+
+ /node-addon-api@7.1.0:
+ resolution: {integrity: sha512-mNcltoe1R8o7STTegSOHdnJNN7s5EUvhoS7ShnTHDyOSd+8H+UdWODq6qSv67PjC8Zc5JRT8+oLAMCr0SIXw7g==}
+ engines: {node: ^16 || ^18 || >= 20}
+ dev: false
+
+ /node-gyp@8.4.1:
+ resolution: {integrity: sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==}
+ engines: {node: '>= 10.12.0'}
+ hasBin: true
+ requiresBuild: true
+ dependencies:
+ env-paths: 2.2.1
+ glob: 7.2.3
+ graceful-fs: 4.2.11
+ make-fetch-happen: 9.1.0
+ nopt: 5.0.0
+ npmlog: 6.0.2
+ rimraf: 3.0.2
+ semver: 7.6.0
+ tar: 6.2.0
+ which: 2.0.2
+ transitivePeerDependencies:
+ - bluebird
+ - supports-color
+ dev: false
+ optional: true
+
+ /nopt@5.0.0:
+ resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==}
+ engines: {node: '>=6'}
+ hasBin: true
+ requiresBuild: true
+ dependencies:
+ abbrev: 1.1.1
+ dev: false
+ optional: true
+
+ /npmlog@6.0.2:
+ resolution: {integrity: sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==}
+ engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
+ requiresBuild: true
+ dependencies:
+ are-we-there-yet: 3.0.1
+ console-control-strings: 1.1.0
+ gauge: 4.0.4
+ set-blocking: 2.0.0
+ dev: false
+ optional: true
+
+ /object-assign@4.1.1:
+ resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
+ engines: {node: '>=0.10.0'}
+ dev: false
+
+ /once@1.4.0:
+ resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
+ dependencies:
+ wrappy: 1.0.2
+
+ /optionator@0.9.3:
+ resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==}
+ engines: {node: '>= 0.8.0'}
+ dependencies:
+ '@aashutoshrathi/word-wrap': 1.2.6
+ deep-is: 0.1.4
+ fast-levenshtein: 2.0.6
+ levn: 0.4.1
+ prelude-ls: 1.2.1
+ type-check: 0.4.0
+ dev: true
+
+ /p-limit@3.1.0:
+ resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
+ engines: {node: '>=10'}
+ dependencies:
+ yocto-queue: 0.1.0
+ dev: true
+
+ /p-locate@5.0.0:
+ resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
+ engines: {node: '>=10'}
+ dependencies:
+ p-limit: 3.1.0
+ dev: true
+
+ /p-map@4.0.0:
+ resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==}
+ engines: {node: '>=10'}
+ requiresBuild: true
+ dependencies:
+ aggregate-error: 3.1.0
+ dev: false
+ optional: true
+
+ /parent-module@1.0.1:
+ resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
+ engines: {node: '>=6'}
+ dependencies:
+ callsites: 3.1.0
+ dev: true
+
+ /parse5-htmlparser2-tree-adapter@6.0.1:
+ resolution: {integrity: sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==}
+ dependencies:
+ parse5: 6.0.1
+ dev: false
+
+ /parse5@5.1.1:
+ resolution: {integrity: sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==}
+ dev: false
+
+ /parse5@6.0.1:
+ resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==}
+ dev: false
+
+ /path-exists@4.0.0:
+ resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
+ engines: {node: '>=8'}
+ dev: true
+
+ /path-is-absolute@1.0.1:
+ resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==}
+ engines: {node: '>=0.10.0'}
+ requiresBuild: true
+
+ /path-key@3.1.1:
+ resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
+ engines: {node: '>=8'}
+
+ /path-scurry@1.10.1:
+ resolution: {integrity: sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==}
+ engines: {node: '>=16 || 14 >=14.17'}
+ dependencies:
+ lru-cache: 10.2.0
+ minipass: 7.0.4
+ dev: false
+
+ /path-type@4.0.0:
+ resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
+ engines: {node: '>=8'}
+ dev: true
+
+ /picomatch@2.3.1:
+ resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
+ engines: {node: '>=8.6'}
+ dev: true
+
+ /prebuild-install@7.1.2:
+ resolution: {integrity: sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==}
+ engines: {node: '>=10'}
+ hasBin: true
+ dependencies:
+ detect-libc: 2.0.2
+ expand-template: 2.0.3
+ github-from-package: 0.0.0
+ minimist: 1.2.8
+ mkdirp-classic: 0.5.3
+ napi-build-utils: 1.0.2
+ node-abi: 3.56.0
+ pump: 3.0.0
+ rc: 1.2.8
+ simple-get: 4.0.1
+ tar-fs: 2.1.1
+ tunnel-agent: 0.6.0
+ dev: false
+
+ /prelude-ls@1.2.1:
+ resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
+ engines: {node: '>= 0.8.0'}
+ dev: true
+
+ /promise-inflight@1.0.1:
+ resolution: {integrity: sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==}
+ requiresBuild: true
+ peerDependencies:
+ bluebird: '*'
+ peerDependenciesMeta:
+ bluebird:
+ optional: true
+ dev: false
+ optional: true
+
+ /promise-retry@2.0.1:
+ resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==}
+ engines: {node: '>=10'}
+ requiresBuild: true
+ dependencies:
+ err-code: 2.0.3
+ retry: 0.12.0
+ dev: false
+ optional: true
+
+ /pump@3.0.0:
+ resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==}
+ dependencies:
+ end-of-stream: 1.4.4
+ once: 1.4.0
+ dev: false
+
+ /punycode@2.3.1:
+ resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
+ engines: {node: '>=6'}
+
+ /queue-microtask@1.2.3:
+ resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
+ dev: true
+
+ /rc@1.2.8:
+ resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
+ hasBin: true
+ dependencies:
+ deep-extend: 0.6.0
+ ini: 1.3.8
+ minimist: 1.2.8
+ strip-json-comments: 2.0.1
+ dev: false
+
+ /readable-stream@3.6.2:
+ resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
+ engines: {node: '>= 6'}
+ dependencies:
+ inherits: 2.0.4
+ string_decoder: 1.3.0
+ util-deprecate: 1.0.2
+ dev: false
+
+ /reflect-metadata@0.1.14:
+ resolution: {integrity: sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A==}
+ dev: false
+
+ /reflect-metadata@0.2.1:
+ resolution: {integrity: sha512-i5lLI6iw9AU3Uu4szRNPPEkomnkjRTaVt9hy/bn5g/oSzekBSMeLZblcjP74AW0vBabqERLLIrz+gR8QYR54Tw==}
+ dev: false
+
+ /require-directory@2.1.1:
+ resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
+ engines: {node: '>=0.10.0'}
+ dev: false
+
+ /resolve-from@4.0.0:
+ resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
+ engines: {node: '>=4'}
+ dev: true
+
+ /retry@0.12.0:
+ resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==}
+ engines: {node: '>= 4'}
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /reusify@1.0.4:
+ resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==}
+ engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
+ dev: true
+
+ /revalidator@0.3.1:
+ resolution: {integrity: sha512-orq+Nw+V5pDpQwGEuN2n1AgJ+0A8WqhFHKt5KgkxfAowUKgO1CWV32IR3TNB4g9/FX3gJt9qBJO8DYlwonnB0Q==}
+ engines: {node: '>= 0.8.0'}
+ dev: false
+
+ /rimraf@3.0.2:
+ resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==}
+ hasBin: true
+ dependencies:
+ glob: 7.2.3
+
+ /run-parallel@1.2.0:
+ resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
+ dependencies:
+ queue-microtask: 1.2.3
+ dev: true
+
+ /rxjs@7.8.1:
+ resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==}
+ dependencies:
+ tslib: 2.6.2
+ dev: false
+
+ /safe-buffer@5.2.1:
+ resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
+ dev: false
+
+ /safer-buffer@2.1.2:
+ resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
+ dev: false
+
+ /semver@7.6.0:
+ resolution: {integrity: sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==}
+ engines: {node: '>=10'}
+ hasBin: true
+ dependencies:
+ lru-cache: 6.0.0
+
+ /seq-queue@0.0.5:
+ resolution: {integrity: sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==}
+ dev: false
+
+ /set-blocking@2.0.0:
+ resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /sha.js@2.4.11:
+ resolution: {integrity: sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==}
+ hasBin: true
+ dependencies:
+ inherits: 2.0.4
+ safe-buffer: 5.2.1
+ dev: false
+
+ /shebang-command@2.0.0:
+ resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
+ engines: {node: '>=8'}
+ dependencies:
+ shebang-regex: 3.0.0
+
+ /shebang-regex@3.0.0:
+ resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
+ engines: {node: '>=8'}
+
+ /shiki@0.14.7:
+ resolution: {integrity: sha512-dNPAPrxSc87ua2sKJ3H5dQ/6ZaY8RNnaAqK+t0eG7p0Soi2ydiqbGOTaZCqaYvA/uZYfS1LJnemt3Q+mSfcPCg==}
+ dependencies:
+ ansi-sequence-parser: 1.1.1
+ jsonc-parser: 3.2.1
+ vscode-oniguruma: 1.7.0
+ vscode-textmate: 8.0.0
+ dev: true
+
+ /signal-exit@3.0.7:
+ resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /signal-exit@4.1.0:
+ resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
+ engines: {node: '>=14'}
+ dev: false
+
+ /simple-concat@1.0.1:
+ resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==}
+ dev: false
+
+ /simple-get@4.0.1:
+ resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==}
+ dependencies:
+ decompress-response: 6.0.0
+ once: 1.4.0
+ simple-concat: 1.0.1
+ dev: false
+
+ /slash@3.0.0:
+ resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
+ engines: {node: '>=8'}
+ dev: true
+
+ /smart-buffer@4.2.0:
+ resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==}
+ engines: {node: '>= 6.0.0', npm: '>= 3.0.0'}
+ dev: false
+
+ /socks-proxy-agent@6.2.1:
+ resolution: {integrity: sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==}
+ engines: {node: '>= 10'}
+ requiresBuild: true
+ dependencies:
+ agent-base: 6.0.2
+ debug: 4.3.4
+ socks: 2.8.1
+ transitivePeerDependencies:
+ - supports-color
+ dev: false
+ optional: true
+
+ /socks@2.8.1:
+ resolution: {integrity: sha512-B6w7tkwNid7ToxjZ08rQMT8M9BJAf8DKx8Ft4NivzH0zBUfd6jldGcisJn/RLgxcX3FPNDdNQCUEMMT79b+oCQ==}
+ engines: {node: '>= 10.0.0', npm: '>= 3.0.0'}
+ dependencies:
+ ip-address: 9.0.5
+ smart-buffer: 4.2.0
+ dev: false
+
+ /sparse-bitfield@3.0.3:
+ resolution: {integrity: sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==}
+ requiresBuild: true
+ dependencies:
+ memory-pager: 1.5.0
+ dev: false
+ optional: true
+
+ /sprintf-js@1.1.3:
+ resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==}
+ dev: false
+
+ /sqlite3@5.1.7:
+ resolution: {integrity: sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog==}
+ requiresBuild: true
+ dependencies:
+ bindings: 1.5.0
+ node-addon-api: 7.1.0
+ prebuild-install: 7.1.2
+ tar: 6.2.0
+ optionalDependencies:
+ node-gyp: 8.4.1
+ transitivePeerDependencies:
+ - bluebird
+ - supports-color
+ dev: false
+
+ /sqlstring@2.3.3:
+ resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==}
+ engines: {node: '>= 0.6'}
+ dev: false
+
+ /ssri@8.0.1:
+ resolution: {integrity: sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==}
+ engines: {node: '>= 8'}
+ requiresBuild: true
+ dependencies:
+ minipass: 3.3.6
+ dev: false
+ optional: true
+
+ /string-width@4.2.3:
+ resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
+ engines: {node: '>=8'}
+ dependencies:
+ emoji-regex: 8.0.0
+ is-fullwidth-code-point: 3.0.0
+ strip-ansi: 6.0.1
+ dev: false
+
+ /string-width@5.1.2:
+ resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
+ engines: {node: '>=12'}
+ dependencies:
+ eastasianwidth: 0.2.0
+ emoji-regex: 9.2.2
+ strip-ansi: 7.1.0
+ dev: false
+
+ /string_decoder@1.3.0:
+ resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
+ dependencies:
+ safe-buffer: 5.2.1
+ dev: false
+
+ /strip-ansi@6.0.1:
+ resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
+ engines: {node: '>=8'}
+ dependencies:
+ ansi-regex: 5.0.1
+
+ /strip-ansi@7.1.0:
+ resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==}
+ engines: {node: '>=12'}
+ dependencies:
+ ansi-regex: 6.0.1
+ dev: false
+
+ /strip-json-comments@2.0.1:
+ resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==}
+ engines: {node: '>=0.10.0'}
+ dev: false
+
+ /strip-json-comments@3.1.1:
+ resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
+ engines: {node: '>=8'}
+ dev: true
+
+ /supports-color@7.2.0:
+ resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
+ engines: {node: '>=8'}
+ dependencies:
+ has-flag: 4.0.0
+
+ /tar-fs@2.1.1:
+ resolution: {integrity: sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==}
+ dependencies:
+ chownr: 1.1.4
+ mkdirp-classic: 0.5.3
+ pump: 3.0.0
+ tar-stream: 2.2.0
+ dev: false
+
+ /tar-stream@2.2.0:
+ resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==}
+ engines: {node: '>=6'}
+ dependencies:
+ bl: 4.1.0
+ end-of-stream: 1.4.4
+ fs-constants: 1.0.0
+ inherits: 2.0.4
+ readable-stream: 3.6.2
+ dev: false
+
+ /tar@6.2.0:
+ resolution: {integrity: sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==}
+ engines: {node: '>=10'}
+ dependencies:
+ chownr: 2.0.0
+ fs-minipass: 2.1.0
+ minipass: 5.0.0
+ minizlib: 2.1.2
+ mkdirp: 1.0.4
+ yallist: 4.0.0
+ dev: false
+
+ /text-table@0.2.0:
+ resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
+ dev: true
+
+ /thenify-all@1.6.0:
+ resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==}
+ engines: {node: '>=0.8'}
+ dependencies:
+ thenify: 3.3.1
+ dev: false
+
+ /thenify@3.3.1:
+ resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==}
+ dependencies:
+ any-promise: 1.3.0
+ dev: false
+
+ /to-regex-range@5.0.1:
+ resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
+ engines: {node: '>=8.0'}
+ dependencies:
+ is-number: 7.0.0
+ dev: true
+
+ /tr46@3.0.0:
+ resolution: {integrity: sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==}
+ engines: {node: '>=12'}
+ dependencies:
+ punycode: 2.3.1
+ dev: false
+
+ /ts-api-utils@1.3.0(typescript@5.4.2):
+ resolution: {integrity: sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==}
+ engines: {node: '>=16'}
+ peerDependencies:
+ typescript: '>=4.2.0'
+ dependencies:
+ typescript: 5.4.2
+ dev: true
+
+ /ts-node@10.9.2(@types/node@20.11.26)(typescript@5.4.2):
+ resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==}
+ hasBin: true
+ peerDependencies:
+ '@swc/core': '>=1.2.50'
+ '@swc/wasm': '>=1.2.50'
+ '@types/node': '*'
+ typescript: '>=2.7'
+ peerDependenciesMeta:
+ '@swc/core':
+ optional: true
+ '@swc/wasm':
+ optional: true
+ dependencies:
+ '@cspotcode/source-map-support': 0.8.1
+ '@tsconfig/node10': 1.0.9
+ '@tsconfig/node12': 1.0.11
+ '@tsconfig/node14': 1.0.3
+ '@tsconfig/node16': 1.0.4
+ '@types/node': 20.11.26
+ acorn: 8.11.3
+ acorn-walk: 8.3.2
+ arg: 4.1.3
+ create-require: 1.1.1
+ diff: 4.0.2
+ make-error: 1.3.6
+ typescript: 5.4.2
+ v8-compile-cache-lib: 3.0.1
+ yn: 3.1.1
+
+ /tslib@2.6.2:
+ resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==}
+ dev: false
+
+ /tunnel-agent@0.6.0:
+ resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==}
+ dependencies:
+ safe-buffer: 5.2.1
+ dev: false
+
+ /type-check@0.4.0:
+ resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
+ engines: {node: '>= 0.8.0'}
+ dependencies:
+ prelude-ls: 1.2.1
+ dev: true
+
+ /type-fest@0.20.2:
+ resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==}
+ engines: {node: '>=10'}
+ dev: true
+
+ /typedoc@0.25.12(typescript@5.4.2):
+ resolution: {integrity: sha512-F+qhkK2VoTweDXd1c42GS/By2DvI2uDF4/EpG424dTexSHdtCH52C6IcAvMA6jR3DzAWZjHpUOW+E02kyPNUNw==}
+ engines: {node: '>= 16'}
+ hasBin: true
+ peerDependencies:
+ typescript: 4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x
+ dependencies:
+ lunr: 2.3.9
+ marked: 4.3.0
+ minimatch: 9.0.3
+ shiki: 0.14.7
+ typescript: 5.4.2
+ dev: true
+
+ /typeorm@0.3.20(mongodb@5.9.2)(mysql2@3.9.2)(sqlite3@5.1.7)(ts-node@10.9.2):
+ resolution: {integrity: sha512-sJ0T08dV5eoZroaq9uPKBoNcGslHBR4E4y+EBHs//SiGbblGe7IeduP/IH4ddCcj0qp3PHwDwGnuvqEAnKlq/Q==}
+ engines: {node: '>=16.13.0'}
+ hasBin: true
+ peerDependencies:
+ '@google-cloud/spanner': ^5.18.0
+ '@sap/hana-client': ^2.12.25
+ better-sqlite3: ^7.1.2 || ^8.0.0 || ^9.0.0
+ hdb-pool: ^0.1.6
+ ioredis: ^5.0.4
+ mongodb: ^5.8.0
+ mssql: ^9.1.1 || ^10.0.1
+ mysql2: ^2.2.5 || ^3.0.1
+ oracledb: ^6.3.0
+ pg: ^8.5.1
+ pg-native: ^3.0.0
+ pg-query-stream: ^4.0.0
+ redis: ^3.1.1 || ^4.0.0
+ sql.js: ^1.4.0
+ sqlite3: ^5.0.3
+ ts-node: ^10.7.0
+ typeorm-aurora-data-api-driver: ^2.0.0
+ peerDependenciesMeta:
+ '@google-cloud/spanner':
+ optional: true
+ '@sap/hana-client':
+ optional: true
+ better-sqlite3:
+ optional: true
+ hdb-pool:
+ optional: true
+ ioredis:
+ optional: true
+ mongodb:
+ optional: true
+ mssql:
+ optional: true
+ mysql2:
+ optional: true
+ oracledb:
+ optional: true
+ pg:
+ optional: true
+ pg-native:
+ optional: true
+ pg-query-stream:
+ optional: true
+ redis:
+ optional: true
+ sql.js:
+ optional: true
+ sqlite3:
+ optional: true
+ ts-node:
+ optional: true
+ typeorm-aurora-data-api-driver:
+ optional: true
+ dependencies:
+ '@sqltools/formatter': 1.2.5
+ app-root-path: 3.1.0
+ buffer: 6.0.3
+ chalk: 4.1.2
+ cli-highlight: 2.1.11
+ dayjs: 1.11.10
+ debug: 4.3.4
+ dotenv: 16.4.5
+ glob: 10.3.10
+ mkdirp: 2.1.6
+ mongodb: 5.9.2
+ mysql2: 3.9.2
+ reflect-metadata: 0.2.1
+ sha.js: 2.4.11
+ sqlite3: 5.1.7
+ ts-node: 10.9.2(@types/node@20.11.26)(typescript@5.4.2)
+ tslib: 2.6.2
+ uuid: 9.0.1
+ yargs: 17.7.2
+ transitivePeerDependencies:
+ - supports-color
+ dev: false
+
+ /typescript@5.4.2:
+ resolution: {integrity: sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==}
+ engines: {node: '>=14.17'}
+ hasBin: true
+
+ /undici-types@5.26.5:
+ resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
+
+ /unique-filename@1.1.1:
+ resolution: {integrity: sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==}
+ requiresBuild: true
+ dependencies:
+ unique-slug: 2.0.2
+ dev: false
+ optional: true
+
+ /unique-slug@2.0.2:
+ resolution: {integrity: sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==}
+ requiresBuild: true
+ dependencies:
+ imurmurhash: 0.1.4
+ dev: false
+ optional: true
+
+ /uri-js@4.4.1:
+ resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
+ dependencies:
+ punycode: 2.3.1
+ dev: true
+
+ /util-deprecate@1.0.2:
+ resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
+ requiresBuild: true
+ dev: false
+
+ /uuid@9.0.1:
+ resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==}
+ hasBin: true
+ dev: false
+
+ /v8-compile-cache-lib@3.0.1:
+ resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==}
+
+ /vscode-oniguruma@1.7.0:
+ resolution: {integrity: sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==}
+ dev: true
+
+ /vscode-textmate@8.0.0:
+ resolution: {integrity: sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==}
+ dev: true
+
+ /webidl-conversions@7.0.0:
+ resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
+ engines: {node: '>=12'}
+ dev: false
+
+ /whatwg-url@11.0.0:
+ resolution: {integrity: sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==}
+ engines: {node: '>=12'}
+ dependencies:
+ tr46: 3.0.0
+ webidl-conversions: 7.0.0
+ dev: false
+
+ /which@2.0.2:
+ resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
+ engines: {node: '>= 8'}
+ hasBin: true
+ dependencies:
+ isexe: 2.0.0
+
+ /wide-align@1.1.5:
+ resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==}
+ requiresBuild: true
+ dependencies:
+ string-width: 4.2.3
+ dev: false
+ optional: true
+
+ /wrap-ansi@7.0.0:
+ resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
+ engines: {node: '>=10'}
+ dependencies:
+ ansi-styles: 4.3.0
+ string-width: 4.2.3
+ strip-ansi: 6.0.1
+ dev: false
+
+ /wrap-ansi@8.1.0:
+ resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
+ engines: {node: '>=12'}
+ dependencies:
+ ansi-styles: 6.2.1
+ string-width: 5.1.2
+ strip-ansi: 7.1.0
+ dev: false
+
+ /wrappy@1.0.2:
+ resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
+ requiresBuild: true
+
+ /y18n@5.0.8:
+ resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
+ engines: {node: '>=10'}
+ dev: false
+
+ /yallist@4.0.0:
+ resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
+
+ /yargs-parser@20.2.9:
+ resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==}
+ engines: {node: '>=10'}
+ dev: false
+
+ /yargs-parser@21.1.1:
+ resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
+ engines: {node: '>=12'}
+ dev: false
+
+ /yargs@16.2.0:
+ resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==}
+ engines: {node: '>=10'}
+ dependencies:
+ cliui: 7.0.4
+ escalade: 3.1.2
+ get-caller-file: 2.0.5
+ require-directory: 2.1.1
+ string-width: 4.2.3
+ y18n: 5.0.8
+ yargs-parser: 20.2.9
+ dev: false
+
+ /yargs@17.7.2:
+ resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
+ engines: {node: '>=12'}
+ dependencies:
+ cliui: 8.0.1
+ escalade: 3.1.2
+ get-caller-file: 2.0.5
+ require-directory: 2.1.1
+ string-width: 4.2.3
+ y18n: 5.0.8
+ yargs-parser: 21.1.1
+ dev: false
+
+ /yn@3.1.1:
+ resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==}
+ engines: {node: '>=6'}
+
+ /yocto-queue@0.1.0:
+ resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
+ engines: {node: '>=10'}
+ dev: true
diff --git a/src/db/DBWrapper.ts b/src/db/DBWrapper.ts
new file mode 100644
index 0000000..95892da
--- /dev/null
+++ b/src/db/DBWrapper.ts
@@ -0,0 +1,1175 @@
+import "reflect-metadata";
+import {
+ DataSource,
+ EntityTarget,
+ MongoRepository,
+ ObjectLiteral,
+ Repository,
+} from "typeorm";
+import UserManager, {MySQLUser, SQLiteUser, MongoUser} from "../entities/User";
+import {
+ MySQLPunto,
+ MySQLCard,
+ MySQLPuntoPlayer,
+} from "../entities/Game/MySQLGame";
+import {
+ SQLitePunto,
+ SQLiteCard,
+ SQLitePuntoPlayer,
+} from "../entities/Game/SQLiteGame";
+import MongoPunto from "../entities/Game/MongoGame";
+import {Neogma, RelationshipPropertiesI} from "neogma";
+import Result, {ResultStatus} from "./Result";
+import Neo4jManager, {Neo4jUser, NeogmaCardInstance} from "./Neo4jManager";
+import Board, {WinType} from "../game/Board";
+import Card from "../game/Card";
+import Player from "../game/Player";
+import GameManager from "../entities/Game";
+
+const MySqlDataSource = new DataSource({
+ name: "mysqlConnection",
+ type: "mysql",
+ host: "localhost",
+ port: 3306,
+ username: "root",
+ password: "root",
+ database: "punto",
+ entities: [MySQLUser, MySQLPunto, MySQLCard, MySQLPuntoPlayer],
+ synchronize: true,
+});
+
+const SQLiteDataSource = new DataSource({
+ name: "sqliteConnection",
+ type: "sqlite",
+ database: process.cwd() + "/punto.sqlite",
+ entities: [SQLiteUser, SQLitePunto, SQLiteCard, SQLitePuntoPlayer],
+ synchronize: true,
+});
+
+const MongoDataSource = new DataSource({
+ name: "mongoConnection",
+ type: "mongodb",
+ host: "localhost",
+ port: 27017,
+ database: "punto",
+ entities: [MongoUser, MongoPunto],
+ synchronize: true,
+});
+
+const NeogmaConnection = new Neogma({
+ url: "bolt://localhost:7687",
+ username: "neo4j",
+ password: "password",
+});
+
+class DBWrapper {
+ /**
+ * Instance of DBWrapper
+ * @type {DBWrapper}
+ */
+ private static _instance: DBWrapper;
+
+ /**
+ * Enum to specify which database to use
+ * @type {DBType}
+ */
+ private _dbToUse: DBType = [];
+ /**
+ * Retrive the type of connection used. If `All`, all connections are used
+ * @type {string[]}
+ */
+ public get dbToUse(): DBType {
+ return this._dbToUse;
+ }
+
+ /**
+ * Connection to the MySQL database, undefined if not initialized
+ * @type {DataSource}
+ */
+ private _MySqlConnection?: DataSource = undefined;
+ /**
+ * Connection to the MySQL database, undefined if not initialized
+ * @type {DataSource}
+ */
+ public get MySqlConnection(): DataSource | undefined {
+ return this._MySqlConnection;
+ }
+
+ /**
+ * Connection to the SQLite database, undefined if not initialized
+ * @type {DataSource}
+ */
+ private _SqliteConnection?: DataSource = undefined;
+ /**
+ * Connection to the SQLite database, undefined if not initialized
+ * @type {DataSource}
+ */
+ public get SqliteConnection(): DataSource | undefined {
+ return this._SqliteConnection;
+ }
+
+ /**
+ * Connection to the MongoDB database, undefined if not initialized
+ * @type {DataSource}
+ */
+ private _MongoConnection?: DataSource = undefined;
+ /**
+ * Connection to the MongoDB database, undefined if not initialized
+ * @type {DataSource}
+ */
+ public get MongoConnection(): DataSource | undefined {
+ return this._MongoConnection;
+ }
+
+ /**
+ * Connection to the Neo4j database, undefined if not initialized
+ * @type {Neogma}
+ */
+ private _Neo4jConnection?: Neogma = undefined;
+ /**
+ * Connection to the Neo4j database, undefined if not initialized
+ * @type {Neogma}
+ */
+ public get Neo4jConnection(): Neogma | undefined {
+ return this._Neo4jConnection;
+ }
+
+ /**
+ * Constructor of DBWrapper
+ */
+ private constructor() {}
+
+ /**
+ * Get the instance of DBWrapper
+ */
+ public static getInstance(): DBWrapper {
+ if (!DBWrapper._instance) {
+ DBWrapper._instance = new DBWrapper();
+ }
+
+ return DBWrapper._instance;
+ }
+
+ /**
+ * Initialize the connection to the MySQL database
+ * @returns {Promise} `true` if the connection is initialized, `false` otherwise
+ */
+ private async initMySql(): Promise {
+ return new Promise((resolve, reject) => {
+ if (
+ this._MySqlConnection === undefined ||
+ !this._MySqlConnection.isInitialized
+ ) {
+ MySqlDataSource.initialize()
+ .then(() => {
+ this._MySqlConnection = MySqlDataSource;
+ resolve(true);
+ })
+ .catch((error) => {
+ console.log(error);
+ reject(false);
+ });
+ } else if (
+ this._MySqlConnection !== undefined &&
+ this._MySqlConnection.isInitialized
+ ) {
+ resolve(true);
+ } else {
+ resolve(false);
+ }
+ });
+ }
+
+ /**
+ * Initialize the connection to the SQLite database
+ * @returns {Promise} `true` if the connection is initialized, `false` otherwise
+ */
+ private async initSQLite(): Promise {
+ return new Promise((resolve, reject) => {
+ if (
+ this._SqliteConnection === undefined ||
+ !this._SqliteConnection.isInitialized
+ ) {
+ SQLiteDataSource.initialize()
+ .then(() => {
+ this._SqliteConnection = SQLiteDataSource;
+ resolve(true);
+ })
+ .catch((error) => {
+ console.log(error);
+ reject(false);
+ });
+ } else if (
+ this._SqliteConnection !== undefined &&
+ this._SqliteConnection.isInitialized
+ ) {
+ resolve(true);
+ } else {
+ resolve(false);
+ }
+ });
+ }
+
+ /**
+ * Initialize the connection to the MongoDB database
+ * @returns {Promise} `true` if the connection is initialized, `false` otherwise
+ */
+ private async initMongo(): Promise {
+ return new Promise((resolve, reject) => {
+ if (
+ this._MongoConnection === undefined ||
+ !this._MongoConnection.isInitialized
+ ) {
+ MongoDataSource.initialize()
+ .then(() => {
+ this._MongoConnection = MongoDataSource;
+ resolve(true);
+ })
+ .catch((error) => {
+ console.log(error);
+ reject(false);
+ });
+ } else if (
+ this._MongoConnection !== undefined &&
+ this._MongoConnection.isInitialized
+ ) {
+ resolve(true);
+ } else {
+ resolve(false);
+ }
+ });
+ }
+
+ private async initNeo4j(): Promise {
+ return new Promise((resolve) => {
+ if (this._Neo4jConnection === undefined) {
+ this._Neo4jConnection = NeogmaConnection;
+ }
+
+ resolve(true);
+ });
+ }
+
+ /**
+ * Retrieve the list of database connections to use
+ * @returns {DataSource[]} List of database connections to use
+ */
+ private retrieveDBConnections(): DataSource[] {
+ const dbConnections: DataSource[] = [];
+
+ if (this._dbToUse === "All") {
+ if (this._MySqlConnection) {
+ dbConnections.push(this._MySqlConnection);
+ }
+ if (this._SqliteConnection) {
+ dbConnections.push(this._SqliteConnection);
+ }
+ if (this._MongoConnection) {
+ dbConnections.push(this._MongoConnection);
+ }
+ } else {
+ if (this._MySqlConnection && this._dbToUse.includes(DBList.MySql)) {
+ dbConnections.push(this._MySqlConnection);
+ }
+
+ if (
+ this._SqliteConnection &&
+ this._dbToUse.includes(DBList.SQLite)
+ ) {
+ dbConnections.push(this._SqliteConnection);
+ }
+
+ if (this._MongoConnection && this._dbToUse.includes(DBList.Mongo)) {
+ dbConnections.push(this._MongoConnection);
+ }
+ }
+
+ return dbConnections;
+ }
+
+ /**
+ * Retrieve the repository of the entity
+ * @param {EntityTarget} entity The entity to retrieve the repository
+ * @param {DataSource} dataSource The database to use. If not specified, all connections are used. If specified, only the specified connection is used
+ * @returns {Repository | MongoRepository | undefined} The repository of the entity, `undefined` if the entity is not present in the database
+ * @throws {Error} If the entity is present in multiple connections and the connection is not specified
+ */
+ public retrieveRepository(
+ entity: EntityTarget,
+ dataSource?: DataSource,
+ ): Repository | MongoRepository | undefined {
+ let repository: Repository | undefined = undefined;
+
+ if (dataSource) {
+ if (dataSource.hasMetadata(entity)) {
+ if (dataSource === this._MongoConnection) {
+ repository = dataSource.getMongoRepository(entity);
+ } else {
+ repository = dataSource.getRepository(entity);
+ }
+ } else {
+ repository = undefined;
+ }
+ } else {
+ this.retrieveDBConnections().forEach((dbConnection) => {
+ if (dbConnection.hasMetadata(entity)) {
+ if (!repository) {
+ if (dbConnection === this._MongoConnection) {
+ repository =
+ dbConnection.getMongoRepository(entity);
+ } else {
+ repository = dbConnection.getRepository(entity);
+ }
+ } else {
+ throw new Error(
+ "The entity is present in multiple connections. Please specify the connection to use.",
+ );
+ }
+ }
+ });
+ }
+
+ return repository;
+ }
+
+ /**
+ * Add a database to the list of database to use
+ * @param {DBType} dbsToAdd The database to add to the list of database to use
+ */
+ private addToDbToUse(dbsToAdd: DBType) {
+ if (this._dbToUse === "All") {
+ return;
+ }
+
+ if (dbsToAdd === "All") {
+ this._dbToUse = dbsToAdd;
+ return;
+ }
+
+ for (let i = 0; i < dbsToAdd.length; i++) {
+ const db = dbsToAdd[i];
+
+ if (!this._dbToUse.includes(db)) {
+ this._dbToUse.push(db);
+ }
+ }
+
+ // Check if dbToUse contains all the databases in DBList
+ if (containsAllEnumValues(DBList, this._dbToUse)) {
+ this._dbToUse = "All";
+ }
+ }
+
+ /**
+ * Remove a database from the list of database to use
+ * @param {DBList} dbToRemove The database to remove from the list of database to use
+ */
+ private removeFromDbToUse(dbToRemove: DBList) {
+ if (this._dbToUse === "All") {
+ this._dbToUse = getAllEnumValues(DBList);
+ }
+
+ if (this._dbToUse.includes(dbToRemove)) {
+ this._dbToUse = this._dbToUse.filter((db) => db !== dbToRemove);
+ }
+ }
+
+ /**
+ * Initialize the connection to the database
+ * @param {DBType} dbToUse List of database to use. If dbToUse is not specified or is `All`, all connections are initialized
+ * @returns {Promise} `true` if the connection is initialized, `false` otherwise
+ *
+ * @example
+ * ```typescript
+ * const dbWrapper = DBWrapper.getInstance();
+ *
+ * // Initialize only the MySQL connection to the database
+ * await dbWrapper.init([DBList.MySql]);
+ *
+ * // Initialize the MySQL and MongoDB connections to the database
+ * await dbWrapper.init([DBList.MySql, DBList.Mongo]);
+ *
+ * // Initialize all connections to the database
+ * await dbWrapper.init(); // or dbWrapper.init("All");
+ * ```
+ */
+ public async init(dbToUse: DBType = "All"): Promise {
+ if (dbToUse === "All") {
+ this._dbToUse = dbToUse;
+ } else {
+ this.addToDbToUse(dbToUse);
+ }
+
+ let initResult = true;
+
+ if (dbToUse === "All") {
+ await this.initMySql().catch(() => {
+ initResult = false;
+ });
+ await this.initSQLite().catch(() => {
+ initResult = false;
+ });
+ await this.initMongo().catch(() => {
+ initResult = false;
+ });
+ await this.initNeo4j().catch(() => {
+ initResult = false;
+ });
+ } else {
+ if (initResult && dbToUse.includes(DBList.MySql)) {
+ await this.initMySql().catch(() => {
+ initResult = false;
+ });
+ }
+
+ if (initResult && dbToUse.includes(DBList.SQLite)) {
+ await this.initSQLite().catch(() => {
+ initResult = false;
+ });
+ }
+
+ if (initResult && dbToUse.includes(DBList.Mongo)) {
+ await this.initMongo().catch(() => {
+ initResult = false;
+ });
+ }
+
+ if (initResult && dbToUse.includes(DBList.Neo4j)) {
+ await this.initNeo4j().catch(() => {
+ initResult = false;
+ });
+ }
+ }
+
+ return initResult;
+ }
+
+ /**
+ * Close the connection to the database.
+ * @param {DBType} dbToClose List of database to close. If dbToClose is not specified or is `All`, all connections are closed
+ *
+ * @example
+ * ```typescript
+ * const dbWrapper = DBWrapper.getInstance();
+ *
+ * // Close only the MySQL connection to the database
+ * await dbWrapper.close([DBList.MySql]);
+ *
+ * // Close the MySQL and MongoDB connections to the database
+ * await dbWrapper.close([DBList.MySql, DBList.Mongo]);
+ *
+ * // Close all connections to the database
+ * await dbWrapper.close(); // or dbWrapper.close("All");
+ * ```
+ */
+ public async close(dbToClose: DBType = "All"): Promise {
+ if (dbToClose === "All") {
+ if (this._MySqlConnection) {
+ await this._MySqlConnection
+ .destroy()
+ .then(() => {
+ // Remove the connection from the list of connections
+ this._MySqlConnection = undefined;
+
+ this.removeFromDbToUse(DBList.MySql);
+ })
+ .catch(() => {});
+ }
+
+ if (this._SqliteConnection) {
+ await this._SqliteConnection
+ .destroy()
+ .then(() => {
+ // Remove the connection from the list of connections
+ this._SqliteConnection = undefined;
+
+ this.removeFromDbToUse(DBList.SQLite);
+ })
+ .catch(() => {});
+ }
+
+ if (this._MongoConnection) {
+ await this._MongoConnection
+ .destroy()
+ .then(() => {
+ // Remove the connection from the list of connections
+ this._MongoConnection = undefined;
+
+ this.removeFromDbToUse(DBList.Mongo);
+ })
+ .catch(() => {});
+ }
+
+ if (this._Neo4jConnection) {
+ await this._Neo4jConnection.driver
+ .close()
+ .then(() => {
+ // Remove the connection from the list of connections
+ this._Neo4jConnection = undefined;
+ })
+ .catch(() => {});
+ }
+ } else {
+ if (dbToClose.includes(DBList.MySql)) {
+ if (this._MySqlConnection) {
+ await this._MySqlConnection
+ .destroy()
+ .then(() => {
+ // Remove the connection from the list of connections
+ this._MySqlConnection = undefined;
+
+ this.removeFromDbToUse(DBList.MySql);
+ })
+ .catch(() => {});
+ }
+ }
+
+ if (dbToClose.includes(DBList.SQLite)) {
+ if (this._SqliteConnection) {
+ await this._SqliteConnection
+ .destroy()
+ .then(() => {
+ // Remove the connection from the list of connections
+ this._SqliteConnection = undefined;
+
+ this.removeFromDbToUse(DBList.SQLite);
+ })
+ .catch(() => {});
+ }
+ }
+
+ if (dbToClose.includes(DBList.Mongo)) {
+ if (this._MongoConnection) {
+ await this._MongoConnection
+ .destroy()
+ .then(() => {
+ // Remove the connection from the list of connections
+ this._MongoConnection = undefined;
+
+ this.removeFromDbToUse(DBList.Mongo);
+ })
+ .catch(() => {});
+ }
+ }
+
+ if (dbToClose.includes(DBList.Mongo)) {
+ if (this._Neo4jConnection) {
+ await this._Neo4jConnection.driver
+ .close()
+ .then(() => {
+ // Remove the connection from the list of connections
+ this._Neo4jConnection = undefined;
+ })
+ .catch(() => {});
+ }
+ }
+ }
+ }
+
+ public async transfer(
+ source: DBList,
+ destination: DBList,
+ ): Promise {
+ let status: ResultStatus | null = null;
+ let message: string | undefined;
+ let data: unknown | undefined;
+ let error: unknown | undefined;
+
+ if (source === destination) {
+ return new Result({
+ status: ResultStatus.Fail,
+ message: "Source and destination are the same",
+ });
+ }
+
+ // Check if source is the only database to use
+ if (
+ this._dbToUse === "All" ||
+ this._dbToUse.length !== 1 ||
+ this._dbToUse[0] !== source
+ ) {
+ await this.close();
+ await this.init([source]);
+ }
+
+ const reconstructedBoards: Board[] = [];
+
+ if (source === DBList.Neo4j && this._Neo4jConnection) {
+ const result = await Neo4jManager.findAll(this._Neo4jConnection);
+ const {users, puntos, cards} = result;
+
+ // Traitement des puntos
+ for (const punto of puntos) {
+ const playersRelation = await punto.findRelationships({
+ alias: "Player",
+ });
+ const cardsRelation = await punto.findRelationships({
+ alias: "Card",
+ });
+
+ const players: Player[] = [];
+ const winners: Player[] = [];
+ const losers: Player[] = [];
+
+ // Traitement des relations avec les joueurs
+ for (const playerRelation of playersRelation) {
+ const playerData = playerRelation.target as Neo4jUser;
+ const relationData =
+ playerRelation.relationship as RelationshipPropertiesI;
+
+ const player = Player.build(
+ playerData.name,
+ [],
+ null,
+ relationData.points as number,
+ false,
+ [],
+ 0,
+ );
+
+ players.push(player);
+
+ if (relationData.status === "winner") {
+ winners.push(player);
+ } else {
+ losers.push(player);
+ }
+ }
+
+ const listCards: Card[] = [];
+
+ // Traitement des relations avec les cartes
+ for (const cardRelation of cardsRelation) {
+ // const cardData = cardRelation.target as Neo4jCard;
+ const cardData = cardRelation.target as NeogmaCardInstance;
+
+ const playersNameWhoPlayedTheCard =
+ await cardData.findRelationships({alias: "Player"});
+ const playerNameWhoPlayedTheCard =
+ playersNameWhoPlayedTheCard[0] as (typeof playersNameWhoPlayedTheCard)[0];
+
+ const playerWhoPlayedTheCard = players.find(
+ (player) =>
+ player.name ===
+ (playerNameWhoPlayedTheCard.target as Neo4jUser)
+ .name,
+ );
+
+ const card = Card.build(
+ cardData.color as string,
+ cardData.value as number,
+ cardData.x as number,
+ cardData.y as number,
+ cardData.playedTurn as number,
+ cardData.playedIn as number,
+ playerWhoPlayedTheCard,
+ );
+
+ listCards.push(card);
+ }
+
+ // Get the card with the highest playedTurn
+ const lastTurnCard = listCards.reduce((prev, current) => {
+ return prev.playedTurn > current.playedTurn
+ ? prev
+ : current;
+ });
+
+ const lastTurn = lastTurnCard.playedTurn;
+
+ let winType: WinType = WinType.None;
+
+ switch (punto.winType) {
+ case "Win":
+ winType = WinType.Win;
+ break;
+ case "Draw":
+ winType = WinType.Draw;
+ break;
+ case "Drop":
+ winType = WinType.Drop;
+ break;
+ default:
+ winType = WinType.None;
+ break;
+ }
+
+ const board = Board.build(
+ listCards,
+ players,
+ lastTurn,
+ winType,
+ winners,
+ losers,
+ );
+
+ reconstructedBoards.push(board);
+ }
+ } else {
+ const gameManager = new GameManager(this);
+
+ const result = await gameManager.findAll();
+
+ let data = undefined;
+
+ switch (source) {
+ case DBList.MySql:
+ data = result.mySqlRepo?.data;
+ break;
+ case DBList.SQLite:
+ data = result.sqliteRepo?.data;
+ break;
+ case DBList.Mongo:
+ data = result.mongoRepo?.data as MongoPunto[];
+ break;
+ default:
+ break;
+ }
+
+ if (source === DBList.Mongo && data && Array.isArray(data)) {
+ for (const puntoFromDB of data) {
+ const players: Player[] = [];
+ const winners: Player[] = [];
+ const losers: Player[] = [];
+
+ for (const playerFromDB of puntoFromDB.players) {
+ const player = Player.build(
+ playerFromDB.playerName,
+ [],
+ null,
+ playerFromDB.points,
+ false,
+ [],
+ 0,
+ );
+
+ players.push(player);
+
+ if (playerFromDB.status === "winner") {
+ winners.push(player);
+ } else {
+ losers.push(player);
+ }
+ }
+
+ const cards: Card[] = [];
+
+ for (const cardFromDB of puntoFromDB.board) {
+ const playerWhoPlayedTheCard = players.find(
+ (player) => player.name === cardFromDB.playedBy,
+ );
+
+ const card = Card.build(
+ cardFromDB.color,
+ cardFromDB.value,
+ cardFromDB.x,
+ cardFromDB.y,
+ cardFromDB.playedTurn,
+ cardFromDB.playedIn,
+ playerWhoPlayedTheCard,
+ );
+
+ cards.push(card);
+ }
+
+ const lastTurnCard = cards.reduce((prev, current) => {
+ return prev.playedTurn > current.playedTurn
+ ? prev
+ : current;
+ });
+
+ const lastTurn = lastTurnCard.playedTurn;
+
+ let winType: WinType = WinType.None;
+
+ switch (puntoFromDB.winType) {
+ case "Win":
+ winType = WinType.Win;
+ break;
+ case "Draw":
+ winType = WinType.Draw;
+ break;
+ case "Drop":
+ winType = WinType.Drop;
+ break;
+ default:
+ winType = WinType.None;
+ break;
+ }
+
+ const board = Board.build(
+ cards,
+ players,
+ lastTurn,
+ winType,
+ winners,
+ losers,
+ );
+
+ reconstructedBoards.push(board);
+ }
+ } else if (data) {
+ let puntosData = undefined;
+ let playersData = undefined;
+ let cardsData = undefined;
+
+ const userManager = new UserManager(this, "");
+
+ const users = await userManager.findAll();
+
+ switch (source) {
+ case DBList.MySql:
+ data = data as {
+ mySqlPunto: MySQLPunto[];
+ mySqlPlayers: MySQLPuntoPlayer[];
+ mySqlCards: MySQLCard[];
+ };
+
+ puntosData = data.mySqlPunto as MySQLPunto[];
+ playersData = data.mySqlPlayers as MySQLPuntoPlayer[];
+ cardsData = data.mySqlCards as MySQLCard[];
+ break;
+ case DBList.SQLite:
+ data = data as {
+ sqlitePunto: SQLitePunto[];
+ sqlitePlayers: SQLitePuntoPlayer[];
+ sqliteCards: SQLiteCard[];
+ };
+
+ puntosData = data.sqlitePunto as SQLitePunto[];
+ playersData = data.sqlitePlayers as SQLitePuntoPlayer[];
+ cardsData = data.sqliteCards as SQLiteCard[];
+ break;
+ default:
+ break;
+ }
+
+ if (
+ puntosData &&
+ playersData &&
+ cardsData &&
+ Array.isArray(puntosData) &&
+ Array.isArray(playersData) &&
+ Array.isArray(cardsData)
+ ) {
+ for (const puntoFromDB of puntosData) {
+ const players: Player[] = [];
+ const winners: Player[] = [];
+ const losers: Player[] = [];
+
+ for (const playerFromDB of playersData) {
+ let name = "";
+
+ if (users.mySqlRepo) {
+ const user = (
+ users.mySqlRepo.data as MySQLUser[]
+ ).find(
+ (user) =>
+ user._id === playerFromDB.playerID,
+ );
+
+ if (user) {
+ name = user.name;
+ }
+ } else if (users.sqliteRepo) {
+ const user = (
+ users.sqliteRepo.data as SQLiteUser[]
+ ).find(
+ (user) =>
+ user._id === playerFromDB.playerID,
+ );
+
+ if (user) {
+ name = user.name;
+ }
+ } else {
+ return new Result({
+ status: ResultStatus.Fail,
+ message: "No user data found",
+ });
+ }
+
+ if (playerFromDB.boardId === puntoFromDB._id) {
+ const player = Player.build(
+ name,
+ [],
+ null,
+ playerFromDB.points,
+ false,
+ [],
+ 0,
+ );
+
+ players.push(player);
+
+ if (playerFromDB.status === "winner") {
+ winners.push(player);
+ } else {
+ losers.push(player);
+ }
+ }
+ }
+
+ const cards: Card[] = [];
+
+ for (const cardFromDB of cardsData) {
+ if (cardFromDB.boardId === puntoFromDB._id) {
+ let name = "";
+
+ if (users.mySqlRepo) {
+ const user = (
+ users.mySqlRepo.data as MySQLUser[]
+ ).find(
+ (user) =>
+ user._id === cardFromDB.playedBy,
+ );
+
+ if (user) {
+ name = user.name;
+ }
+ } else if (users.sqliteRepo) {
+ const user = (
+ users.sqliteRepo.data as SQLiteUser[]
+ ).find(
+ (user) =>
+ user._id === cardFromDB.playedBy,
+ );
+
+ if (user) {
+ name = user.name;
+ }
+ } else {
+ return new Result({
+ status: ResultStatus.Fail,
+ message: "No user data found",
+ });
+ }
+
+ const playerWhoPlayedTheCard = players.find(
+ (player) => player.name === name,
+ );
+
+ const card = Card.build(
+ cardFromDB.color,
+ cardFromDB.value,
+ cardFromDB.x,
+ cardFromDB.y,
+ cardFromDB.playedTurn,
+ cardFromDB.playedIn,
+ playerWhoPlayedTheCard,
+ );
+
+ cards.push(card);
+ }
+ }
+
+ const lastTurnCard = cards.reduce((prev, current) => {
+ return prev.playedTurn > current.playedTurn
+ ? prev
+ : current;
+ });
+
+ const lastTurn = lastTurnCard.playedTurn;
+
+ let winType: WinType = WinType.None;
+
+ switch (puntoFromDB.winType) {
+ case "Win":
+ winType = WinType.Win;
+ break;
+ case "Draw":
+ winType = WinType.Draw;
+ break;
+ case "Drop":
+ winType = WinType.Drop;
+ break;
+ default:
+ winType = WinType.None;
+ break;
+ }
+
+ const board = Board.build(
+ cards,
+ players,
+ lastTurn,
+ winType,
+ winners,
+ losers,
+ );
+
+ reconstructedBoards.push(board);
+ }
+ }
+ } else {
+ return new Result({
+ status: ResultStatus.Fail,
+ message: "No data found",
+ });
+ }
+ }
+
+ await this.close([source]);
+ await this.init([destination]);
+
+ for (const board of reconstructedBoards) {
+ const players = board.players;
+
+ for (let i = 0; i < players.length; i++) {
+ const player = players[i];
+ const findResults = await UserManager.find(player.name);
+
+ const isArrayWithData = (data: unknown): boolean => {
+ return Array.isArray(data) && data.length > 0;
+ };
+
+ // Vérifier si l'utilisateur existe dans chaque base de données
+ const userExistsInMySQL = isArrayWithData(
+ findResults.mySqlRepo?.data,
+ );
+ const userExistsInSQLite = isArrayWithData(
+ findResults.sqliteRepo?.data,
+ );
+ const userExistsInMongo = isArrayWithData(
+ findResults.mongoRepo?.data,
+ );
+
+ // Si l'utilisateur n'existe dans aucune base de données ou s'il n'existe pas dans toutes les bases de données
+ if (
+ !userExistsInMySQL ||
+ !userExistsInSQLite ||
+ !userExistsInMongo
+ ) {
+ // Construire ou reconstruire l'utilisateur
+ const user =
+ userExistsInMySQL ||
+ userExistsInSQLite ||
+ userExistsInMongo
+ ? await UserManager.build(player.name)
+ : new UserManager(this, player.name);
+
+ if (
+ userExistsInMySQL ||
+ userExistsInSQLite ||
+ userExistsInMongo
+ ) {
+ await user.rebuild();
+ }
+
+ const saveResultsUsers = await user.save();
+
+ const mySqlUserStatus = saveResultsUsers.mySqlRepo?.status;
+ const sqliteUserStatus =
+ saveResultsUsers.sqliteRepo?.status;
+ const mongoUserStatus = saveResultsUsers.mongoRepo?.status;
+
+ // Backup error handling
+ if (
+ mySqlUserStatus !== undefined &&
+ mySqlUserStatus !== ResultStatus.Success
+ ) {
+ console.error(saveResultsUsers.mySqlRepo?.error);
+ }
+ if (
+ sqliteUserStatus !== undefined &&
+ sqliteUserStatus !== ResultStatus.Success
+ ) {
+ console.error(saveResultsUsers.sqliteRepo?.error);
+ }
+ if (
+ mongoUserStatus !== undefined &&
+ mongoUserStatus !== ResultStatus.Success
+ ) {
+ console.error(saveResultsUsers.mongoRepo?.error);
+ }
+ } else {
+ // L'utilisateur existe dans toutes les bases de données, aucune action n'est requise
+ }
+ }
+
+ const game = new GameManager(this);
+
+ await game.buildEntities(board);
+
+ const saveResultsGame = await game.save();
+
+ // Save for Neo4j
+ if (this.Neo4jConnection && this.dbToUse.includes(DBList.Neo4j)) {
+ const neo4jManager = Neo4jManager.getInstance(
+ this.Neo4jConnection,
+ );
+
+ await neo4jManager.createIfNotExist(board);
+ }
+
+ const mySqlGameStatus = saveResultsGame.mySqlRepo?.status;
+ const sqliteGameStatus = saveResultsGame.sqliteRepo?.status;
+ const mongoGameStatus = saveResultsGame.mongoRepo?.status;
+
+ // Backup error handling
+ if (
+ mySqlGameStatus !== undefined &&
+ mySqlGameStatus !== ResultStatus.Success
+ ) {
+ console.error(saveResultsGame.mySqlRepo?.error);
+ status = ResultStatus.Fail;
+ }
+ if (
+ sqliteGameStatus !== undefined &&
+ sqliteGameStatus !== ResultStatus.Success
+ ) {
+ console.error(saveResultsGame.sqliteRepo?.error);
+ status = ResultStatus.Fail;
+ }
+ if (
+ mongoGameStatus !== undefined &&
+ mongoGameStatus !== ResultStatus.Success
+ ) {
+ console.error(saveResultsGame.mongoRepo?.error);
+ status = ResultStatus.Fail;
+ }
+ }
+
+ await this.close([destination]);
+
+ if (!status) {
+ status = ResultStatus.Success;
+ }
+
+ return new Result({
+ status,
+ message,
+ data,
+ error,
+ });
+ }
+}
+
+/**
+ * Enum to specify which database to use
+ */
+enum DBList {
+ MySql = "MySql",
+ SQLite = "SQLite",
+ Mongo = "Mongo",
+ Neo4j = "Neo4j",
+}
+
+type DBType = "All" | DBList[];
+
+export default DBWrapper;
+
+export {DBWrapper, DBType, DBList};
+
+function getAllEnumValues(enumObj: {[s: string]: E}): E[] {
+ return Object.keys(enumObj)
+ .map((key) => enumObj[key as keyof typeof enumObj])
+ .filter((value): value is E => typeof value === "string") as E[];
+}
+
+function containsAllEnumValues(
+ enumToCheck: typeof DBList,
+ arrayToCheck: DBList[],
+): boolean {
+ const allValues = getAllEnumValues(enumToCheck);
+ return allValues.every((value) => arrayToCheck.includes(value as DBList));
+}
diff --git a/src/db/Neo4jManager.ts b/src/db/Neo4jManager.ts
new file mode 100644
index 0000000..d02b582
--- /dev/null
+++ b/src/db/Neo4jManager.ts
@@ -0,0 +1,705 @@
+import {
+ NeogmaModel,
+ Neo4jSupportedProperties,
+ NeogmaInstance,
+ Neogma,
+ ModelFactory,
+} from "neogma";
+import Board from "../game/Board";
+import Card from "../game/Card";
+
+class Neo4jManager {
+ private static _neogma: Neogma;
+ public static get neogma(): Neogma {
+ return Neo4jManager._neogma;
+ }
+
+ private _neo4jUser: Neo4jUser[] = [];
+ public get neo4jUser(): Neo4jUser[] {
+ return this._neo4jUser;
+ }
+
+ private _neo4jPunto?: Neo4jPunto;
+ public get neo4jPunto(): Neo4jPunto | undefined {
+ return this._neo4jPunto;
+ }
+
+ private _neo4jCard: Neo4jCard[] = [];
+ public get neo4jCard(): Neo4jCard[] {
+ return this._neo4jCard;
+ }
+
+ private static _instance: Neo4jManager;
+ public static get instance(): Neo4jManager {
+ return Neo4jManager._instance;
+ }
+
+ private constructor(neogma: Neogma) {
+ Neo4jManager._neogma = neogma;
+ }
+
+ public static getInstance(neogma: Neogma): Neo4jManager {
+ if (!Neo4jManager._instance) {
+ Neo4jManager._instance = new Neo4jManager(neogma);
+ }
+
+ return Neo4jManager._instance;
+ }
+
+ public async createIfNotExist(board: Board) {
+ const players = board.players;
+ const cards = board.cards;
+
+ Neo4jUser.initModel(Neo4jManager.neogma);
+ Neo4jPunto.initModel(Neo4jManager.neogma);
+ Neo4jCard.initModel(Neo4jManager.neogma);
+
+ Neo4jUser.initModel(Neo4jManager.neogma);
+ Neo4jPunto.initModel(Neo4jManager.neogma);
+ Neo4jCard.initModel(Neo4jManager.neogma);
+
+ // Create nodes
+ for (const player of players) {
+ const neo4jUser = new Neo4jUser(player.name);
+ await neo4jUser.createIfNotExist();
+ this._neo4jUser.push(neo4jUser);
+ }
+
+ const neo4jPunto = new Neo4jPunto(board.id, board.winType);
+ await neo4jPunto.createIfNotExist();
+ this._neo4jPunto = neo4jPunto;
+
+ for (const card of cards) {
+ const neo4jCard = new Neo4jCard(card);
+ await neo4jCard.createIfNotExist();
+ this._neo4jCard.push(neo4jCard);
+ }
+
+ // Create relationships
+ for (const player of players) {
+ const neo4jUser = this._neo4jUser.find(
+ (neo4jUser) => neo4jUser.name === player.name,
+ );
+
+ if (!neo4jUser) {
+ throw new Error("Neo4jUser is undefined");
+ // continue;
+ }
+
+ await neo4jUser.user?.relateTo({
+ alias: "Punto",
+ where: {
+ _id: neo4jPunto.id,
+ },
+ properties: {
+ points: player.points,
+ status: board.winners.includes(player) ? "winner" : "loser",
+ },
+ });
+ }
+
+ for (const card of cards) {
+ const neo4jCard = this._neo4jCard.find(
+ (neo4jCard) => neo4jCard.id === card.id,
+ );
+
+ if (!neo4jCard) {
+ throw new Error("Neo4jCard is undefined");
+ // continue;
+ }
+
+ await neo4jCard.card?.relateTo({
+ alias: "Punto",
+ where: {
+ _id: neo4jPunto.id,
+ },
+ });
+
+ const neo4jUser = this._neo4jUser.find(
+ (neo4jUser) => neo4jUser.name === card.playedBy?.name,
+ );
+
+ if (!neo4jUser) {
+ throw new Error("Neo4jUser is undefined");
+ // continue;
+ }
+
+ await neo4jCard.card?.relateTo({
+ alias: "Player",
+ where: {
+ name: neo4jUser.name,
+ },
+ });
+ }
+ }
+
+ public async save(): Promise {
+ for (const neo4jUser of this.neo4jUser) {
+ await neo4jUser.save();
+ }
+
+ await this.neo4jPunto?.save();
+
+ for (const neo4jCard of this.neo4jCard) {
+ await neo4jCard.save();
+ }
+ }
+
+ public async delete(): Promise {
+ for (const neo4jUser of this.neo4jUser) {
+ await neo4jUser.delete();
+ }
+
+ await this.neo4jPunto?.delete();
+
+ for (const neo4jCard of this.neo4jCard) {
+ await neo4jCard.delete();
+ }
+ }
+
+ public async deleteAll(): Promise {
+ await Neo4jUser.deleteAll();
+ await Neo4jPunto.deleteAll();
+ await Neo4jCard.deleteAll();
+ }
+
+ public async findAll(): Promise<{
+ users: NeogmaUserInstance[];
+ puntos: NeogmaPuntoInstance[];
+ cards: NeogmaCardInstance[];
+ }> {
+ return await Neo4jManager.findAll();
+ }
+
+ public static async findAll(neogma?: Neogma): Promise<{
+ users: NeogmaUserInstance[];
+ puntos: NeogmaPuntoInstance[];
+ cards: NeogmaCardInstance[];
+ }> {
+ if (!Neo4jManager.neogma) {
+ if (!neogma) {
+ throw new Error("Neogma is undefined");
+ }
+
+ Neo4jManager._neogma = neogma;
+ }
+
+ Neo4jUser.initModel(Neo4jManager.neogma);
+ Neo4jPunto.initModel(Neo4jManager.neogma);
+ Neo4jCard.initModel(Neo4jManager.neogma);
+
+ Neo4jUser.initModel(Neo4jManager.neogma);
+ Neo4jPunto.initModel(Neo4jManager.neogma);
+ Neo4jCard.initModel(Neo4jManager.neogma);
+
+ const users = await Neo4jUser.findAll();
+ const puntos = await Neo4jPunto.findAll();
+ const cards = await Neo4jCard.findAll();
+
+ return {
+ users,
+ puntos,
+ cards,
+ };
+ }
+
+ public async retrieveAll(): Promise {
+ const boards: Board[] = [];
+
+ return boards;
+ }
+}
+
+type NeogmaUserModel = NeogmaModel<
+ Neo4jSupportedProperties,
+ {Punto: unknown; Card: unknown}
+>;
+type NeogmaUserInstance = NeogmaInstance<
+ Neo4jSupportedProperties,
+ {Punto: unknown; Card: unknown}
+>;
+
+class Neo4jUser {
+ private static _model: NeogmaUserModel;
+ public static get model(): NeogmaUserModel {
+ return Neo4jUser._model;
+ }
+
+ private _name: string;
+ public get name(): string {
+ return this._name;
+ }
+
+ private _user?: NeogmaUserInstance;
+ public get user(): NeogmaUserInstance | undefined {
+ return this._user;
+ }
+ public set user(value: NeogmaUserInstance) {
+ this._user = value;
+ }
+
+ constructor(name: string) {
+ this._name = name;
+ }
+
+ public static initModel(neogma: Neogma): void {
+ if (!neogma) {
+ throw new Error("Neogma is undefined");
+ }
+
+ Neo4jUser._model = ModelFactory(
+ {
+ label: "User",
+ schema: {
+ name: {
+ type: "string",
+ required: true,
+ },
+ },
+ primaryKeyField: "name",
+ relationships: {
+ Punto: {
+ model: Neo4jPunto.model,
+ direction: "out",
+ name: "PLAYED_IN",
+ properties: {
+ points: {
+ property: "points",
+ schema: {type: "number"},
+ },
+ status: {
+ property: "status",
+ schema: {type: "string"},
+ },
+ },
+ },
+ Card: {
+ model: Neo4jCard.model,
+ direction: "in",
+ name: "PLAYED_BY",
+ },
+ },
+ },
+ neogma,
+ );
+ }
+
+ public async createIfNotExist(): Promise {
+ this._user = await Neo4jUser._model.createOne(
+ {
+ name: this._name,
+ },
+ {
+ merge: true,
+ },
+ );
+
+ return this._user;
+ }
+
+ public async save(): Promise {
+ // this._user = await this._user?.save();
+ this.createIfNotExist();
+
+ return this._user;
+ }
+
+ /**
+ * Deletes the user from the database.
+ * @returns {Promise} The deleted user.
+ */
+ public async delete(): Promise {
+ const nbr = await this._user?.delete({detach: true});
+
+ return nbr === 1 ? this._user : undefined;
+ }
+
+ public static async deleteAll(): Promise {
+ await Neo4jUser._model.delete({where: {}, detach: true});
+ }
+
+ /**
+ * Finds the user in the database with the specified name in this object.
+ * @param {boolean} createIfNotExist If true, the user will be created if it does not exist.
+ * @returns {Promise} The found user.
+ *
+ * @example
+ * // Assume that the user "John" does not exist in the database.
+ * const user = new Neo4jUser(neogma, "John");
+ *
+ * const findedUser = await user.find(); // Returns undefined;
+ *
+ * await user.createIfNotExist();
+ *
+ * const findedUser = await user.find(); // Returns the created user;
+ *
+ * // OR (assuming that the user "John" does not exist in the database)
+ *
+ * const findedUser = await user.find(true); // Returns the created user;
+ */
+ public async find(
+ createIfNotExist: boolean = false,
+ ): Promise {
+ const findedUser = await Neo4jUser._model.findOne({
+ where: {
+ name: this._name,
+ },
+ });
+
+ if (findedUser) {
+ this._user = findedUser;
+ } else if (createIfNotExist) {
+ this._user = await this.createIfNotExist();
+ }
+
+ return this._user;
+ }
+
+ public static async find(
+ name: string,
+ ): Promise {
+ const findedUser = await Neo4jUser._model.findOne({
+ where: {
+ name: name,
+ },
+ });
+
+ if (findedUser) {
+ return findedUser;
+ } else {
+ return undefined;
+ }
+ }
+
+ public static async findAll(): Promise {
+ const findedUsers = await Neo4jUser._model.findMany();
+
+ return findedUsers;
+ }
+}
+
+type NeogmaPuntoModel = NeogmaModel<
+ Neo4jSupportedProperties,
+ {Player: unknown; Card: unknown}
+>;
+type NeogmaPuntoInstance = NeogmaInstance<
+ Neo4jSupportedProperties,
+ {Player: unknown; Card: unknown}
+>;
+class Neo4jPunto {
+ private static _model: NeogmaPuntoModel;
+
+ public static get model(): NeogmaPuntoModel {
+ return Neo4jPunto._model;
+ }
+
+ private _punto?: NeogmaPuntoInstance;
+ public get punto(): NeogmaPuntoInstance | undefined {
+ return this._punto;
+ }
+
+ private _id: string;
+ public get id(): string {
+ return this._id;
+ }
+
+ private _winType: string;
+ public get winType(): string {
+ return this._winType;
+ }
+
+ constructor(id: string, winType: string) {
+ this._id = id;
+ this._winType = winType;
+ }
+
+ public static initModel(neogma: Neogma): void {
+ if (!neogma) {
+ throw new Error("Neogma is undefined");
+ }
+
+ Neo4jPunto._model = ModelFactory(
+ {
+ label: "Punto",
+ schema: {
+ _id: {
+ type: "string",
+ required: true,
+ },
+ winType: {
+ type: "string",
+ required: true,
+ },
+ },
+ primaryKeyField: "_id",
+ relationships: {
+ Player: {
+ model: Neo4jUser.model,
+ direction: "in",
+ name: "PLAYED_IN",
+ properties: {
+ points: {
+ property: "points",
+ schema: {type: "number"},
+ },
+ status: {
+ property: "status",
+ schema: {type: "string"},
+ },
+ },
+ },
+ Card: {
+ model: Neo4jCard.model,
+ direction: "out",
+ name: "CONTAINS_CARD",
+ },
+ },
+ },
+ neogma,
+ );
+ }
+
+ public async createIfNotExist(): Promise {
+ this._punto = await Neo4jPunto._model.createOne(
+ {
+ _id: this._id,
+ winType: this._winType,
+ },
+ {
+ merge: true,
+ },
+ );
+
+ return this._punto;
+ }
+
+ public async save(): Promise {
+ this._punto = await this._punto?.save();
+
+ return this._punto;
+ }
+
+ public async delete(): Promise {
+ const nbr = await this._punto?.delete({detach: true});
+
+ return nbr === 1 ? this._punto : undefined;
+ }
+
+ public static async deleteAll(): Promise {
+ await Neo4jPunto._model.delete({where: {}, detach: true});
+ }
+
+ public static async find(
+ id: string,
+ ): Promise {
+ const findedPunto = await Neo4jPunto._model.findOne({
+ where: {
+ _id: id,
+ },
+ });
+
+ if (findedPunto) {
+ return findedPunto;
+ } else {
+ return undefined;
+ }
+ }
+
+ public static async findAll(): Promise {
+ const findedPuntos = await Neo4jPunto._model.findMany();
+
+ return findedPuntos;
+ }
+}
+
+type NeogmaCardModel = NeogmaModel<
+ Neo4jSupportedProperties,
+ {Punto: unknown; Player: unknown}
+>;
+type NeogmaCardInstance = NeogmaInstance<
+ Neo4jSupportedProperties,
+ {Punto: unknown; Player: unknown}
+>;
+
+class Neo4jCard {
+ private static _model: NeogmaCardModel;
+ public static get model(): NeogmaCardModel {
+ return Neo4jCard._model;
+ }
+
+ private _card?: NeogmaCardInstance;
+ public get card(): NeogmaCardInstance | undefined {
+ return this._card;
+ }
+
+ private _id: string;
+ public get id(): string {
+ return this._id;
+ }
+
+ private _x: number;
+ public get x(): number {
+ return this._x;
+ }
+
+ private _y: number;
+ public get y(): number {
+ return this._y;
+ }
+
+ private _color: string;
+ public get color(): string {
+ return this._color;
+ }
+
+ private _value: number;
+ public get value(): number {
+ return this._value;
+ }
+
+ private _playedTurn: number;
+ public get playedTurn(): number {
+ return this._playedTurn;
+ }
+
+ private _playedIn: number;
+ public get playedIn(): number {
+ return this._playedIn;
+ }
+
+ constructor(card: Card) {
+ this._id = card.id;
+ this._x = card.x;
+ this._y = card.y;
+ this._color = card.color;
+ this._value = card.value;
+ this._playedTurn = card.playedTurn;
+ this._playedIn = card.playedIn;
+ }
+
+ public static initModel(neogma: Neogma): void {
+ if (!neogma) {
+ throw new Error("Neogma is undefined");
+ }
+
+ Neo4jCard._model = ModelFactory(
+ {
+ label: "Card",
+ schema: {
+ _id: {
+ type: "string",
+ required: true,
+ },
+ x: {
+ type: "number",
+ required: true,
+ },
+ y: {
+ type: "number",
+ required: true,
+ },
+ color: {
+ type: "string",
+ required: true,
+ },
+ value: {
+ type: "number",
+ required: true,
+ },
+ playedTurn: {
+ type: "number",
+ required: true,
+ },
+ playedIn: {
+ type: "number",
+ required: true,
+ },
+ },
+ primaryKeyField: "_id",
+ relationships: {
+ Punto: {
+ model: Neo4jPunto.model,
+ direction: "in",
+ name: "CONTAINS_CARD",
+ },
+ Player: {
+ model: Neo4jUser.model,
+ direction: "out",
+ name: "PLAYED_BY",
+ },
+ },
+ },
+ neogma,
+ );
+ }
+
+ public async createIfNotExist(): Promise {
+ this._card = await Neo4jCard._model.createOne(
+ {
+ _id: this._id,
+ x: this._x,
+ y: this._y,
+ color: this._color,
+ value: this._value,
+ playedTurn: this._playedTurn,
+ playedIn: this._playedIn,
+ },
+ {
+ merge: true,
+ },
+ );
+
+ return this._card;
+ }
+
+ public async save(): Promise {
+ this._card = await this._card?.save();
+
+ return this._card;
+ }
+
+ public async delete(): Promise {
+ const nbr = await this._card?.delete({detach: true});
+
+ return nbr === 1 ? this._card : undefined;
+ }
+
+ public static async deleteAll(): Promise {
+ await Neo4jCard._model.delete({where: {}, detach: true});
+ }
+
+ public static async find(
+ id: string,
+ ): Promise {
+ const findedCard = await Neo4jCard._model.findOne({
+ where: {
+ _id: id,
+ },
+ });
+
+ if (findedCard) {
+ return findedCard;
+ } else {
+ return undefined;
+ }
+ }
+
+ public static async findAll(): Promise {
+ const findedCards = await Neo4jCard._model.findMany();
+
+ return findedCards;
+ }
+}
+
+export default Neo4jManager;
+export {
+ Neo4jUser,
+ Neo4jPunto,
+ Neo4jCard,
+ NeogmaUserInstance,
+ NeogmaUserModel,
+ NeogmaPuntoInstance,
+ NeogmaPuntoModel,
+ NeogmaCardInstance,
+ NeogmaCardModel,
+};
diff --git a/src/db/Result.ts b/src/db/Result.ts
new file mode 100644
index 0000000..5425a31
--- /dev/null
+++ b/src/db/Result.ts
@@ -0,0 +1,84 @@
+/**
+ * Class representing a Result with various properties.
+ */
+class Result {
+ /**
+ * Unique identifier for the Result. Auto-generated if not specified.
+ * @type {string}
+ */
+ public readonly id: string;
+
+ /**
+ * Represents the status of the Result, based on the ResultStatus enum.
+ * @type {ResultStatus}
+ */
+ public readonly status: ResultStatus;
+
+ /**
+ * Optional message associated with the Result.
+ * @type {string}
+ */
+ public readonly message?: string;
+
+ /**
+ * Optional data payload of the Result.
+ * @type {unknown}
+ */
+ public readonly data?: unknown;
+
+ /**
+ * Optional error information of the Result.
+ * @type {unknown}
+ */
+ public readonly error?: unknown;
+
+ /**
+ * Constructs a new Result instance.
+ * @param {Object} params - Parameters for the result including id, status, message, data, and error.
+ */
+ constructor({
+ id,
+ status,
+ message,
+ data,
+ error,
+ }: {
+ id?: string;
+ status: ResultStatus;
+ message?: string;
+ data?: unknown;
+ error?: unknown;
+ }) {
+ this.id = id ?? this.generateUUID();
+ this.status = status;
+ this.message = message;
+ this.data = data;
+ this.error = error;
+ }
+
+ /**
+ * Generates a UUID.
+ * @returns {string} - A new UUID string.
+ */
+ private generateUUID(): string {
+ return crypto.randomUUID();
+ }
+}
+
+/**
+ * Enumeration for possible Result statuses.
+ */
+enum ResultStatus {
+ /**
+ * Indicates a successful Result.
+ */
+ Success,
+
+ /**
+ * Indicates a failed Result.
+ */
+ Fail,
+}
+
+export default Result;
+export {Result, ResultStatus};
diff --git a/src/entities/BaseEntityManager.ts b/src/entities/BaseEntityManager.ts
new file mode 100644
index 0000000..6605f13
--- /dev/null
+++ b/src/entities/BaseEntityManager.ts
@@ -0,0 +1,238 @@
+import {
+ EntityTarget,
+ MongoRepository,
+ ObjectLiteral,
+ Repository,
+} from "typeorm";
+import DBWrapper from "../db/DBWrapper";
+import Result from "../db/Result";
+
+abstract class BaseEntityManager {
+ /**
+ * Instance of DBWrapper.
+ * This is a protected and static member of BaseEntityManager.
+ * For more information, refer to DBWrapper documentation.
+ * @protected
+ * @static
+ * @type {DBWrapper}
+ * @memberof BaseEntityManager
+ * @see DBWrapper
+ */
+ protected static dbWrapper: DBWrapper;
+
+ /**
+ * Repository for MySQL. It gets initialized when {@link BaseEntityManager.initRepositories} is invoked with the MySQL entity.
+ * To obtain the repository, use the {@link DBWrapper.retrieveRepository} method.
+ * @protected
+ * @static
+ * @type {Repository}
+ * @memberof BaseEntityManager
+ */
+ protected static mySqlRepo?: Repository;
+
+ /**
+ * Repository for SQLite. It gets initialized when {@link BaseEntityManager.initRepositories} is called with the SQLite entity.
+ * To retrieve the repository, use the {@link DBWrapper.retrieveRepository} method.
+ * @protected
+ * @static
+ * @type {Repository}
+ * @memberof BaseEntityManager
+ */
+ protected static sqliteRepo?: Repository;
+
+ /**
+ * Repository for MongoDB. This is initialized upon calling {@link BaseEntityManager.initRepositories} with the MongoDB entity.
+ * The repository can be retrieved using the {@link DBWrapper.retrieveRepository} method.
+ * @protected
+ * @static
+ * @type {Repository}
+ * @memberof BaseEntityManager
+ */
+ protected static mongoRepo?: Repository;
+
+ /**
+ * The MySQL entity used in this class. Replace with the specific MySQL entity of the child class.
+ * This is a protected and abstract member.
+ * @protected
+ * @abstract
+ * @type {ObjectLiteral}
+ * @memberof BaseEntityManager
+ */
+ protected abstract mySqlEntity?: ObjectLiteral;
+
+ /**
+ * The SQLite entity used in this class. Replace with the specific SQLite entity of the child class.
+ * This is a protected and abstract member.
+ * @protected
+ * @abstract
+ * @type {ObjectLiteral}
+ * @memberof BaseEntityManager
+ */
+ protected abstract sqliteEntity?: ObjectLiteral;
+
+ /**
+ * The MongoDB entity used in this class. Replace with the specific MongoDB entity of the child class.
+ * This is a protected and abstract member.
+ * @protected
+ * @abstract
+ * @type {ObjectLiteral}
+ * @memberof BaseEntityManager
+ */
+ protected abstract mongoEntity?: ObjectLiteral;
+
+ /**
+ * Creates an instance of BaseEntityManager.
+ * @param {DBWrapper} dbWrapper The DBWrapper instance to be used in this class.
+ * @memberof BaseEntityManager
+ */
+ public constructor(dbWrapper: DBWrapper) {
+ BaseEntityManager.dbWrapper = dbWrapper;
+ }
+
+ /**
+ * Initializes the repositories for MySQL, SQLite, and MongoDB.
+ * This method should be called before any other method in this class is invoked.
+ * @param {EntityTarget} MySQLEntity The MySQL entity to be used in this class.
+ * @param {EntityTarget} SQLiteEntity The SQLite entity to be used in this class.
+ * @param {EntityTarget} MongoEntity The MongoDB entity to be used in this class.
+ * @protected
+ * @static
+ * @memberof BaseEntityManager
+ */
+ protected static initRepositories(
+ MySQLEntity: EntityTarget,
+ SQLiteEntity: EntityTarget,
+ MongoEntity: EntityTarget,
+ ): void {
+ if (!BaseEntityManager.dbWrapper) {
+ BaseEntityManager.dbWrapper = DBWrapper.getInstance();
+ }
+
+ BaseEntityManager.mySqlRepo =
+ BaseEntityManager.dbWrapper.retrieveRepository(
+ MySQLEntity,
+ BaseEntityManager.dbWrapper.MySqlConnection,
+ );
+ BaseEntityManager.sqliteRepo =
+ BaseEntityManager.dbWrapper.retrieveRepository(
+ SQLiteEntity,
+ BaseEntityManager.dbWrapper.SqliteConnection,
+ );
+ BaseEntityManager.mongoRepo =
+ BaseEntityManager.dbWrapper.retrieveRepository(
+ MongoEntity,
+ BaseEntityManager.dbWrapper.MongoConnection,
+ ) as MongoRepository;
+ }
+
+ /**
+ * Builds the entity from the database(s).
+ * Each child class also implements a static `build` method that can be used to directly build the entity.
+ *
+ * @returns {Promise} The built entity.
+ */
+ public abstract build(): Promise;
+
+ /**
+ * Rebuilds the entity from the database(s).
+ *
+ * @returns {Promise} A promise that resolves when the entity has been rebuilt.
+ */
+ public abstract rebuild(): Promise;
+
+ /**
+ * Saves the entity to the database(s).
+ *
+ * @returns {Promise<{
+ * mySqlRepo?: Result;
+ * sqliteRepo?: Result;
+ * mongoRepo?: Result;
+ * }>} The results of the save operation. For each repository:
+ * - If the operation was successful, the status will be `Success` and the `data` will be the saved object.
+ * - If the operation was unsuccessful, the status will be `Fail` and the `error` will be the error that occurred.
+ * - If `undefined` is returned, it means that the repository was not initialized and the operation was not performed.
+ */
+ public abstract save(): Promise<{
+ mySqlRepo?: Result;
+ sqliteRepo?: Result;
+ mongoRepo?: Result;
+ }>;
+
+ /**
+ * Removes the entity from the database(s).
+ *
+ * @returns {Promise<{
+ * mySqlRepo?: Result;
+ * sqliteRepo?: Result;
+ * mongoRepo?: Result;
+ * }>} The results of the remove operation. For each repository:
+ * - If the operation was successful, the status will be `Success` and the `data` will be the removed object.
+ * - If the operation was unsuccessful, the status will be `Fail` and the `error` will be the error that occurred.
+ * - If `undefined` is returned, it means that the repository was not initialized and the operation was not performed.
+ */
+ public abstract remove(): Promise<{
+ mySqlRepo?: Result;
+ sqliteRepo?: Result;
+ mongoRepo?: Result;
+ }>;
+
+ /**
+ * Removes all entities from the database(s).
+ * Each child class also implements a static `removeAll` method that can be used to remove all entities.
+ *
+ * @returns {Promise<{
+ * mySqlRepo?: Result;
+ * sqliteRepo?: Result;
+ * mongoRepo?: Result;
+ * }>} The results of the remove operation. For each repository:
+ * - If the operation was successful, the status will be `Success` and there is no `data`.
+ * - If the operation was unsuccessful, the status will be `Fail` and the `error` will be the error that occurred.
+ * - If `undefined` is returned, it means that the repository was not initialized and the operation was not performed.
+ */
+ public abstract removeAll(): Promise<{
+ mySqlRepo?: Result;
+ sqliteRepo?: Result;
+ mongoRepo?: Result;
+ }>;
+
+ /**
+ * Finds the entity in the database(s) using the specified options defined in the child class.
+ * Each child class also implements a static `find` method that can be used to find the entity by passing it the options defined in the child class.
+ *
+ * @returns {Promise<{
+ * mySqlRepo?: Result;
+ * sqliteRepo?: Result;
+ * mongoRepo?: Result;
+ * }>} The results of the find operation. For each repository:
+ * - If the operation was successful, the status will be `Success` and the `data` will be the found object.
+ * - If the operation was unsuccessful, the status will be `Fail` and the `error` will be the error that occurred.
+ * - If `undefined` is returned, it means that the repository was not initialized and the operation was not performed.
+ */
+ public abstract find(): Promise<{
+ mySqlRepo?: Result;
+ sqliteRepo?: Result;
+ mongoRepo?: Result;
+ }>;
+
+ /**
+ * Finds all entities in the database(s).
+ * Each child class also implements a static `findAll` method that can be used to find all entities.
+ *
+ * @returns {Promise<{
+ * mySqlRepo?: Result;
+ * sqliteRepo?: Result;
+ * mongoRepo?: Result;
+ * }>} The results of the find operation. For each repository:
+ * - If the operation was successful, the status will be `Success` and the `data` will be the found objects.
+ * - If the operation was unsuccessful, the status will be `Fail` and the `error` will be the error that occurred.
+ * - If `undefined` is returned, it means that the repository was not initialized and the operation was not performed.
+ */
+ public abstract findAll(): Promise<{
+ mySqlRepo?: Result;
+ sqliteRepo?: Result;
+ mongoRepo?: Result;
+ }>;
+}
+
+export default BaseEntityManager;
+export {BaseEntityManager};
diff --git a/src/entities/Game.ts b/src/entities/Game.ts
new file mode 100644
index 0000000..4d44919
--- /dev/null
+++ b/src/entities/Game.ts
@@ -0,0 +1,1887 @@
+import DBWrapper from "../db/DBWrapper";
+import Result, {ResultStatus} from "../db/Result";
+import BaseEntityManager from "./BaseEntityManager";
+import {MySQLPunto, MySQLCard, MySQLPuntoPlayer} from "./Game/MySQLGame";
+import {SQLitePunto, SQLiteCard, SQLitePuntoPlayer} from "./Game/SQLiteGame";
+import MongoPunto, {MongoCard, MongoPuntoPlayer} from "./Game/MongoGame";
+
+import Cards from "../game/Card";
+import UserManager from "./User";
+import Board from "../game/Board";
+
+/**
+ * The MySQL entity used in this class.
+ * It is used to type the {@link GameManager.mySqlEntity} member.
+ * It's composed of the MySQL entities of the punto, cards, and players.
+ * @property {MySQLPunto} punto The MySQL entity of the punto.
+ * @property {MySQLCard[]} cards The MySQL entities of the cards.
+ * @property {MySQLPuntoPlayer[]} players The MySQL entities of the players.
+ * @memberof GameManager
+ */
+type MySQLGameEntity = {
+ punto: MySQLPunto;
+ cards: MySQLCard[];
+ players: MySQLPuntoPlayer[];
+};
+
+/**
+ * The SQLite entity used in this class.
+ * It is used to type the {@link GameManager.sqliteEntity} member.
+ * It's composed of the SQLite entities of the punto, cards, and players.
+ * @property {SQLitePunto} punto The SQLite entity of the punto.
+ * @property {SQLiteCard[]} cards The SQLite entities of the cards.
+ * @property {SQLitePuntoPlayer[]} players The SQLite entities of the players.
+ * @memberof GameManager
+ */
+type SQLiteGameEntity = {
+ punto: SQLitePunto;
+ cards: SQLiteCard[];
+ players: SQLitePuntoPlayer[];
+};
+
+class GameManager extends BaseEntityManager {
+ /**
+ * The MySQL entity used in this class. Replace {@link BaseEntityManager.mySqlEntity} with the specific MySQL entity for this class.
+ * This is a protected member.
+ * @protected
+ * @type {MySQLGameEntity}
+ * @memberof GameManager
+ */
+ protected mySqlEntity?: MySQLGameEntity;
+
+ /**
+ * The SQLite entity used in this class. Replace {@link BaseEntityManager.sqliteEntity} with the specific SQLite entity for this class.
+ * This is a protected member.
+ * @protected
+ * @type {SQLiteGameEntity}
+ * @memberof GameManager
+ */
+ protected sqliteEntity?: SQLiteGameEntity;
+
+ /**
+ * The MongoDB entity used in this class. Replace {@link BaseEntityManager.mongoEntity} with the specific MongoDB entity for this class.
+ * This is a protected member.
+ * @protected
+ * @type {MongoUser}
+ * @memberof GameManager
+ */
+ protected mongoEntity?: MongoPunto;
+
+ /**
+ * Flag that indicates if the entities were built with the {@link GameManager.buildEntities} method.
+ * @type {boolean}
+ * @memberof GameManager
+ * @private
+ */
+ private unbuiltEntities: boolean = true;
+
+ /**
+ * The type of the game repository to be used. If not specified, the default Punto repository will be used.
+ * Specify `GameRepoType.Punto` for punto repository, `GameRepoType.Player` for player repository, and `GameRepoType.Card` for card repository.
+ * You can specify nothing for punto repository. (For MongoDB, the punto repository is the only repository that is used.)
+ * @type {GameRepoType}
+ * @memberof GameManager
+ * @static
+ * @private
+ */
+ private static gameRepoType?: GameRepoType;
+
+ /**
+ * Creates an instance of GameManager.
+ * After creating an instance of this class, you should call {@link GameManager.buildEntities} to build the entities.
+ *
+ * @param {DBWrapper} dbWrapper The DBWrapper instance to be used in this class.
+ * @memberof GameManager
+ *
+ * @example
+ * const gameManager = new GameManager(dbWrapper);
+ *
+ * gameManager.buildEntities(cards, players, winType, playersStatus);
+ *
+ * // You can now uses the DB methods
+ * gameManager.save();
+ */
+ public constructor(dbWrapper: DBWrapper) {
+ super(dbWrapper);
+
+ GameManager.initRepositories();
+ }
+
+ /**
+ * Builds the entities of the punto from the given board.
+ * This method should be called before any other DB method is invoked like {@link GameManager.save}.
+ * When the entities are built, you cannot call this method again and update the entities.
+ *
+ * @param {Board} board The board of the punto.
+ * @returns {Promise} A promise that resolves when the entities are built.
+ *
+ * @example
+ * const gameManager = new GameManager(dbWrapper);
+ *
+ * await gameManager.buildEntities(board);
+ *
+ * // You can now uses the DB methods
+ * await gameManager.save();
+ */
+ public async buildEntities(board: Board): Promise {
+ if (!this.unbuiltEntities) {
+ return;
+ }
+
+ const players = board.players;
+
+ const winners = board.winners;
+ const losers = board.losers;
+
+ const cards = board.cards;
+
+ const winType = board.winType;
+
+ const distributedCardsByPlayer: {[key: string]: Cards[]} = {};
+
+ // Distribute the cards by player
+ for (const card of cards) {
+ const playerId = card.playedBy?.id ? card.playedBy.id : "none";
+
+ if (!distributedCardsByPlayer[playerId]) {
+ distributedCardsByPlayer[playerId] = [];
+ }
+
+ distributedCardsByPlayer[playerId].push(card);
+ }
+
+ const mySqlPuntoPlayers: MySQLPuntoPlayer[] = [];
+ const mySqlCards: MySQLCard[] = [];
+
+ const sqlitePuntoPlayers: SQLitePuntoPlayer[] = [];
+ const sqliteCards: SQLiteCard[] = [];
+
+ const mongoPuntoPlayers: MongoPuntoPlayer[] = [];
+ const mongoCards: MongoCard[] = [];
+
+ for (const player of players) {
+ const playerId = player.id;
+
+ const name = player.name;
+ const points = player.points;
+ const status = winners.includes(player)
+ ? "winner"
+ : losers.includes(player)
+ ? "loser"
+ : "none";
+
+ // Retrieve the player ID from the database(s)
+
+ const findResult = await UserManager.find(name);
+
+ const mySqlData = Array.isArray(findResult.mySqlRepo?.data)
+ ? findResult.mySqlRepo?.data[0]
+ : undefined;
+ const sqliteData = Array.isArray(findResult.sqliteRepo?.data)
+ ? findResult.sqliteRepo?.data[0]
+ : undefined;
+ const mongoData = Array.isArray(findResult.mongoRepo?.data)
+ ? findResult.mongoRepo?.data[0]
+ : undefined;
+
+ let mySqlPlayerID: number | undefined;
+ let sqlitePlayerID: number | undefined;
+ // let mongoPlayerID: ObjectId | undefined;
+ let mongoPlayerID: string | undefined;
+
+ let finded = false;
+
+ if (mySqlData && sqliteData && mongoData) {
+ finded = true;
+ }
+
+ if (finded) {
+ const user = await UserManager.build(name);
+
+ mySqlPlayerID = user.mySqlId;
+ sqlitePlayerID = user.sqliteId;
+ // mongoPlayerID = user.mongoId;
+ mongoPlayerID = user.name;
+ } else {
+ // If the user is not found or is not in all databases
+
+ // If the user is not found in any database, create a new user
+ if (!mySqlData && !sqliteData && !mongoData) {
+ const user = new UserManager(GameManager.dbWrapper, name);
+
+ await user.save();
+
+ mySqlPlayerID = user.mySqlId;
+ sqlitePlayerID = user.sqliteId;
+ // mongoPlayerID = user.mongoId;
+ mongoPlayerID = user.name;
+ }
+ // If the user is found in at least one database, but not in all databases
+ else {
+ // Build the user from the database(s)
+ const user = await UserManager.build(name);
+
+ // Rebuild the user for the databases that it is not found in
+ await user.rebuild();
+
+ // Add the user to the databases that it is not found in
+ await user.save();
+
+ mySqlPlayerID = user.mySqlId;
+ sqlitePlayerID = user.sqliteId;
+ // mongoPlayerID = user.mongoId;
+ mongoPlayerID = user.name;
+ }
+ }
+
+ if (mySqlPlayerID) {
+ const mySqlPuntoPlayer = new MySQLPuntoPlayer(
+ mySqlPlayerID,
+ points,
+ status,
+ );
+ mySqlPuntoPlayers.push(mySqlPuntoPlayer);
+ }
+
+ if (sqlitePlayerID) {
+ const sqlitePuntoPlayer = new SQLitePuntoPlayer(
+ sqlitePlayerID,
+ points,
+ status,
+ );
+ sqlitePuntoPlayers.push(sqlitePuntoPlayer);
+ }
+
+ if (mongoPlayerID) {
+ const mongoPuntoPlayer = new MongoPuntoPlayer(
+ mongoPlayerID,
+ points,
+ status,
+ );
+
+ mongoPuntoPlayers.push(mongoPuntoPlayer);
+ }
+
+ // If there are cards that are played by the player
+ if (distributedCardsByPlayer[playerId] !== undefined) {
+ // Construct the cards of the player
+ for (const card of distributedCardsByPlayer[playerId]) {
+ const x = card.x;
+ const y = card.y;
+ const color = card.color;
+ const value = card.value;
+ const playedTurn = card.playedTurn;
+ const playedIn = card.playedIn;
+
+ const mySqlPlayedBy = mySqlPlayerID;
+ const sqlitePlayedBy = sqlitePlayerID;
+ const mongoPlayedBy = mongoPlayerID;
+
+ const mySqlCard = new MySQLCard(
+ x,
+ y,
+ color,
+ value,
+ playedTurn,
+ playedIn,
+ mySqlPlayedBy,
+ );
+
+ const sqliteCard = new SQLiteCard(
+ x,
+ y,
+ color,
+ value,
+ playedTurn,
+ playedIn,
+ sqlitePlayedBy,
+ );
+
+ const mongoCard = new MongoCard(
+ x,
+ y,
+ color,
+ value,
+ playedTurn,
+ playedIn,
+ mongoPlayedBy,
+ );
+
+ mySqlCards.push(mySqlCard);
+ sqliteCards.push(sqliteCard);
+ mongoCards.push(mongoCard);
+ }
+ }
+
+ // If there are cards that are not played
+ if (distributedCardsByPlayer["none"] !== undefined) {
+ // Adds unplayed cards (playerId = "none")
+ for (const card of distributedCardsByPlayer["none"]) {
+ const x = card.x;
+ const y = card.y;
+ const color = card.color;
+ const value = card.value;
+ const playedTurn = card.playedTurn;
+ const playedIn = card.playedIn;
+ const playedBy = undefined;
+
+ const mySqlCard = new MySQLCard(
+ x,
+ y,
+ color,
+ value,
+ playedTurn,
+ playedIn,
+ playedBy,
+ );
+
+ const sqliteCard = new SQLiteCard(
+ x,
+ y,
+ color,
+ value,
+ playedTurn,
+ playedIn,
+ playedBy,
+ );
+
+ const mongoCard = new MongoCard(
+ x,
+ y,
+ color,
+ value,
+ playedTurn,
+ playedIn,
+ playedBy,
+ );
+
+ mySqlCards.push(mySqlCard);
+ sqliteCards.push(sqliteCard);
+ mongoCards.push(mongoCard);
+ }
+ }
+ }
+
+ this.mySqlEntity = {
+ punto: new MySQLPunto(winType),
+ cards: mySqlCards,
+ players: mySqlPuntoPlayers,
+ };
+
+ this.sqliteEntity = {
+ punto: new SQLitePunto(winType),
+ cards: sqliteCards,
+ players: sqlitePuntoPlayers,
+ };
+
+ this.mongoEntity = new MongoPunto(
+ mongoCards,
+ mongoPuntoPlayers,
+ winType,
+ );
+
+ this.unbuiltEntities = false;
+ }
+
+ /**
+ * Initializes the repositories for MySQL, SQLite, and MongoDB.
+ * This method should be called before any other method in this class is invoked.
+ * For change the type of the game repository to be used, change the {@link GameManager.gameRepoType} member.
+ *
+ * @example
+ * // For punto repository
+ * GameManager.gameRepoType = GameRepoType.Punto;
+ * GameManager.initRepositories();
+ *
+ * // For player repository
+ * GameManager.gameRepoType = GameRepoType.Player;
+ * GameManager.initRepositories();
+ *
+ * // For card repository
+ * GameManager.gameRepoType = GameRepoType.Card;
+ * GameManager.initRepositories();
+ *
+ * @protected
+ * @static
+ * @memberof GameManager
+ */
+ protected static initRepositories(): void {
+ switch (GameManager.gameRepoType) {
+ case GameRepoType.Punto:
+ BaseEntityManager.initRepositories(
+ MySQLPunto,
+ SQLitePunto,
+ MongoPunto,
+ );
+ break;
+
+ case GameRepoType.Player:
+ BaseEntityManager.initRepositories(
+ MySQLPuntoPlayer,
+ SQLitePuntoPlayer,
+ MongoPunto,
+ );
+ break;
+
+ case GameRepoType.Card:
+ BaseEntityManager.initRepositories(
+ MySQLCard,
+ SQLiteCard,
+ MongoPunto,
+ );
+ break;
+
+ default:
+ BaseEntityManager.initRepositories(
+ MySQLPunto,
+ SQLitePunto,
+ MongoPunto,
+ );
+ }
+ }
+
+ /**
+ * Builds a punto from the database(s).
+ * This builds a punto from the database(s) and returns it.
+ * The puntos constructed are not necessarily the same,
+ * because only the first punto returned by the {@link GameManager.find} method is used.
+ *
+ * @returns {Promise} The built punto. It is the same as calling {@link GameManager.build}.
+ */
+ public async build(): Promise {
+ return await GameManager.build();
+ }
+
+ /**
+ * Builds a punto from the database(s).
+ * This builds a punto from the database(s) and returns it.
+ * The puntos constructed are not necessarily the same,
+ * because only the first punto returned by the {@link GameManager.find} method is used.
+ *
+ * @returns {Promise} The built punto.
+ */
+ public static async build(): Promise {
+ GameManager.initRepositories();
+
+ const buildedPunto = new GameManager(GameManager.dbWrapper);
+
+ const puntosFromDBs = await GameManager.find();
+
+ const mySqlPuntoData = puntosFromDBs.mySqlRepo?.data;
+ const sqlitePuntoData = puntosFromDBs.sqliteRepo?.data;
+ const mongoPuntoData = puntosFromDBs.mongoRepo?.data;
+
+ if (
+ mySqlPuntoData &&
+ Array.isArray(mySqlPuntoData) &&
+ mySqlPuntoData.length > 0
+ ) {
+ const mySqlPunto = mySqlPuntoData[0];
+
+ buildedPunto.mySqlEntity = mySqlPunto;
+ }
+
+ if (
+ sqlitePuntoData &&
+ Array.isArray(sqlitePuntoData) &&
+ sqlitePuntoData.length > 0
+ ) {
+ const sqlitePunto = sqlitePuntoData[0];
+
+ buildedPunto.sqliteEntity = sqlitePunto;
+ }
+
+ if (
+ mongoPuntoData &&
+ Array.isArray(mongoPuntoData) &&
+ mongoPuntoData.length > 0
+ ) {
+ const mongoPunto = mongoPuntoData[0];
+
+ buildedPunto.mongoEntity = mongoPunto;
+ }
+
+ if (
+ buildedPunto.mySqlEntity &&
+ buildedPunto.sqliteEntity &&
+ buildedPunto.mongoEntity
+ ) {
+ buildedPunto.unbuiltEntities = false;
+ }
+
+ return buildedPunto;
+ }
+
+ /**
+ * Base : Rebuilds the punto from the database(s).
+ * This method should not be called beacaue it has no sence for punto and it throws an error.
+ *
+ * @returns {Promise} A promise that resolves when the punto is rebuilt.
+ * @throws {Error} Always throws an error because this method has no sence for punto.
+ * @memberof GameManager
+ */
+ public async rebuild(): Promise {
+ throw new Error("Method have non sence for punto");
+ }
+
+ /**
+ * Saves the punto in the database(s).
+ *
+ * @returns {Promise<{
+ * mySqlRepo?: Result;
+ * sqliteRepo?: Result;
+ * mongoRepo?: Result;
+ * }>} The results of the save operation. For each repository:
+ * - If the operation was successful, the status will be `Success` and the `data` will be the saved object.
+ * - If the operation was unsuccessful, the status will be `Fail` and the `error` will be the error that occurred.
+ * - If `undefined` is returned, it means that the repository was not initialized and the operation was not performed.
+ */
+ public async save(): Promise<{
+ mySqlRepo?: Result;
+ sqliteRepo?: Result;
+ mongoRepo?: Result;
+ }> {
+ if (this.unbuiltEntities) {
+ throw new Error(
+ "You should call the buildEntities method before saving the punto.",
+ );
+ }
+
+ GameManager.gameRepoType = GameRepoType.Punto;
+ GameManager.initRepositories();
+
+ const results: {
+ mySqlRepo?: Result;
+ sqliteRepo?: Result;
+ mongoRepo?: Result;
+ } = {
+ mySqlRepo: undefined,
+ sqliteRepo: undefined,
+ mongoRepo: undefined,
+ };
+
+ if (this.mySqlEntity && GameManager.mySqlRepo) {
+ let puntoResult: Result | undefined;
+ let cardsResult: Result | undefined;
+ let playersResult: Result | undefined;
+
+ GameManager.gameRepoType = GameRepoType.Punto;
+ GameManager.initRepositories();
+
+ await GameManager.mySqlRepo
+ .save(this.mySqlEntity.punto)
+ .then((result) => {
+ puntoResult = new Result({
+ status: result
+ ? ResultStatus.Success
+ : ResultStatus.Fail,
+ data: result,
+ });
+ })
+ .catch((error) => {
+ puntoResult = new Result({
+ status: ResultStatus.Fail,
+ error,
+ });
+ });
+
+ for (const player of this.mySqlEntity.players) {
+ player.boardId = this.mySqlEntity.punto._id;
+ }
+
+ for (const card of this.mySqlEntity.cards) {
+ card.boardId = this.mySqlEntity.punto._id;
+ }
+
+ GameManager.gameRepoType = GameRepoType.Player;
+ GameManager.initRepositories();
+
+ await GameManager.mySqlRepo
+ .save(this.mySqlEntity.players)
+ .then((result) => {
+ playersResult = new Result({
+ status: result
+ ? ResultStatus.Success
+ : ResultStatus.Fail,
+ data: result,
+ });
+ })
+ .catch((error) => {
+ playersResult = new Result({
+ status: ResultStatus.Fail,
+ error,
+ });
+ });
+
+ GameManager.gameRepoType = GameRepoType.Card;
+ GameManager.initRepositories();
+
+ await GameManager.mySqlRepo
+ .save(this.mySqlEntity.cards)
+ .then((result) => {
+ cardsResult = new Result({
+ status: result
+ ? ResultStatus.Success
+ : ResultStatus.Fail,
+ data: result,
+ });
+ })
+ .catch((error) => {
+ cardsResult = new Result({
+ status: ResultStatus.Fail,
+ error,
+ });
+ });
+
+ let data = {};
+ let errors = {};
+
+ if (puntoResult?.data) {
+ data = {
+ ...data,
+ punto: puntoResult.data,
+ };
+ }
+ if (cardsResult?.data) {
+ data = {
+ ...data,
+ cards: cardsResult.data,
+ };
+ }
+ if (playersResult?.data) {
+ data = {
+ ...data,
+ players: playersResult.data,
+ };
+ }
+
+ if (puntoResult?.error) {
+ errors = {
+ ...errors,
+ punto: puntoResult.error,
+ };
+ }
+ if (cardsResult?.error) {
+ errors = {
+ ...errors,
+ cards: cardsResult.error,
+ };
+ }
+ if (playersResult?.error) {
+ errors = {
+ ...errors,
+ players: playersResult.error,
+ };
+ }
+
+ results.mySqlRepo = new Result({
+ status:
+ puntoResult?.status === ResultStatus.Success &&
+ cardsResult?.status === ResultStatus.Success &&
+ playersResult?.status === ResultStatus.Success
+ ? ResultStatus.Success
+ : ResultStatus.Fail,
+ data: Object.keys(data).length !== 0 ? data : undefined,
+ error: Object.keys(errors).length !== 0 ? errors : undefined,
+ });
+ }
+
+ if (this.sqliteEntity && GameManager.sqliteRepo) {
+ let puntoResult: Result | undefined;
+ let cardsResult: Result | undefined;
+ let playersResult: Result | undefined;
+
+ GameManager.gameRepoType = GameRepoType.Punto;
+ GameManager.initRepositories();
+
+ await GameManager.sqliteRepo
+ .save(this.sqliteEntity.punto)
+ .then((result) => {
+ puntoResult = new Result({
+ status: result
+ ? ResultStatus.Success
+ : ResultStatus.Fail,
+ data: result,
+ });
+ })
+ .catch((error) => {
+ puntoResult = new Result({
+ status: ResultStatus.Fail,
+ error,
+ });
+ });
+
+ const thisBoardId = this.sqliteEntity.punto._id;
+
+ for (const player of this.sqliteEntity.players) {
+ player.boardId = thisBoardId;
+ }
+
+ for (const card of this.sqliteEntity.cards) {
+ card.boardId = thisBoardId;
+ }
+
+ GameManager.gameRepoType = GameRepoType.Player;
+ GameManager.initRepositories();
+
+ await GameManager.sqliteRepo
+ .save(this.sqliteEntity.players)
+ .then((result) => {
+ playersResult = new Result({
+ status: result
+ ? ResultStatus.Success
+ : ResultStatus.Fail,
+ data: result,
+ });
+ })
+ .catch((error) => {
+ playersResult = new Result({
+ status: ResultStatus.Fail,
+ error,
+ });
+ });
+
+ GameManager.gameRepoType = GameRepoType.Card;
+ GameManager.initRepositories();
+
+ await GameManager.sqliteRepo
+ .save(this.sqliteEntity.cards)
+ .then((result) => {
+ cardsResult = new Result({
+ status: result
+ ? ResultStatus.Success
+ : ResultStatus.Fail,
+ data: result,
+ });
+ })
+ .catch((error) => {
+ cardsResult = new Result({
+ status: ResultStatus.Fail,
+ error,
+ });
+ });
+
+ let data = {};
+ let errors = {};
+
+ if (puntoResult?.data) {
+ data = {
+ ...data,
+ punto: puntoResult.data,
+ };
+ }
+ if (cardsResult?.data) {
+ data = {
+ ...data,
+ cards: cardsResult.data,
+ };
+ }
+ if (playersResult?.data) {
+ data = {
+ ...data,
+ players: playersResult.data,
+ };
+ }
+
+ if (puntoResult?.error) {
+ errors = {
+ ...errors,
+ punto: puntoResult.error,
+ };
+ }
+ if (cardsResult?.error) {
+ errors = {
+ ...errors,
+ cards: cardsResult.error,
+ };
+ }
+ if (playersResult?.error) {
+ errors = {
+ ...errors,
+ players: playersResult.error,
+ };
+ }
+
+ results.sqliteRepo = new Result({
+ status:
+ puntoResult?.status === ResultStatus.Success &&
+ cardsResult?.status === ResultStatus.Success &&
+ playersResult?.status === ResultStatus.Success
+ ? ResultStatus.Success
+ : ResultStatus.Fail,
+ data: Object.keys(data).length !== 0 ? data : undefined,
+ error: Object.keys(errors).length !== 0 ? errors : undefined,
+ });
+ }
+
+ if (this.mongoEntity && GameManager.mongoRepo) {
+ // GameManager.gameRepoType = GameRepoType.Punto;
+ // GameManager.initRepositories();
+
+ await GameManager.mongoRepo
+ .save(this.mongoEntity)
+ .then((result) => {
+ results.mongoRepo = new Result({
+ status: result
+ ? ResultStatus.Success
+ : ResultStatus.Fail,
+ data: result,
+ });
+ })
+ .catch((error) => {
+ results.mongoRepo = new Result({
+ status: ResultStatus.Fail,
+ error,
+ });
+ });
+ }
+
+ return results;
+ }
+
+ /**
+ * Removes the punto from the database(s).
+ *
+ * @returns {Promise<{
+ * mySqlRepo?: Result;
+ * sqliteRepo?: Result;
+ * mongoRepo?: Result;
+ * }>} The results of the remove operation. For each repository:
+ * - If the operation was successful, the status will be `Success` and the `data` will be the removed object.
+ * - If the operation was unsuccessful, the status will be `Fail` and the `error` will be the error that occurred.
+ * - If `undefined` is returned, it means that the repository was not initialized and the operation was not performed.
+ */
+ public async remove(): Promise<{
+ mySqlRepo?: Result;
+ sqliteRepo?: Result;
+ mongoRepo?: Result;
+ }> {
+ if (this.unbuiltEntities) {
+ throw new Error(
+ "You should call the buildEntities method before removing the punto.",
+ );
+ }
+
+ GameManager.gameRepoType = GameRepoType.Punto;
+ GameManager.initRepositories();
+
+ const results: {
+ mySqlRepo?: Result;
+ sqliteRepo?: Result;
+ mongoRepo?: Result;
+ } = {
+ mySqlRepo: undefined,
+ sqliteRepo: undefined,
+ mongoRepo: undefined,
+ };
+
+ if (this.mySqlEntity && GameManager.mySqlRepo) {
+ let puntoResult: Result | undefined;
+ let cardsResult: Result | undefined;
+ let playersResult: Result | undefined;
+
+ GameManager.gameRepoType = GameRepoType.Punto;
+ GameManager.initRepositories();
+
+ await GameManager.mySqlRepo
+ .remove(this.mySqlEntity.punto)
+ .then((result) => {
+ puntoResult = new Result({
+ status: result
+ ? ResultStatus.Success
+ : ResultStatus.Fail,
+ data: result,
+ });
+ })
+ .catch((error) => {
+ puntoResult = new Result({
+ status: ResultStatus.Fail,
+ error,
+ });
+ });
+
+ GameManager.gameRepoType = GameRepoType.Player;
+ GameManager.initRepositories();
+
+ await GameManager.mySqlRepo
+ .remove(this.mySqlEntity.players)
+ .then((result) => {
+ playersResult = new Result({
+ status: result
+ ? ResultStatus.Success
+ : ResultStatus.Fail,
+ data: result,
+ });
+ })
+ .catch((error) => {
+ playersResult = new Result({
+ status: ResultStatus.Fail,
+ error,
+ });
+ });
+
+ GameManager.gameRepoType = GameRepoType.Card;
+ GameManager.initRepositories();
+
+ await GameManager.mySqlRepo
+ .remove(this.mySqlEntity.cards)
+ .then((result) => {
+ cardsResult = new Result({
+ status: result
+ ? ResultStatus.Success
+ : ResultStatus.Fail,
+ data: result,
+ });
+ })
+ .catch((error) => {
+ cardsResult = new Result({
+ status: ResultStatus.Fail,
+ error,
+ });
+ });
+
+ let data = {};
+ let errors = {};
+
+ if (puntoResult?.data) {
+ data = {
+ ...data,
+ punto: puntoResult.data,
+ };
+ }
+ if (cardsResult?.data) {
+ data = {
+ ...data,
+ cards: cardsResult.data,
+ };
+ }
+ if (playersResult?.data) {
+ data = {
+ ...data,
+ players: playersResult.data,
+ };
+ }
+
+ if (puntoResult?.error) {
+ errors = {
+ ...errors,
+ punto: puntoResult.error,
+ };
+ }
+ if (cardsResult?.error) {
+ errors = {
+ ...errors,
+ cards: cardsResult.error,
+ };
+ }
+ if (playersResult?.error) {
+ errors = {
+ ...errors,
+ players: playersResult.error,
+ };
+ }
+
+ results.mySqlRepo = new Result({
+ status:
+ puntoResult?.status === ResultStatus.Success &&
+ cardsResult?.status === ResultStatus.Success &&
+ playersResult?.status === ResultStatus.Success
+ ? ResultStatus.Success
+ : ResultStatus.Fail,
+ data: Object.keys(data).length !== 0 ? data : undefined,
+ error: Object.keys(errors).length !== 0 ? errors : undefined,
+ });
+ }
+
+ if (this.sqliteEntity && GameManager.sqliteRepo) {
+ let puntoResult: Result | undefined;
+ let cardsResult: Result | undefined;
+ let playersResult: Result | undefined;
+
+ GameManager.gameRepoType = GameRepoType.Punto;
+ GameManager.initRepositories();
+
+ await GameManager.sqliteRepo
+ .remove(this.sqliteEntity.punto)
+ .then((result) => {
+ puntoResult = new Result({
+ status: result
+ ? ResultStatus.Success
+ : ResultStatus.Fail,
+ data: result,
+ });
+ })
+ .catch((error) => {
+ puntoResult = new Result({
+ status: ResultStatus.Fail,
+ error,
+ });
+ });
+
+ GameManager.gameRepoType = GameRepoType.Player;
+ GameManager.initRepositories();
+
+ await GameManager.sqliteRepo
+ .remove(this.sqliteEntity.players)
+ .then((result) => {
+ playersResult = new Result({
+ status: result
+ ? ResultStatus.Success
+ : ResultStatus.Fail,
+ data: result,
+ });
+ })
+ .catch((error) => {
+ playersResult = new Result({
+ status: ResultStatus.Fail,
+ error,
+ });
+ });
+
+ GameManager.gameRepoType = GameRepoType.Card;
+ GameManager.initRepositories();
+
+ await GameManager.sqliteRepo
+ .remove(this.sqliteEntity.cards)
+ .then((result) => {
+ cardsResult = new Result({
+ status: result
+ ? ResultStatus.Success
+ : ResultStatus.Fail,
+ data: result,
+ });
+ })
+ .catch((error) => {
+ cardsResult = new Result({
+ status: ResultStatus.Fail,
+ error,
+ });
+ });
+
+ let data = {};
+ let errors = {};
+
+ if (puntoResult?.data) {
+ data = {
+ ...data,
+ punto: puntoResult.data,
+ };
+ }
+ if (cardsResult?.data) {
+ data = {
+ ...data,
+ cards: cardsResult.data,
+ };
+ }
+ if (playersResult?.data) {
+ data = {
+ ...data,
+ players: playersResult.data,
+ };
+ }
+
+ if (puntoResult?.error) {
+ errors = {
+ ...errors,
+ punto: puntoResult.error,
+ };
+ }
+ if (cardsResult?.error) {
+ errors = {
+ ...errors,
+ cards: cardsResult.error,
+ };
+ }
+ if (playersResult?.error) {
+ errors = {
+ ...errors,
+ players: playersResult.error,
+ };
+ }
+
+ results.sqliteRepo = new Result({
+ status:
+ puntoResult?.status === ResultStatus.Success &&
+ cardsResult?.status === ResultStatus.Success &&
+ playersResult?.status === ResultStatus.Success
+ ? ResultStatus.Success
+ : ResultStatus.Fail,
+ data: Object.keys(data).length !== 0 ? data : undefined,
+ error: Object.keys(errors).length !== 0 ? errors : undefined,
+ });
+ }
+
+ if (this.mongoEntity && GameManager.mongoRepo) {
+ // GameManager.gameRepoType = GameRepoType.Punto;
+ // GameManager.initRepositories();
+
+ await GameManager.mongoRepo
+ .remove(this.mongoEntity)
+ .then((result) => {
+ console.log("result", result);
+
+ results.mongoRepo = new Result({
+ status: result
+ ? ResultStatus.Success
+ : ResultStatus.Fail,
+ data: result,
+ });
+ })
+ .catch((error) => {
+ console.log("error", error);
+
+ results.mongoRepo = new Result({
+ status: ResultStatus.Fail,
+ error,
+ });
+ });
+ }
+
+ return results;
+ }
+
+ /**
+ * Removes all the puntos from the database(s).
+ * This method is the same as calling {@link GameManager.removeAll}
+ *
+ * @returns {Promise<{
+ * mySqlRepo?: Result;
+ * sqliteRepo?: Result;
+ * mongoRepo?: Result;
+ * }>} The results of the remove operation. For each repository:
+ * - If the operation was successful, the status will be `Success` and there is no `data`.
+ * - If the operation was unsuccessful, the status will be `Fail` and the `error` will be the error that occurred.
+ * - If `undefined` is returned, it means that the repository was not initialized and the operation was not performed.
+ */
+ public async removeAll(): Promise<{
+ mySqlRepo?: Result;
+ sqliteRepo?: Result;
+ mongoRepo?: Result;
+ }> {
+ return GameManager.removeAll();
+ }
+
+ /**
+ * Removes all the puntos from the database(s).
+ *
+ * @returns {Promise<{
+ * mySqlRepo?: Result;
+ * sqliteRepo?: Result;
+ * mongoRepo?: Result;
+ * }>} The results of the remove operation. For each repository:
+ * - If the operation was successful, the status will be `Success` and there is no `data`.
+ * - If the operation was unsuccessful, the status will be `Fail` and the `error` will be the error that occurred.
+ * - If `undefined` is returned, it means that the repository was not initialized and the operation was not performed.
+ */
+ public static async removeAll(): Promise<{
+ mySqlRepo?: Result;
+ sqliteRepo?: Result;
+ mongoRepo?: Result;
+ }> {
+ GameManager.gameRepoType = GameRepoType.Punto;
+ GameManager.initRepositories();
+
+ const results: {
+ mySqlRepo?: Result;
+ sqliteRepo?: Result;
+ mongoRepo?: Result;
+ } = {
+ mySqlRepo: undefined,
+ sqliteRepo: undefined,
+ mongoRepo: undefined,
+ };
+
+ let mySqlPuntoResult: Result | undefined;
+ let mySqlCardsResult: Result | undefined;
+ let mySqlPlayersResult: Result | undefined;
+
+ let sqlitePuntoResult: Result | undefined;
+ let sqliteCardsResult: Result | undefined;
+ let sqlitePlayersResult: Result | undefined;
+
+ await GameManager.mySqlRepo
+ ?.clear()
+ .then(() => {
+ mySqlPuntoResult = new Result({
+ status: ResultStatus.Success,
+ });
+ })
+ .catch((error) => {
+ mySqlPuntoResult = new Result({
+ status: ResultStatus.Fail,
+ error,
+ });
+ });
+
+ await GameManager.sqliteRepo
+ ?.clear()
+ .then(() => {
+ sqlitePuntoResult = new Result({
+ status: ResultStatus.Success,
+ });
+ })
+ .catch((error) => {
+ sqlitePuntoResult = new Result({
+ status: ResultStatus.Fail,
+ error,
+ });
+ });
+
+ await GameManager.mongoRepo
+ ?.clear()
+ .then(() => {
+ results.mongoRepo = new Result({
+ status: ResultStatus.Success,
+ });
+ })
+ .catch((error) => {
+ results.mongoRepo = new Result({
+ status: ResultStatus.Fail,
+ error,
+ });
+ });
+
+ GameManager.gameRepoType = GameRepoType.Player;
+ GameManager.initRepositories();
+
+ await GameManager.mySqlRepo
+ ?.clear()
+ .then(() => {
+ mySqlPlayersResult = new Result({
+ status: ResultStatus.Success,
+ });
+ })
+ .catch((error) => {
+ mySqlPlayersResult = new Result({
+ status: ResultStatus.Fail,
+ error,
+ });
+ });
+
+ await GameManager.sqliteRepo
+ ?.clear()
+ .then(() => {
+ sqlitePlayersResult = new Result({
+ status: ResultStatus.Success,
+ });
+ })
+ .catch((error) => {
+ sqlitePlayersResult = new Result({
+ status: ResultStatus.Fail,
+ error,
+ });
+ });
+
+ GameManager.gameRepoType = GameRepoType.Card;
+ GameManager.initRepositories();
+
+ await GameManager.mySqlRepo
+ ?.clear()
+ .then(() => {
+ mySqlCardsResult = new Result({
+ status: ResultStatus.Success,
+ });
+ })
+ .catch((error) => {
+ mySqlCardsResult = new Result({
+ status: ResultStatus.Fail,
+ error,
+ });
+ });
+
+ await GameManager.sqliteRepo
+ ?.clear()
+ .then(() => {
+ sqliteCardsResult = new Result({
+ status: ResultStatus.Success,
+ });
+ })
+ .catch((error) => {
+ sqliteCardsResult = new Result({
+ status: ResultStatus.Fail,
+ error,
+ });
+ });
+
+ let errors = {};
+
+ if (GameManager.mySqlRepo) {
+ if (mySqlPuntoResult?.error) {
+ errors = {
+ ...errors,
+ mySqlPunto: mySqlPuntoResult.error,
+ };
+ }
+ if (mySqlPlayersResult?.error) {
+ errors = {
+ ...errors,
+ mySqlPlayers: mySqlPlayersResult.error,
+ };
+ }
+ if (mySqlCardsResult?.error) {
+ errors = {
+ ...errors,
+ mySqlCards: mySqlCardsResult.error,
+ };
+ }
+
+ results.mySqlRepo = new Result({
+ status:
+ mySqlPuntoResult?.status === ResultStatus.Success &&
+ mySqlPlayersResult?.status === ResultStatus.Success &&
+ mySqlCardsResult?.status === ResultStatus.Success
+ ? ResultStatus.Success
+ : ResultStatus.Fail,
+ error: Object.keys(errors).length !== 0 ? errors : undefined,
+ });
+ }
+
+ if (GameManager.sqliteRepo) {
+ if (sqlitePuntoResult?.error) {
+ errors = {
+ ...errors,
+ sqlitePunto: sqlitePuntoResult.error,
+ };
+ }
+ if (sqlitePlayersResult?.error) {
+ errors = {
+ ...errors,
+ sqlitePlayers: sqlitePlayersResult.error,
+ };
+ }
+ if (sqliteCardsResult?.error) {
+ errors = {
+ ...errors,
+ sqliteCards: sqliteCardsResult.error,
+ };
+ }
+
+ results.sqliteRepo = new Result({
+ status:
+ sqlitePuntoResult?.status === ResultStatus.Success &&
+ sqlitePlayersResult?.status === ResultStatus.Success &&
+ sqliteCardsResult?.status === ResultStatus.Success
+ ? ResultStatus.Success
+ : ResultStatus.Fail,
+ error: Object.keys(errors).length !== 0 ? errors : undefined,
+ });
+ }
+
+ return results;
+ }
+
+ /**
+ * Finds the punto in the database(s) by its properties.
+ * This method is the same as calling {@link GameManager.find} with the properties of the punto as the parameter
+ * except that this method uses the properties of the punto that calls this method as the parameter.
+ * So, even if no such properties are specified in the actual code,
+ * you must call {@link GameManager.buildEntities} before calling this method.
+ * Also, as there are no properties, you can directly call {@link GameManager.findAll}.
+ *
+ * @example
+ * const punto = new GameManager(dbWrapper);
+ * await punto.buildEntities(board);
+ * const results = await punto.find();
+ *
+ * // is the same as
+ *
+ * const results = await GameManager.find();
+ *
+ * // And the same as
+ *
+ * const results = await GameManager.findAll();
+ *
+ * @returns {Promise<{
+ * mySqlRepo?: Result;
+ * sqliteRepo?: Result;
+ * mongoRepo?: Result;
+ * }>} The results of the find operation. For each repository:
+ * - If the operation was successful, the status will be `Success` and the `data` will be the found object.
+ * - If the operation was unsuccessful, the status will be `Fail` and the `error` will be the error that occurred.
+ * - If `undefined` is returned, it means that the repository was not initialized and the operation was not performed.
+ */
+ public async find(): Promise<{
+ mySqlRepo?: Result;
+ sqliteRepo?: Result;
+ mongoRepo?: Result;
+ }> {
+ if (this.unbuiltEntities) {
+ throw new Error(
+ "You should call the buildEntities method before finding the punto.",
+ );
+ }
+
+ GameManager.initRepositories();
+
+ const results: {
+ mySqlRepo?: Result;
+ sqliteRepo?: Result;
+ mongoRepo?: Result;
+ } = {
+ mySqlRepo: undefined,
+ sqliteRepo: undefined,
+ mongoRepo: undefined,
+ };
+
+ await GameManager.mySqlRepo
+ ?.find({
+ where: {
+ // TODO : add properties
+ },
+ })
+ .then((result) => {
+ results.mySqlRepo = new Result({
+ status: result ? ResultStatus.Success : ResultStatus.Fail,
+ data: result,
+ });
+ })
+ .catch((error) => {
+ results.mySqlRepo = new Result({
+ status: ResultStatus.Fail,
+ error,
+ });
+ });
+
+ await GameManager.sqliteRepo
+ ?.find({
+ where: {
+ // TODO : add properties
+ },
+ })
+ .then((result) => {
+ results.sqliteRepo = new Result({
+ status: result ? ResultStatus.Success : ResultStatus.Fail,
+ data: result,
+ });
+ })
+ .catch((error) => {
+ results.sqliteRepo = new Result({
+ status: ResultStatus.Fail,
+ error,
+ });
+ });
+
+ await GameManager.mongoRepo
+ ?.find({
+ where: {
+ // TODO : add properties
+ },
+ })
+ .then((result) => {
+ results.mongoRepo = new Result({
+ status: result ? ResultStatus.Success : ResultStatus.Fail,
+ data: result,
+ });
+ })
+ .catch((error) => {
+ results.mongoRepo = new Result({
+ status: ResultStatus.Fail,
+ error,
+ });
+ });
+
+ return results;
+ }
+
+ /**
+ * Finds a punto in the database(s) by its properties.
+ * But as there are no properties, you can directly call {@link GameManager.findAll}.
+ *
+ * @example
+ * const results = await GameManager.find();
+ *
+ * // Is the same as
+ *
+ * const results = await GameManager.findAll();
+ *
+ * @returns {Promise<{
+ * mySqlRepo?: Result;
+ * sqliteRepo?: Result;
+ * mongoRepo?: Result;
+ * }>} The results of the find operation. For each repository:
+ * - If the operation was successful, the status will be `Success` and the `data` will be the found object.
+ * - If the operation was unsuccessful, the status will be `Fail` and the `error` will be the error that occurred.
+ * - If `undefined` is returned, it means that the repository was not initialized and the operation was not performed.
+ */
+ public static async find(): Promise<{
+ mySqlRepo?: Result;
+ sqliteRepo?: Result;
+ mongoRepo?: Result;
+ }> {
+ GameManager.initRepositories();
+
+ const results: {
+ mySqlRepo?: Result;
+ sqliteRepo?: Result;
+ mongoRepo?: Result;
+ } = {
+ mySqlRepo: undefined,
+ sqliteRepo: undefined,
+ mongoRepo: undefined,
+ };
+
+ await GameManager.mySqlRepo
+ ?.find({
+ where: {
+ // TODO : add properties
+ },
+ })
+ .then((result) => {
+ results.mySqlRepo = new Result({
+ status: result ? ResultStatus.Success : ResultStatus.Fail,
+ data: result,
+ });
+ })
+ .catch((error) => {
+ results.mySqlRepo = new Result({
+ status: ResultStatus.Fail,
+ error,
+ });
+ });
+
+ await GameManager.sqliteRepo
+ ?.find({
+ where: {
+ // TODO : add properties
+ },
+ })
+ .then((result) => {
+ results.sqliteRepo = new Result({
+ status: result ? ResultStatus.Success : ResultStatus.Fail,
+ data: result,
+ });
+ })
+ .catch((error) => {
+ results.sqliteRepo = new Result({
+ status: ResultStatus.Fail,
+ error,
+ });
+ });
+
+ await GameManager.mongoRepo
+ ?.find({
+ where: {
+ // TODO : add properties
+ },
+ })
+ .then((result) => {
+ results.mongoRepo = new Result({
+ status: result ? ResultStatus.Success : ResultStatus.Fail,
+ data: result,
+ });
+ })
+ .catch((error) => {
+ results.mongoRepo = new Result({
+ status: ResultStatus.Fail,
+ error,
+ });
+ });
+
+ return results;
+ }
+
+ /**
+ * Finds all puntos in the database(s). It is the same as calling {@link GameManager.findAll}.
+ *
+ * @example
+ * const punto = new GameManager(dbWrapper);
+ * const results = await punto.findAll();
+ *
+ * // is the same as
+ *
+ * const results = await GameManager.findAll();
+ *
+ * @returns {Promise<{
+ * mySqlRepo?: Result;
+ * sqliteRepo?: Result;
+ * mongoRepo?: Result;
+ * }>} The results of the find operation. For each repository:
+ * - If the operation was successful, the status will be `Success` and the `data` will be the found objects.
+ * - If the operation was unsuccessful, the status will be `Fail` and the `error` will be the error that occurred.
+ * - If `undefined` is returned, it means that the repository was not initialized and the operation was not performed.
+ */
+ public async findAll(): Promise<{
+ mySqlRepo?: Result;
+ sqliteRepo?: Result;
+ mongoRepo?: Result;
+ }> {
+ return GameManager.findAll();
+ }
+
+ /**
+ * Finds all puntos in the database(s).
+ *
+ * @returns {Promise<{
+ * mySqlRepo?: Result;
+ * sqliteRepo?: Result;
+ * mongoRepo?: Result;
+ * }>} The results of the find operation. For each repository:
+ * - If the operation was successful, the status will be `Success` and the `data` will be the found objects.
+ * - If the operation was unsuccessful, the status will be `Fail` and the `error` will be the error that occurred.
+ * - If `undefined` is returned, it means that the repository was not initialized and the operation was not performed.
+ */
+ public static async findAll(): Promise<{
+ mySqlRepo?: Result;
+ sqliteRepo?: Result;
+ mongoRepo?: Result;
+ }> {
+ GameManager.initRepositories();
+
+ const results: {
+ mySqlRepo?: Result;
+ sqliteRepo?: Result;
+ mongoRepo?: Result;
+ } = {
+ mySqlRepo: undefined,
+ sqliteRepo: undefined,
+ mongoRepo: undefined,
+ };
+
+ let mySqlPuntoResult: Result | undefined;
+ let mySqlCardsResult: Result | undefined;
+ let mySqlPlayersResult: Result | undefined;
+
+ let sqlitePuntoResult: Result | undefined;
+ let sqliteCardsResult: Result | undefined;
+ let sqlitePlayersResult: Result | undefined;
+
+ await GameManager.mySqlRepo
+ ?.find()
+ .then((result) => {
+ // results.mySqlRepo = new Result({
+ // status: result ? ResultStatus.Success : ResultStatus.Fail,
+ // data: result,
+ // });
+ mySqlPuntoResult = new Result({
+ status: result ? ResultStatus.Success : ResultStatus.Fail,
+ data: result,
+ });
+ })
+ .catch((error) => {
+ // results.mySqlRepo = new Result({
+ // status: ResultStatus.Fail,
+ // error,
+ // });
+ mySqlPuntoResult = new Result({
+ status: ResultStatus.Fail,
+ error,
+ });
+ });
+
+ await GameManager.sqliteRepo
+ ?.find()
+ .then((result) => {
+ // results.sqliteRepo = new Result({
+ // status: result ? ResultStatus.Success : ResultStatus.Fail,
+ // data: result,
+ // });
+ sqlitePuntoResult = new Result({
+ status: result ? ResultStatus.Success : ResultStatus.Fail,
+ data: result,
+ });
+ })
+ .catch((error) => {
+ // results.sqliteRepo = new Result({
+ // status: ResultStatus.Fail,
+ // error,
+ // });
+ sqlitePuntoResult = new Result({
+ status: ResultStatus.Fail,
+ error,
+ });
+ });
+
+ await GameManager.mongoRepo
+ ?.find()
+ .then((result) => {
+ results.mongoRepo = new Result({
+ status: result ? ResultStatus.Success : ResultStatus.Fail,
+ data: result,
+ });
+ })
+ .catch((error) => {
+ results.mongoRepo = new Result({
+ status: ResultStatus.Fail,
+ error,
+ });
+ });
+
+ GameManager.gameRepoType = GameRepoType.Player;
+ GameManager.initRepositories();
+
+ await GameManager.mySqlRepo
+ ?.find()
+ .then((result) => {
+ mySqlPlayersResult = new Result({
+ status: result ? ResultStatus.Success : ResultStatus.Fail,
+ data: result,
+ });
+ })
+ .catch((error) => {
+ mySqlPlayersResult = new Result({
+ status: ResultStatus.Fail,
+ error,
+ });
+ });
+
+ await GameManager.sqliteRepo
+ ?.find()
+ .then((result) => {
+ sqlitePlayersResult = new Result({
+ status: result ? ResultStatus.Success : ResultStatus.Fail,
+ data: result,
+ });
+ })
+ .catch((error) => {
+ sqlitePlayersResult = new Result({
+ status: ResultStatus.Fail,
+ error,
+ });
+ });
+
+ GameManager.gameRepoType = GameRepoType.Card;
+ GameManager.initRepositories();
+
+ await GameManager.mySqlRepo
+ ?.find()
+ .then((result) => {
+ mySqlCardsResult = new Result({
+ status: result ? ResultStatus.Success : ResultStatus.Fail,
+ data: result,
+ });
+ })
+ .catch((error) => {
+ mySqlCardsResult = new Result({
+ status: ResultStatus.Fail,
+ error,
+ });
+ });
+
+ await GameManager.sqliteRepo
+ ?.find()
+ .then((result) => {
+ sqliteCardsResult = new Result({
+ status: result ? ResultStatus.Success : ResultStatus.Fail,
+ data: result,
+ });
+ })
+ .catch((error) => {
+ sqliteCardsResult = new Result({
+ status: ResultStatus.Fail,
+ error,
+ });
+ });
+
+ let data = {};
+ let errors = {};
+
+ if (mySqlPuntoResult?.data) {
+ data = {
+ ...data,
+ mySqlPunto: mySqlPuntoResult.data,
+ };
+ }
+ if (mySqlPlayersResult?.data) {
+ data = {
+ ...data,
+ mySqlPlayers: mySqlPlayersResult.data,
+ };
+ }
+ if (mySqlCardsResult?.data) {
+ data = {
+ ...data,
+ mySqlCards: mySqlCardsResult.data,
+ };
+ }
+
+ if (sqlitePuntoResult?.data) {
+ data = {
+ ...data,
+ sqlitePunto: sqlitePuntoResult.data,
+ };
+ }
+ if (sqlitePlayersResult?.data) {
+ data = {
+ ...data,
+ sqlitePlayers: sqlitePlayersResult.data,
+ };
+ }
+ if (sqliteCardsResult?.data) {
+ data = {
+ ...data,
+ sqliteCards: sqliteCardsResult.data,
+ };
+ }
+
+ if (mySqlPuntoResult?.error) {
+ errors = {
+ ...errors,
+ mySqlPunto: mySqlPuntoResult.error,
+ };
+ }
+ if (mySqlPlayersResult?.error) {
+ errors = {
+ ...errors,
+ mySqlPlayers: mySqlPlayersResult.error,
+ };
+ }
+ if (mySqlCardsResult?.error) {
+ errors = {
+ ...errors,
+ mySqlCards: mySqlCardsResult.error,
+ };
+ }
+
+ if (sqlitePuntoResult?.error) {
+ errors = {
+ ...errors,
+ sqlitePunto: sqlitePuntoResult.error,
+ };
+ }
+ if (sqlitePlayersResult?.error) {
+ errors = {
+ ...errors,
+ sqlitePlayers: sqlitePlayersResult.error,
+ };
+ }
+ if (sqliteCardsResult?.error) {
+ errors = {
+ ...errors,
+ sqliteCards: sqliteCardsResult.error,
+ };
+ }
+
+ results.mySqlRepo = new Result({
+ status:
+ mySqlPuntoResult?.status === ResultStatus.Success &&
+ mySqlPlayersResult?.status === ResultStatus.Success &&
+ mySqlCardsResult?.status === ResultStatus.Success
+ ? ResultStatus.Success
+ : ResultStatus.Fail,
+ data: Object.keys(data).length !== 0 ? data : undefined,
+ error: Object.keys(errors).length !== 0 ? errors : undefined,
+ });
+
+ results.sqliteRepo = new Result({
+ status:
+ sqlitePuntoResult?.status === ResultStatus.Success &&
+ sqlitePlayersResult?.status === ResultStatus.Success &&
+ sqliteCardsResult?.status === ResultStatus.Success
+ ? ResultStatus.Success
+ : ResultStatus.Fail,
+ data: Object.keys(data).length !== 0 ? data : undefined,
+ error: Object.keys(errors).length !== 0 ? errors : undefined,
+ });
+
+ return results;
+ }
+}
+
+enum GameRepoType {
+ Punto = "Punto",
+ Player = "Player",
+ Card = "Card",
+}
+
+export default GameManager;
+export {GameRepoType, MySQLGameEntity, SQLiteGameEntity};
diff --git a/src/entities/Game/MongoGame.ts b/src/entities/Game/MongoGame.ts
new file mode 100644
index 0000000..d08b108
--- /dev/null
+++ b/src/entities/Game/MongoGame.ts
@@ -0,0 +1,116 @@
+import {Entity, Column, ObjectIdColumn, ObjectId, Index} from "typeorm";
+
+@Entity({
+ name: "puntos",
+ orderBy: {
+ winType: "ASC",
+ },
+})
+class MongoPunto {
+ @ObjectIdColumn()
+ _id!: ObjectId;
+
+ @Column(() => MongoCard)
+ board: MongoCard[];
+
+ @Column(() => MongoPuntoPlayer)
+ players: MongoPuntoPlayer[];
+
+ @Column()
+ @Index()
+ winType: string;
+
+ constructor(
+ board: MongoCard[],
+ players: MongoPuntoPlayer[],
+ winType: string,
+ ) {
+ this.board = board;
+ this.players = players;
+ this.winType = winType;
+ }
+}
+
+class MongoCard {
+ @Column()
+ x: number;
+
+ @Column()
+ y: number;
+
+ @Column()
+ color: string;
+
+ @Column()
+ value: number;
+
+ @Column()
+ playedTurn: number;
+
+ @Column()
+ playedIn: number;
+
+ // @ObjectIdColumn({
+ // nullable: true,
+ // })
+ // @Column(() => ObjectId)
+ // playedBy!: ObjectId;
+
+ @Column({
+ nullable: true,
+ })
+ playedBy!: string;
+
+ constructor(
+ x: number,
+ y: number,
+ color: string,
+ value: number,
+ playedTurn: number,
+ playedIn: number,
+ // playedBy?: ObjectId,
+ playedBy?: string,
+ ) {
+ this.x = x;
+ this.y = y;
+ this.color = color;
+ this.value = value;
+ this.playedTurn = playedTurn;
+ this.playedIn = playedIn;
+ // this.playedBy = playedBy;
+ if (playedBy) {
+ this.playedBy = playedBy;
+ }
+ }
+}
+
+class MongoPuntoPlayer {
+ // @ObjectIdColumn()
+ // @Column(() => ObjectId)
+ // @Index()
+ // playerID: ObjectId;
+
+ @Column()
+ playerName: string;
+
+ @Column()
+ points: number;
+
+ @Column()
+ @Index()
+ status: string;
+
+ constructor(
+ /*playerID: ObjectId,*/ name: string,
+ points: number,
+ status: string,
+ ) {
+ // this.playerID = playerID;
+ this.playerName = name;
+ this.points = points;
+ this.status = status;
+ }
+}
+
+export default MongoPunto;
+export {MongoCard, MongoPuntoPlayer};
diff --git a/src/entities/Game/MySQLGame.ts b/src/entities/Game/MySQLGame.ts
new file mode 100644
index 0000000..6179d57
--- /dev/null
+++ b/src/entities/Game/MySQLGame.ts
@@ -0,0 +1,106 @@
+import {Entity, PrimaryGeneratedColumn, Column, Index} from "typeorm";
+
+@Entity({
+ name: "puntos",
+ orderBy: {
+ winType: "ASC",
+ },
+})
+class MySQLPunto {
+ @PrimaryGeneratedColumn()
+ _id!: number;
+
+ @Column()
+ @Index()
+ winType: string;
+
+ constructor(winType: string) {
+ this.winType = winType;
+ }
+}
+
+@Entity({
+ name: "cards",
+})
+class MySQLCard {
+ @PrimaryGeneratedColumn()
+ _id!: number;
+
+ @Column()
+ boardId!: number;
+
+ @Column()
+ x: number;
+
+ @Column()
+ y: number;
+
+ @Column()
+ color: string;
+
+ @Column()
+ value: number;
+
+ @Column()
+ playedTurn: number;
+
+ @Column()
+ playedIn: number;
+
+ @Column({
+ nullable: true,
+ })
+ playedBy!: number;
+
+ constructor(
+ x: number,
+ y: number,
+ color: string,
+ value: number,
+ playedTurn: number,
+ playedIn: number,
+ playedBy?: number,
+ ) {
+ this.x = x;
+ this.y = y;
+ this.color = color;
+ this.value = value;
+ this.playedTurn = playedTurn;
+ this.playedIn = playedIn;
+ // this.playedBy = playedBy;
+ if (playedBy) {
+ this.playedBy = playedBy;
+ }
+ }
+}
+
+@Entity({
+ name: "punto_players",
+})
+class MySQLPuntoPlayer {
+ @PrimaryGeneratedColumn()
+ _id!: number;
+
+ @Column()
+ @Index()
+ playerID: number;
+
+ @Column()
+ @Index()
+ boardId!: number;
+
+ @Column()
+ points: number;
+
+ @Column()
+ @Index()
+ status: string;
+
+ constructor(playerID: number, points: number, status: string) {
+ this.playerID = playerID;
+ this.points = points;
+ this.status = status;
+ }
+}
+
+export {MySQLPunto, MySQLCard, MySQLPuntoPlayer};
diff --git a/src/entities/Game/SQLiteGame.ts b/src/entities/Game/SQLiteGame.ts
new file mode 100644
index 0000000..1996e28
--- /dev/null
+++ b/src/entities/Game/SQLiteGame.ts
@@ -0,0 +1,106 @@
+import {Entity, PrimaryGeneratedColumn, Column, Index} from "typeorm";
+
+@Entity({
+ name: "puntos",
+ orderBy: {
+ winType: "ASC",
+ },
+})
+class SQLitePunto {
+ @PrimaryGeneratedColumn()
+ _id!: number;
+
+ @Column()
+ @Index()
+ winType: string;
+
+ constructor(winType: string) {
+ this.winType = winType;
+ }
+}
+
+@Entity({
+ name: "cards",
+})
+class SQLiteCard {
+ @PrimaryGeneratedColumn()
+ _id!: number;
+
+ @Column()
+ boardId!: number;
+
+ @Column()
+ x: number;
+
+ @Column()
+ y: number;
+
+ @Column()
+ color: string;
+
+ @Column()
+ value: number;
+
+ @Column()
+ playedTurn: number;
+
+ @Column()
+ playedIn: number;
+
+ @Column({
+ nullable: true,
+ })
+ playedBy!: number;
+
+ constructor(
+ x: number,
+ y: number,
+ color: string,
+ value: number,
+ playedTurn: number,
+ playedIn: number,
+ playedBy?: number,
+ ) {
+ this.x = x;
+ this.y = y;
+ this.color = color;
+ this.value = value;
+ this.playedTurn = playedTurn;
+ this.playedIn = playedIn;
+ // this.playedBy = playedBy;
+ if (playedBy) {
+ this.playedBy = playedBy;
+ }
+ }
+}
+
+@Entity({
+ name: "punto_players",
+})
+class SQLitePuntoPlayer {
+ @PrimaryGeneratedColumn()
+ _id!: number;
+
+ @Column()
+ @Index()
+ playerID: number;
+
+ @Column()
+ @Index()
+ boardId!: number;
+
+ @Column()
+ points: number;
+
+ @Column()
+ @Index()
+ status: string;
+
+ constructor(playerID: number, points: number, status: string) {
+ this.playerID = playerID;
+ this.points = points;
+ this.status = status;
+ }
+}
+
+export {SQLitePunto, SQLiteCard, SQLitePuntoPlayer};
diff --git a/src/entities/User.ts b/src/entities/User.ts
new file mode 100644
index 0000000..4fad262
--- /dev/null
+++ b/src/entities/User.ts
@@ -0,0 +1,886 @@
+import {
+ Entity,
+ PrimaryGeneratedColumn,
+ Column,
+ ObjectIdColumn,
+ ObjectId,
+ Index,
+} from "typeorm";
+import DBWrapper from "../db/DBWrapper";
+import Result, {ResultStatus} from "../db/Result";
+import BaseEntityManager from "./BaseEntityManager";
+
+// ALTER TABLE users AUTO_INCREMENT = 0;
+@Entity({
+ name: "users",
+ orderBy: {
+ name: "ASC",
+ },
+})
+class MySQLUser {
+ @PrimaryGeneratedColumn()
+ _id!: number;
+
+ @Column()
+ name: string;
+
+ constructor(name: string = "") {
+ this.name = name;
+ }
+}
+
+// UPDATE sqlite_sequence SET seq = 0 WHERE name = users;
+@Entity({
+ name: "users",
+ orderBy: {
+ name: "ASC",
+ },
+})
+class SQLiteUser {
+ @PrimaryGeneratedColumn()
+ _id!: number;
+
+ @Column()
+ name: string;
+
+ constructor(name: string = "") {
+ this.name = name;
+ }
+}
+
+@Entity({
+ name: "users",
+ orderBy: {
+ name: "ASC",
+ },
+})
+class MongoUser {
+ @ObjectIdColumn()
+ _id!: ObjectId;
+
+ @Column()
+ @Index({unique: true})
+ name: string;
+
+ constructor(name: string = "") {
+ this.name = name;
+ }
+}
+
+class UserManager extends BaseEntityManager {
+ /**
+ * The MySQL entity used in this class. Replace {@link BaseEntityManager.mySqlEntity} with the specific MySQL entity for this class.
+ * This is a protected member.
+ * @protected
+ * @type {MySQLUser}
+ * @memberof UserManager
+ */
+ protected mySqlEntity?: MySQLUser;
+
+ /**
+ * The SQLite entity used in this class. Replace {@link BaseEntityManager.sqliteEntity} with the specific SQLite entity for this class.
+ * This is a protected member.
+ * @protected
+ * @type {SQLiteUser}
+ * @memberof UserManager
+ */
+ protected sqliteEntity?: SQLiteUser;
+
+ /**
+ * The MongoDB entity used in this class. Replace {@link BaseEntityManager.mongoEntity} with the specific MongoDB entity for this class.
+ * This is a protected member.
+ * @protected
+ * @type {MongoUser}
+ * @memberof UserManager
+ */
+ protected mongoEntity?: MongoUser;
+
+ /**
+ * Gets the name of the user.
+ * @type {string | undefined}
+ * @memberof UserManager
+ */
+ get name(): string | undefined {
+ let name: string | undefined;
+
+ if (this.mySqlEntity && this.mySqlEntity.name) {
+ name = this.mySqlEntity.name;
+ } else if (this.sqliteEntity && this.sqliteEntity.name) {
+ name = this.sqliteEntity.name;
+ } else if (this.mongoEntity && this.mongoEntity.name) {
+ name = this.mongoEntity.name;
+ }
+
+ return name;
+ }
+
+ /**
+ * Sets the name of the user for the existing entities.
+ * @param {string} name The name of the user.
+ * @memberof UserManager
+ */
+ set name(name: string) {
+ if (this.mySqlEntity) {
+ this.mySqlEntity.name = name;
+ }
+ if (this.sqliteEntity) {
+ this.sqliteEntity.name = name;
+ }
+ if (this.mongoEntity) {
+ this.mongoEntity.name = name;
+ }
+ }
+
+ get mySqlId(): number | undefined {
+ return this.mySqlEntity?._id;
+ }
+ get sqliteId(): number | undefined {
+ return this.sqliteEntity?._id;
+ }
+ get mongoId(): ObjectId | undefined {
+ return this.mongoEntity?._id;
+ }
+
+ /**
+ * Creates an instance of UserManager.
+ * @param {DBWrapper} dbWrapper The DBWrapper instance to be used in this class.
+ * @param {string} name The name of the user to be used in this class.
+ * @memberof UserManager
+ */
+ public constructor(dbWrapper: DBWrapper, name: string) {
+ super(dbWrapper);
+
+ UserManager.initRepositories();
+
+ this.mySqlEntity = new MySQLUser(name);
+ this.sqliteEntity = new SQLiteUser(name);
+ this.mongoEntity = new MongoUser(name);
+ }
+
+ /**
+ * Initializes the repositories for MySQL, SQLite, and MongoDB.
+ * This method should be called before any other method in this class is invoked.
+ * @protected
+ * @static
+ * @memberof UserManager
+ */
+ protected static initRepositories(): void {
+ BaseEntityManager.initRepositories(MySQLUser, SQLiteUser, MongoUser);
+ }
+
+ /**
+ * Builds a user from the database(s).
+ *
+ * @returns {Promise} The built user. It is the same as calling {@link UserManager.build} with the name of the user as the parameter.
+ * @memberof UserManager
+ * @throws {Error} If the name of the user is undefined.
+ */
+ public async build(): Promise {
+ // return await UserManager.build(this.name);
+ if (!this.name) {
+ throw new Error(
+ "Name is undefined, cannot build user. Please set the name before building the user or use the static build method.",
+ );
+ } else {
+ return await UserManager.build(this.name);
+ }
+ }
+
+ /**
+ * Builds a user from the database(s).
+ *
+ * @param {string} name The name of the user to build.
+ * @returns {Promise} The built user.
+ * @memberof UserManager
+ */
+ public static async build(name: string): Promise {
+ UserManager.initRepositories();
+
+ const buildedUser = new UserManager(UserManager.dbWrapper, name);
+
+ const usersFromDBs = await UserManager.find(name);
+
+ const mySqlUserData = usersFromDBs.mySqlRepo?.data;
+ const sqliteUserData = usersFromDBs.sqliteRepo?.data;
+ const mongoUserData = usersFromDBs.mongoRepo?.data;
+
+ if (
+ mySqlUserData &&
+ Array.isArray(mySqlUserData) &&
+ mySqlUserData.length > 0
+ ) {
+ const mySqlUser = mySqlUserData[0];
+
+ buildedUser.mySqlEntity = mySqlUser;
+ }
+
+ if (
+ sqliteUserData &&
+ Array.isArray(sqliteUserData) &&
+ sqliteUserData.length > 0
+ ) {
+ const sqliteUser = sqliteUserData[0];
+
+ buildedUser.sqliteEntity = sqliteUser;
+ }
+
+ if (
+ mongoUserData &&
+ Array.isArray(mongoUserData) &&
+ mongoUserData.length > 0
+ ) {
+ const mongoUser = mongoUserData[0];
+
+ buildedUser.mongoEntity = mongoUser;
+ }
+
+ return buildedUser;
+ }
+
+ /**
+ * Rebuilds the user from the database(s).
+ *
+ * @returns {Promise} A promise that resolves when the user is rebuilt.
+ * @memberof UserManager
+ * @throws {Error} If the name of the user is undefined.
+ */
+ public async rebuild(): Promise {
+ if (!this.name) {
+ throw new Error(
+ "Name is undefined, cannot rebuild user. Please set the name before rebuilding the user.",
+ );
+ }
+
+ const usersFromDBs = await UserManager.find(this.name);
+
+ const mySqlUserData = usersFromDBs.mySqlRepo?.data;
+ const sqliteUserData = usersFromDBs.sqliteRepo?.data;
+ const mongoUserData = usersFromDBs.mongoRepo?.data;
+
+ if (
+ mySqlUserData &&
+ Array.isArray(mySqlUserData) &&
+ mySqlUserData.length > 0
+ ) {
+ const mySqlUser = mySqlUserData[0];
+
+ this.mySqlEntity = mySqlUser;
+ } else {
+ this.mySqlEntity = new MySQLUser(this.name);
+ }
+
+ if (
+ sqliteUserData &&
+ Array.isArray(sqliteUserData) &&
+ sqliteUserData.length > 0
+ ) {
+ const sqliteUser = sqliteUserData[0];
+
+ this.sqliteEntity = sqliteUser;
+ } else {
+ this.sqliteEntity = new SQLiteUser(this.name);
+ }
+
+ if (
+ mongoUserData &&
+ Array.isArray(mongoUserData) &&
+ mongoUserData.length > 0
+ ) {
+ const mongoUser = mongoUserData[0];
+
+ this.mongoEntity = mongoUser;
+ } else {
+ this.mongoEntity = new MongoUser(this.name);
+ }
+ }
+
+ /**
+ * Saves the user in the database(s).
+ *
+ * @returns {Promise<{
+ * mySqlRepo?: Result;
+ * sqliteRepo?: Result;
+ * mongoRepo?: Result;
+ * neo4jResult?: Result;
+ * }>} The results of the save operation. For each repository:
+ * - If the operation was successful, the status will be `Success` and the `data` will be the saved object.
+ * - If the operation was unsuccessful, the status will be `Fail` and the `error` will be the error that occurred.
+ * - If `undefined` is returned, it means that the repository was not initialized and the operation was not performed.
+ * @memberof UserManager
+ */
+ public async save(): Promise<{
+ mySqlRepo?: Result;
+ sqliteRepo?: Result;
+ mongoRepo?: Result;
+ neo4jResult?: Result;
+ }> {
+ UserManager.initRepositories();
+
+ const results: {
+ mySqlRepo?: Result;
+ sqliteRepo?: Result;
+ mongoRepo?: Result;
+ neo4jResult?: Result;
+ } = {
+ mySqlRepo: undefined,
+ sqliteRepo: undefined,
+ mongoRepo: undefined,
+ neo4jResult: undefined,
+ };
+
+ if (this.mySqlEntity) {
+ await UserManager.mySqlRepo
+ ?.save(this.mySqlEntity)
+ .then((result) => {
+ results.mySqlRepo = new Result({
+ status: result
+ ? ResultStatus.Success
+ : ResultStatus.Fail,
+ data: result,
+ });
+ })
+ .catch((error) => {
+ results.mySqlRepo = new Result({
+ status: ResultStatus.Fail,
+ error,
+ });
+ });
+ }
+
+ if (this.sqliteEntity) {
+ await UserManager.sqliteRepo
+ ?.save(this.sqliteEntity)
+ .then((result) => {
+ results.sqliteRepo = new Result({
+ status: result
+ ? ResultStatus.Success
+ : ResultStatus.Fail,
+ data: result,
+ });
+ })
+ .catch((error) => {
+ results.sqliteRepo = new Result({
+ status: ResultStatus.Fail,
+ error,
+ });
+ });
+ }
+
+ if (this.mongoEntity) {
+ await UserManager.mongoRepo
+ ?.save(this.mongoEntity)
+ .then((result) => {
+ results.mongoRepo = new Result({
+ status: result
+ ? ResultStatus.Success
+ : ResultStatus.Fail,
+ data: result,
+ });
+ })
+ .catch((error) => {
+ results.mongoRepo = new Result({
+ status: ResultStatus.Fail,
+ error,
+ });
+ });
+ }
+
+ return results;
+ }
+
+ /**
+ * Removes the user from the database(s).
+ *
+ * @returns {Promise<{
+ * mySqlRepo?: Result;
+ * sqliteRepo?: Result;
+ * mongoRepo?: Result;
+ * neo4jResult?: Result;
+ * }>} The results of the remove operation. For each repository:
+ * - If the operation was successful, the status will be `Success` and the `data` will be the removed object.
+ * - If the operation was unsuccessful, the status will be `Fail` and the `error` will be the error that occurred.
+ * - If `undefined` is returned, it means that the repository was not initialized and the operation was not performed.
+ */
+ public async remove(): Promise<{
+ mySqlRepo?: Result;
+ sqliteRepo?: Result;
+ mongoRepo?: Result;
+ neo4jResult?: Result;
+ }> {
+ UserManager.initRepositories();
+
+ const results: {
+ mySqlRepo?: Result;
+ sqliteRepo?: Result;
+ mongoRepo?: Result;
+ neo4jResult?: Result;
+ } = {
+ mySqlRepo: undefined,
+ sqliteRepo: undefined,
+ mongoRepo: undefined,
+ neo4jResult: undefined,
+ };
+
+ if (this.mySqlEntity) {
+ await UserManager.mySqlRepo
+ ?.remove(this.mySqlEntity)
+ .then((result) => {
+ results.mySqlRepo = new Result({
+ status: result
+ ? ResultStatus.Success
+ : ResultStatus.Fail,
+ data: result,
+ });
+ })
+ .catch((error) => {
+ results.mySqlRepo = new Result({
+ status: ResultStatus.Fail,
+ error,
+ });
+ });
+ }
+
+ if (this.sqliteEntity) {
+ await UserManager.sqliteRepo
+ ?.remove(this.sqliteEntity)
+ .then((result) => {
+ results.sqliteRepo = new Result({
+ status: result
+ ? ResultStatus.Success
+ : ResultStatus.Fail,
+ data: result,
+ });
+ })
+ .catch((error) => {
+ results.sqliteRepo = new Result({
+ status: ResultStatus.Fail,
+ error,
+ });
+ });
+ }
+
+ if (this.mongoEntity) {
+ await UserManager.mongoRepo
+ ?.remove(this.mongoEntity)
+ .then((result) => {
+ results.mongoRepo = new Result({
+ status: result
+ ? ResultStatus.Success
+ : ResultStatus.Fail,
+ data: result,
+ });
+ })
+ .catch((error) => {
+ results.mongoRepo = new Result({
+ status: ResultStatus.Fail,
+ error,
+ });
+ });
+ }
+
+ return results;
+ }
+
+ /**
+ * Removes all users from the database(s). It is the same as calling {@link UserManager.removeAll}.
+ *
+ * @returns {Promise<{
+ * mySqlRepo?: Result;
+ * sqliteRepo?: Result;
+ * mongoRepo?: Result;
+ * neo4jResult?: Result;
+ * }>} The results of the remove operation. For each repository:
+ * - If the operation was successful, the status will be `Success` and there is no `data`.
+ * - If the operation was unsuccessful, the status will be `Fail` and the `error` will be the error that occurred.
+ * - If `undefined` is returned, it means that the repository was not initialized and the operation was not performed.
+ */
+ public async removeAll(): Promise<{
+ mySqlRepo?: Result;
+ sqliteRepo?: Result;
+ mongoRepo?: Result;
+ neo4jResult?: Result;
+ }> {
+ return UserManager.removeAll();
+ }
+
+ /**
+ * Removes all users from the database(s).
+ *
+ * @returns {Promise<{
+ * mySqlRepo?: Result;
+ * sqliteRepo?: Result;
+ * mongoRepo?: Result;
+ * neo4jResult?: Result;
+ * }>} The results of the remove operation. For each repository:
+ * - If the operation was successful, the status will be `Success` and there is no `data`.
+ * - If the operation was unsuccessful, the status will be `Fail` and the `error` will be the error that occurred.
+ * - If `undefined` is returned, it means that the repository was not initialized and the operation was not performed.
+ */
+ public static async removeAll(): Promise<{
+ mySqlRepo?: Result;
+ sqliteRepo?: Result;
+ mongoRepo?: Result;
+ neo4jResult?: Result;
+ }> {
+ UserManager.initRepositories();
+
+ const results: {
+ mySqlRepo?: Result;
+ sqliteRepo?: Result;
+ mongoRepo?: Result;
+ neo4jResult?: Result;
+ } = {
+ mySqlRepo: undefined,
+ sqliteRepo: undefined,
+ mongoRepo: undefined,
+ neo4jResult: undefined,
+ };
+
+ await UserManager.mySqlRepo
+ ?.clear()
+ .then(() => {
+ results.mySqlRepo = new Result({
+ status: ResultStatus.Success,
+ });
+ })
+ .catch((error) => {
+ results.mySqlRepo = new Result({
+ status: ResultStatus.Fail,
+ error,
+ });
+ });
+
+ await UserManager.sqliteRepo
+ ?.clear()
+ .then(() => {
+ results.sqliteRepo = new Result({
+ status: ResultStatus.Success,
+ });
+ })
+ .catch((error) => {
+ results.sqliteRepo = new Result({
+ status: ResultStatus.Fail,
+ error,
+ });
+ });
+
+ await UserManager.mongoRepo
+ ?.clear()
+ .then(() => {
+ results.mongoRepo = new Result({
+ status: ResultStatus.Success,
+ });
+ })
+ .catch((error) => {
+ results.mongoRepo = new Result({
+ status: ResultStatus.Fail,
+ error,
+ });
+ });
+
+ return results;
+ }
+
+ /**
+ * Finds the user in the database(s) by its name.
+ * This method is the same as calling {@link UserManager.find} with the name of the user as the parameter.
+ *
+ * @example
+ * const user = new UserManager(dbWrapper, "John");
+ *
+ * const results = await user.find();
+ *
+ * // is the same as
+ *
+ * const results = await UserManager.find("John");
+ *
+ * @returns {Promise<{
+ * mySqlRepo?: Result;
+ * sqliteRepo?: Result;
+ * mongoRepo?: Result;
+ * neo4jResult?: Result;
+ * }>} The results of the find operation. For each repository:
+ * - If the operation was successful, the status will be `Success` and the `data` will be the found object.
+ * - If the operation was unsuccessful, the status will be `Fail` and the `error` will be the error that occurred.
+ * - If `undefined` is returned, it means that the repository was not initialized and the operation was not performed.
+ */
+ public async find(): Promise<{
+ mySqlRepo?: Result;
+ sqliteRepo?: Result;
+ mongoRepo?: Result;
+ neo4jResult?: Result;
+ }> {
+ UserManager.initRepositories();
+
+ const results: {
+ mySqlRepo?: Result;
+ sqliteRepo?: Result;
+ mongoRepo?: Result;
+ neo4jResult?: Result;
+ } = {
+ mySqlRepo: undefined,
+ sqliteRepo: undefined,
+ mongoRepo: undefined,
+ neo4jResult: undefined,
+ };
+
+ await UserManager.mySqlRepo
+ ?.find({
+ where: {
+ name: this.name,
+ },
+ })
+ .then((result) => {
+ results.mySqlRepo = new Result({
+ status: result ? ResultStatus.Success : ResultStatus.Fail,
+ data: result,
+ });
+ })
+ .catch((error) => {
+ results.mySqlRepo = new Result({
+ status: ResultStatus.Fail,
+ error,
+ });
+ });
+
+ await UserManager.sqliteRepo
+ ?.find({
+ where: {
+ name: this.name,
+ },
+ })
+ .then((result) => {
+ results.sqliteRepo = new Result({
+ status: result ? ResultStatus.Success : ResultStatus.Fail,
+ data: result,
+ });
+ })
+ .catch((error) => {
+ results.sqliteRepo = new Result({
+ status: ResultStatus.Fail,
+ error,
+ });
+ });
+
+ await UserManager.mongoRepo
+ ?.find({
+ where: {
+ name: this.name,
+ },
+ })
+ .then((result) => {
+ results.mongoRepo = new Result({
+ status: result ? ResultStatus.Success : ResultStatus.Fail,
+ data: result,
+ });
+ })
+ .catch((error) => {
+ results.mongoRepo = new Result({
+ status: ResultStatus.Fail,
+ error,
+ });
+ });
+
+ return results;
+ }
+
+ /**
+ * Finds a user in the database(s) by its name.
+ *
+ * @param {string} name The name of the user to find.
+ * @returns {Promise<{
+ * mySqlRepo?: Result;
+ * sqliteRepo?: Result;
+ * mongoRepo?: Result;
+ * neo4jResult?: Result;
+ * }>} The results of the find operation. For each repository:
+ * - If the operation was successful, the status will be `Success` and the `data` will be the found object.
+ * - If the operation was unsuccessful, the status will be `Fail` and the `error` will be the error that occurred.
+ * - If `undefined` is returned, it means that the repository was not initialized and the operation was not performed.
+ */
+ public static async find(name: string): Promise<{
+ mySqlRepo?: Result;
+ sqliteRepo?: Result;
+ mongoRepo?: Result;
+ neo4jResult?: Result;
+ }> {
+ UserManager.initRepositories();
+
+ const results: {
+ mySqlRepo?: Result;
+ sqliteRepo?: Result;
+ mongoRepo?: Result;
+ neo4jResult?: Result;
+ } = {
+ mySqlRepo: undefined,
+ sqliteRepo: undefined,
+ mongoRepo: undefined,
+ neo4jResult: undefined,
+ };
+
+ await UserManager.mySqlRepo
+ ?.find({
+ where: {
+ name,
+ },
+ })
+ .then((result) => {
+ results.mySqlRepo = new Result({
+ status: result ? ResultStatus.Success : ResultStatus.Fail,
+ data: result,
+ });
+ })
+ .catch((error) => {
+ results.mySqlRepo = new Result({
+ status: ResultStatus.Fail,
+ error,
+ });
+ });
+
+ await UserManager.sqliteRepo
+ ?.find({
+ where: {
+ name,
+ },
+ })
+ .then((result) => {
+ results.sqliteRepo = new Result({
+ status: result ? ResultStatus.Success : ResultStatus.Fail,
+ data: result,
+ });
+ })
+ .catch((error) => {
+ results.sqliteRepo = new Result({
+ status: ResultStatus.Fail,
+ error,
+ });
+ });
+
+ await UserManager.mongoRepo
+ ?.find({
+ where: {
+ name,
+ },
+ })
+ .then((result) => {
+ results.mongoRepo = new Result({
+ status: result ? ResultStatus.Success : ResultStatus.Fail,
+ data: result,
+ });
+ })
+ .catch((error) => {
+ results.mongoRepo = new Result({
+ status: ResultStatus.Fail,
+ error,
+ });
+ });
+
+ return results;
+ }
+
+ /**
+ * Finds all users in the database(s). It is the same as calling {@link UserManager.findAll}.
+ *
+ * @returns {Promise<{
+ * mySqlRepo?: Result;
+ * sqliteRepo?: Result;
+ * mongoRepo?: Result;
+ * neo4jResult?: Result;
+ * }>} The results of the find operation. For each repository:
+ * - If the operation was successful, the status will be `Success` and the `data` will be the found objects.
+ * - If the operation was unsuccessful, the status will be `Fail` and the `error` will be the error that occurred.
+ * - If `undefined` is returned, it means that the repository was not initialized and the operation was not performed.
+ */
+ public async findAll(): Promise<{
+ mySqlRepo?: Result;
+ sqliteRepo?: Result;
+ mongoRepo?: Result;
+ neo4jResult?: Result;
+ }> {
+ return UserManager.findAll();
+ }
+
+ /**
+ * Finds all users in the database(s).
+ *
+ * @returns {Promise<{
+ * mySqlRepo?: Result;
+ * sqliteRepo?: Result;
+ * mongoRepo?: Result;
+ * neo4jResult?: Result;
+ * }>} The results of the find operation. For each repository:
+ * - If the operation was successful, the status will be `Success` and the `data` will be the found objects.
+ * - If the operation was unsuccessful, the status will be `Fail` and the `error` will be the error that occurred.
+ * - If `undefined` is returned, it means that the repository was not initialized and the operation was not performed.
+ */
+ public static async findAll(): Promise<{
+ mySqlRepo?: Result;
+ sqliteRepo?: Result;
+ mongoRepo?: Result;
+ neo4jResult?: Result;
+ }> {
+ UserManager.initRepositories();
+
+ const results: {
+ mySqlRepo?: Result;
+ sqliteRepo?: Result;
+ mongoRepo?: Result;
+ neo4jResult?: Result;
+ } = {
+ mySqlRepo: undefined,
+ sqliteRepo: undefined,
+ mongoRepo: undefined,
+ neo4jResult: undefined,
+ };
+
+ await UserManager.mySqlRepo
+ ?.find()
+ .then((result) => {
+ results.mySqlRepo = new Result({
+ status: result ? ResultStatus.Success : ResultStatus.Fail,
+ data: result,
+ });
+ })
+ .catch((error) => {
+ results.mySqlRepo = new Result({
+ status: ResultStatus.Fail,
+ error,
+ });
+ });
+
+ await UserManager.sqliteRepo
+ ?.find()
+ .then((result) => {
+ results.sqliteRepo = new Result({
+ status: result ? ResultStatus.Success : ResultStatus.Fail,
+ data: result,
+ });
+ })
+ .catch((error) => {
+ results.sqliteRepo = new Result({
+ status: ResultStatus.Fail,
+ error,
+ });
+ });
+
+ await UserManager.mongoRepo
+ ?.find()
+ .then((result) => {
+ results.mongoRepo = new Result({
+ status: result ? ResultStatus.Success : ResultStatus.Fail,
+ data: result,
+ });
+ })
+ .catch((error) => {
+ results.mongoRepo = new Result({
+ status: ResultStatus.Fail,
+ error,
+ });
+ });
+
+ return results;
+ }
+}
+
+export default UserManager;
+export {MySQLUser, SQLiteUser, MongoUser};
diff --git a/src/game/BaseId.ts b/src/game/BaseId.ts
new file mode 100644
index 0000000..466b905
--- /dev/null
+++ b/src/game/BaseId.ts
@@ -0,0 +1,70 @@
+/**
+ * The base class for the ID management.
+ */
+abstract class BaseId {
+ /**
+ * The ID of the object.
+ * @type {string}
+ */
+ private _id: string;
+
+ public get id(): string {
+ return this._id;
+ }
+
+ public set id(id: string) {
+ this._id = id;
+ }
+
+ /**
+ * The constructor for the BaseId class. It initializes a unique ID for the instance.
+ */
+ constructor() {
+ this._id = this.generateId();
+ }
+
+ /**
+ * Generates a unique identifier combining a UUID and a timestamp.
+ * @returns {string} The generated unique identifier.
+ */
+ private generateId(): string {
+ const uuid: string = this.returnUUID();
+ // const timestamp: number = this.returnTimestamp();
+ // return `${uuid}-${timestamp}`;
+ return uuid;
+ }
+
+ protected regenerateId(): void {
+ this._id = this.generateId();
+ }
+
+ /**
+ * Creates a Universally Unique Identifier (UUID).
+ * @returns {string} A UUID string.
+ */
+ private returnUUID(): string {
+ return crypto.randomUUID();
+ }
+
+ /**
+ * Gets the current time as a timestamp.
+ * @returns {number} The current time in milliseconds since the Unix epoch.
+ */
+ private returnTimestamp(): number {
+ return new Date().getTime();
+ }
+
+ /**
+ * Displays the ID of the object in the console.
+ */
+ public displayId(): void {
+ console.log(`${this.constructor.name} ID: ${this._id}`);
+ }
+
+ /**
+ * Abstract method to list the IDs of the object's children.
+ */
+ abstract listIds(): void;
+}
+
+export default BaseId;
diff --git a/src/game/Board.ts b/src/game/Board.ts
new file mode 100644
index 0000000..b5812d4
--- /dev/null
+++ b/src/game/Board.ts
@@ -0,0 +1,1654 @@
+import BaseId from "./BaseId";
+import Player, {PlayerOptions} from "./Player";
+import Card, {Coordinates} from "./Card";
+
+/**
+ * Represents the game board for a card game. It extends the BaseId class to include
+ * a unique identifier for each board instance. The Board manages the state of the game,
+ * including the cards on the board, the players, the deck, and the discard pile,
+ * as well as the turn number, the type of win condition that ends the game,
+ * and the winner(s) of the game.
+ */
+class Board extends BaseId {
+ /**
+ * Cards currently on the board.
+ * @type {Card[]}
+ */
+ private _cards: Card[] = [];
+ public get cards(): Card[] {
+ return this._cards;
+ }
+
+ /**
+ * List of players in the game.
+ * @type {Player[]}
+ */
+ private _players: Player[];
+ public get players(): Player[] {
+ return this._players;
+ }
+
+ /**
+ * The current turn number in the game.
+ * @type {number}
+ */
+ private _turn: number = 0;
+ public get turn(): number {
+ return this._turn;
+ }
+
+ /**
+ * Describes the type of win.
+ * @type {WinType}
+ */
+ private _winType: WinType = WinType.None;
+ public get winType(): string {
+ return this._winType;
+ }
+
+ /**
+ * Array of the player(s) who have won the game.
+ * @type {Player[]}
+ */
+ private _winners: Player[] = [];
+ public get winners(): Player[] {
+ return this._winners;
+ }
+
+ /**
+ * Array of the player(s) who have lost the game.
+ */
+ private _losers: Player[] = [];
+ public get losers(): Player[] {
+ return this._losers;
+ }
+
+ /**
+ * Displays the ID of the Board instance in the console
+ * and calls the listIds method to display the IDs of the Card and Player instances.
+ */
+ public listIds(): void {
+ this.displayId();
+
+ this._cards.forEach((card: Card) => {
+ card.listIds();
+ });
+
+ this._players.forEach((player: Player) => {
+ player.listIds();
+ });
+ }
+
+ /**
+ * The constructor initializes the Board with a default set of values,
+ * including a single card and player to start with.
+ * @param {BoardOptions} boardOptions An object containing the options for the board.
+ */
+ constructor(boardOptions: BoardOptions) {
+ super();
+
+ const {nbrPlayers, listPlayerOptions} = boardOptions;
+
+ if (nbrPlayers < 2 || nbrPlayers > 4) {
+ throw new Error("The number of players must be between 2 and 4.");
+ }
+
+ const listPlayer = [];
+
+ let aPlayerHaveATurn = false;
+
+ for (let i = 0; i < nbrPlayers; i++) {
+ const currentPlayerOptions = listPlayerOptions[i];
+
+ listPlayer.push(new Player(currentPlayerOptions));
+
+ if (currentPlayerOptions.isTurn) {
+ aPlayerHaveATurn = true;
+ }
+ }
+
+ this._players = listPlayer;
+
+ if (!aPlayerHaveATurn) {
+ this.randomizePlayerTurn();
+ }
+
+ this.cardDistribution();
+ }
+
+ public static build(
+ cards: Card[],
+ players: Player[],
+ turn: number,
+ winType: WinType,
+ winners: Player[],
+ losers: Player[],
+ ): Board {
+ const board = new Board({
+ nbrPlayers: players.length,
+ listPlayerOptions: players.map((player) => {
+ return {
+ id: player.id,
+ name: player.name,
+ isTurn: player.isTurn,
+ };
+ }),
+ });
+
+ board._cards = cards;
+ board._players = players;
+ board._turn = turn;
+ board._winType = winType;
+ board._winners = winners;
+ board._losers = losers;
+
+ return board;
+ }
+
+ /**
+ * Distributes the cards to the players.
+ */
+ private cardDistribution(): void {
+ const listPlayer = this._players;
+
+ const nbrPlayers = listPlayer.length;
+
+ const colors: string[] = ["red", "blue", "green", "yellow"];
+
+ const numbers: number[] = [1, 2, 3, 4, 5, 6, 7, 8, 9];
+
+ // Each color has twice each card from 1 to 9
+ const listCard: {[key: string]: Card[]} = {};
+
+ colors.forEach((color) => {
+ listCard[color] = [];
+
+ numbers.forEach((number) => {
+ listCard[color].push(new Card(color, number));
+ listCard[color].push(new Card(color, number));
+ });
+ });
+
+ // For 2 players, each player all cards of two colors (random colors)
+ // for 3 players, each player has all the cards of a suit + 6 cards of the last suit (random color)
+ // for 4 players, each player has all the cards of one color (random color)
+
+ if (nbrPlayers === 2) {
+ listPlayer.forEach((player) => {
+ const color1 =
+ colors[Math.floor(Math.random() * colors.length)];
+
+ // Remove color1 from colors
+ colors.splice(colors.indexOf(color1), 1);
+
+ const color2 =
+ colors[Math.floor(Math.random() * colors.length)];
+
+ // Remove color2 from colors
+ colors.splice(colors.indexOf(color2), 1);
+
+ player.color = [color1, color2];
+
+ // Distribute cards
+ player.color.forEach((color) => {
+ player.fillDeck(listCard[color]);
+ });
+ });
+ } else if (nbrPlayers === 3) {
+ listPlayer.forEach((player) => {
+ const color = colors[Math.floor(Math.random() * colors.length)];
+
+ // Remove color from colors
+ colors.splice(colors.indexOf(color), 1);
+
+ player.color = [color];
+ });
+
+ // Distribute cards
+ const lastColor = colors[0];
+
+ listPlayer.forEach((player) => {
+ player.color.forEach((color) => {
+ player.fillDeck(listCard[color]);
+ });
+
+ const listOf6RandomCards: Card[] = [];
+
+ for (let i = 0; i < 6; i++) {
+ const randomCard =
+ listCard[lastColor][
+ Math.floor(
+ Math.random() * listCard[lastColor].length,
+ )
+ ];
+
+ // Remove randomCard from listCard
+ listCard[lastColor].splice(
+ listCard[lastColor].indexOf(randomCard),
+ 1,
+ );
+
+ listOf6RandomCards.push(randomCard);
+ }
+
+ player.fillDeck(listOf6RandomCards);
+ });
+ } else {
+ // nbrPlayers === 4
+ listPlayer.forEach((player) => {
+ const color = colors[Math.floor(Math.random() * colors.length)];
+
+ // Remove color from colors
+ colors.splice(colors.indexOf(color), 1);
+
+ player.color = [color];
+
+ // Distribute cards
+ player.color.forEach((color) => {
+ player.fillDeck(listCard[color]);
+ });
+ });
+ }
+ }
+
+ /**
+ * Places a card on the board at the given coordinates.
+ * @param card The card to place on the board.
+ * @param x The x coordinate to place the card at.
+ * @param y The y coordinate to place the card at.
+ * @param turn The turn the card was played on.
+ * @param player The player who played the card.
+ * @param playerTurn The position of the player in the turn order.
+ * @returns {boolean} `true` if the card was successfully placed, `false` otherwise.
+ */
+ private placeCard(
+ card: Card,
+ x: number,
+ y: number,
+ turn: number,
+ player: Player,
+ playerTurn: number,
+ ): boolean {
+ if (this.cardCanBePlaced(card, x, y)) {
+ card.x = x;
+ card.y = y;
+
+ card.playedTurn = turn;
+ card.playedIn = playerTurn;
+ card.playedBy = player;
+
+ this._cards.push(card);
+
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Checks whether a card can be placed on the board at the given coordinates.
+ * @param card The card to place on the board.
+ * @param x The x coordinate to place the card at.
+ * @param y The y coordinate to place the card at.
+ * @returns `true` if the card can be placed at the given coordinates, `false` otherwise.
+ */
+ public cardCanBePlaced(card: Card, x: number, y: number): boolean {
+ if (x > 5 || x < -5 || y > 5 || y < -5) {
+ return false;
+ }
+
+ const coordinates: Coordinates = {x, y};
+
+ if (this.lineExceedsLimit(coordinates, 6)) {
+ return false;
+ }
+
+ let canPlaceCard = true;
+
+ if (!this.boardIsEmpty()) {
+ const availablesCoordinates = this.availableCoordinates(card.value);
+
+ if (availablesCoordinates.length === 0) {
+ canPlaceCard = false;
+ } else {
+ let canPlaceCardAtCoordinates = false;
+
+ availablesCoordinates.forEach((coordinates) => {
+ if (coordinates.x === x && coordinates.y === y) {
+ canPlaceCardAtCoordinates = true;
+ }
+ });
+
+ if (!canPlaceCardAtCoordinates) {
+ canPlaceCard = false;
+ }
+ }
+ }
+
+ return canPlaceCard;
+ }
+
+ /**
+ * Plays a card on the board at the given coordinates from the current player's hand (with player turn flag)
+ * @param player The player who is playing the card.
+ * @param playerTurn The position of the player in the turn order.
+ * @param card The card to play.
+ * @param x The x coordinate to place the card at.
+ * @param y The y coordinate to place the card at.
+ * @returns {string} A message describing the result of the play.
+ */
+ public playCard(
+ player: Player,
+ playerTurn: number,
+ card: Card,
+ x: number,
+ y: number,
+ ): string {
+ if (!player.cardInHand()) {
+ return "The player does not have the card in hand.";
+ }
+
+ if (this.placeCard(card, x, y, this._turn, player, playerTurn)) {
+ const removedResult = player.removeCardFromHand();
+
+ if (!removedResult) {
+ return "The card could not be removed from the player's hand.";
+ }
+ } else {
+ return "The card could not be placed.";
+ }
+
+ return "The card was successfully placed.";
+ }
+
+ /**
+ * Checks if victory conditions are met.
+ * @returns {winType: WinType, winners: Player[], losers: Player[]} The type of win achieved and the winner(s) and loser(s) of the game.
+ * - `WinType.Win` if a player has won the game.
+ * - `WinType.Draw` if the game is a draw.
+ * - `WinType.Drop` if a player has dropped out of the game.
+ * - `WinType.None` if no player has won the game.
+ */
+ protected checkVictory(): {
+ winType: WinType;
+ winners: Player[];
+ losers: Player[];
+ } {
+ let winners: Player[] = [];
+ let losers: Player[] = this._players;
+
+ // Check whether a player has won by forming a row, column or diagonal
+ if (this.nbrPlayers() === 2) {
+ // 5 for 2 players
+ const winner = this.hasWinningSeries(5);
+
+ if (winner) {
+ winners.push(winner);
+ }
+
+ if (winners.length > 0) {
+ losers = losers.filter(
+ (player) => winners.indexOf(player) === -1,
+ );
+
+ return {
+ winType: WinType.Win,
+ winners: winners,
+ losers: losers,
+ };
+ }
+ } else {
+ // 4 for 3 or 4 players
+ const winner = this.hasWinningSeries(4);
+
+ if (winner) {
+ winners.push(winner);
+ }
+
+ if (winners.length > 0) {
+ losers = losers.filter(
+ (player) => winners.indexOf(player) === -1,
+ );
+
+ return {
+ winType: WinType.Win,
+ winners: winners,
+ losers: losers,
+ };
+ }
+ }
+
+ // Check if a player can no longer play (blocked game condition)
+ if (this.isGameBlocked()) {
+ winners = this.determineWinnerForBlockedGame();
+
+ losers = losers.filter((player) => winners.indexOf(player) === -1);
+
+ let winType: WinType = WinType.Win;
+
+ if (winners.length === 0) {
+ winType = WinType.Draw;
+ }
+
+ return {
+ winType: winType,
+ winners: winners,
+ losers: losers,
+ };
+ }
+
+ // No victory conditions encountered
+ return {
+ winType: WinType.None,
+ winners: winners,
+ losers: losers,
+ };
+ }
+
+ /**
+ * Checks if a player has formed a winning series of cards.
+ * @param length The length of the series to be considered as a winning series.
+ * @returns {Player[]} The player(s) who have formed a winning series.
+ */
+ private hasWinningSeries(length: number): Player | undefined {
+ const seriesOfCards = this.determineSeriesOfCards(length);
+
+ const playersWithPossibleWinningSeries: {
+ player: Player;
+ serie: Card[];
+ }[] = [];
+
+ seriesOfCards.forEach((series) => {
+ let player: Player | undefined;
+
+ series.forEach((card, index, serie) => {
+ if (player) {
+ if (card.playedBy !== player) {
+ return; // Exit the inner loop
+ } else {
+ if (index === length - 1) {
+ playersWithPossibleWinningSeries.push({
+ player: player,
+ serie: serie,
+ });
+
+ return; // Exit the inner loop
+ }
+ }
+ } else {
+ player = card.playedBy;
+ }
+ });
+ });
+
+ let playerWithWinningSerie: Player | undefined;
+
+ if (playersWithPossibleWinningSeries.length === 1) {
+ playerWithWinningSerie = playersWithPossibleWinningSeries[0].player;
+ }
+
+ return playerWithWinningSerie;
+ }
+
+ /**
+ * Determines whether the game is blocked.
+ * @returns {boolean} `true` if the game is blocked, `false` otherwise.
+ */
+ private isGameBlocked(): boolean {
+ if (this.boardIsFull()) {
+ return true;
+ }
+
+ const playerWhoCantPlay = this.getPlayerTurn();
+
+ const cardNotPlayable = playerWhoCantPlay?.cardInHand();
+
+ if (cardNotPlayable) {
+ // The player still has his card in his hand, so he hasn't been able to play it.
+ return true;
+ } else {
+ // The card is no longer in the player's hand, so the game is not blocked (for this turn).
+ return false;
+ }
+ }
+
+ /**
+ * Determines the winner(s) of the game in case of a blocked game.
+ * @returns {Player[]} The winner(s) of the game.
+ */
+ private determineWinnerForBlockedGame(): Player[] {
+ const length = 3;
+
+ const seriesOfCards = this.determineSeriesOfCards(length);
+
+ const playersWithPossibleWinningSeries: {
+ player: Player;
+ serie: Card[];
+ }[] = [];
+
+ seriesOfCards.forEach((series) => {
+ let player: Player | undefined;
+
+ series.forEach((card, index, serie) => {
+ if (player) {
+ if (card.playedBy !== player) {
+ return; // Exit the inner loop
+ } else {
+ if (index === length - 1) {
+ playersWithPossibleWinningSeries.push({
+ player: player,
+ serie: serie,
+ });
+
+ return; // Exit the inner loop
+ }
+ }
+ } else {
+ player = card.playedBy;
+ }
+ });
+ });
+
+ const playersWithWinningSeries: Player[] = [];
+
+ if (playersWithPossibleWinningSeries.length > 1) {
+ // Count the number of serie of cards of each player
+ const countSeriesOfCards: {
+ player: Player;
+ count: number;
+ }[] = [];
+
+ playersWithPossibleWinningSeries.forEach(
+ (playerWithPossibleWinningSeries) => {
+ const player = playerWithPossibleWinningSeries.player;
+
+ let count = 0;
+
+ seriesOfCards.forEach((series) => {
+ if (series[0].playedBy === player) {
+ count++;
+ }
+ });
+
+ countSeriesOfCards.push({
+ player: player,
+ count: count,
+ });
+ },
+ );
+
+ // Retrieve the player with the most series of cards
+ let maxCount = 0;
+
+ countSeriesOfCards.forEach((playerWithCount) => {
+ if (playerWithCount.count > maxCount) {
+ maxCount = playerWithCount.count;
+ }
+ });
+
+ const playersWithMaxCount = countSeriesOfCards.filter(
+ (playerWithCount) => {
+ return playerWithCount.count === maxCount;
+ },
+ );
+
+ // Remove potential duplicates in playersWithMaxCount
+ const seenPlayersMaxCount = new Set();
+
+ playersWithMaxCount.forEach((playerWithCount) => {
+ if (!seenPlayersMaxCount.has(playerWithCount.player.id)) {
+ seenPlayersMaxCount.add(playerWithCount.player.id);
+ } else {
+ // Remove duplicates
+ const index = playersWithMaxCount.indexOf(playerWithCount);
+ if (index > -1) {
+ playersWithMaxCount.splice(index, 1);
+ }
+ }
+ });
+
+ if (playersWithMaxCount.length > 1) {
+ // Retrive the player with the minimal sum of the values of the cards in the series
+ let minSum = Infinity;
+
+ playersWithMaxCount.forEach((playerWithCount) => {
+ const player = playerWithCount.player;
+
+ let sum = 0;
+
+ seriesOfCards.forEach((series) => {
+ if (series[0].playedBy === player) {
+ series.forEach((card) => {
+ sum += card.value;
+ });
+ }
+ });
+
+ if (sum < minSum) {
+ minSum = sum;
+ }
+ });
+
+ const playersWithMinSum = playersWithMaxCount.filter(
+ (playerWithCount) => {
+ const player = playerWithCount.player;
+
+ let sum = 0;
+
+ seriesOfCards.forEach((series) => {
+ if (series[0].playedBy === player) {
+ series.forEach((card) => {
+ sum += card.value;
+ });
+ }
+ });
+
+ return sum === minSum;
+ },
+ );
+
+ // Remove potential duplicates in playersWithMinSum
+ const seenPlayersMinSum = new Set();
+
+ playersWithMinSum.forEach((playerWithCount) => {
+ if (!seenPlayersMinSum.has(playerWithCount.player.id)) {
+ seenPlayersMinSum.add(playerWithCount.player.id);
+ } else {
+ // Remove duplicates
+ const index =
+ playersWithMinSum.indexOf(playerWithCount);
+ if (index > -1) {
+ playersWithMinSum.splice(index, 1);
+ }
+ }
+ });
+
+ playersWithMinSum.forEach((playerWithMinSum) => {
+ playersWithWinningSeries.push(playerWithMinSum.player);
+ });
+ } else {
+ playersWithWinningSeries.push(playersWithMaxCount[0].player);
+ }
+ }
+
+ return playersWithWinningSeries;
+ }
+
+ /**
+ * Determines the series of cards of the given length.
+ * @param length The length of the series of cards to determine.
+ * @returns {Card[][]} An array of series of cards.
+ * @todo Implement diagonal series
+ */
+ private determineSeriesOfCards(length: number): Card[][] {
+ const seriesOfCards: Card[][] = [];
+
+ const cardsSameX: {[key: string]: Card[]} = {};
+
+ const cardsSameY: {[key: string]: Card[]} = {};
+
+ const cardsDiagonalLeftToRight: {[key: string]: Card[]} = {};
+ const cardsDiagonalRightToLeft: {[key: string]: Card[]} = {};
+
+ this._cards.forEach((card) => {
+ const x = card.x;
+ const y = card.y;
+
+ const diagLeftToRightKey = card.x - card.y;
+ const diagRightToLeftKey = card.x + card.y;
+
+ if (!cardsSameX[x]) {
+ cardsSameX[x] = [];
+ }
+
+ if (!cardsSameY[y]) {
+ cardsSameY[y] = [];
+ }
+
+ if (!cardsDiagonalLeftToRight[diagLeftToRightKey]) {
+ cardsDiagonalLeftToRight[diagLeftToRightKey] = [];
+ }
+ if (!cardsDiagonalRightToLeft[diagRightToLeftKey]) {
+ cardsDiagonalRightToLeft[diagRightToLeftKey] = [];
+ }
+
+ for (let i = cardsSameX[x].length - 1; i >= 0; i--) {
+ const cardInArray = cardsSameX[x][i];
+ if (cardInArray.x === x && cardInArray.y === y) {
+ cardsSameX[x].splice(i, 1); // Deletes element at index i
+ }
+ }
+
+ for (let i = cardsSameY[y].length - 1; i >= 0; i--) {
+ const cardInArray = cardsSameY[y][i];
+ if (cardInArray.x === x && cardInArray.y === y) {
+ cardsSameY[y].splice(i, 1); // Deletes element at index i
+ }
+ }
+
+ for (
+ let i = cardsDiagonalLeftToRight[diagLeftToRightKey].length - 1;
+ i >= 0;
+ i--
+ ) {
+ const cardInArray =
+ cardsDiagonalLeftToRight[diagLeftToRightKey][i];
+ if (cardInArray.x === x && cardInArray.y === y) {
+ cardsDiagonalLeftToRight[diagLeftToRightKey].splice(i, 1); // Deletes element at index i
+ }
+ }
+ for (
+ let i = cardsDiagonalRightToLeft[diagRightToLeftKey].length - 1;
+ i >= 0;
+ i--
+ ) {
+ const cardInArray =
+ cardsDiagonalRightToLeft[diagRightToLeftKey][i];
+ if (cardInArray.x === x && cardInArray.y === y) {
+ cardsDiagonalRightToLeft[diagRightToLeftKey].splice(i, 1); // Deletes element at index i
+ }
+ }
+
+ cardsSameX[x].push(card);
+ cardsSameY[y].push(card);
+
+ cardsDiagonalLeftToRight[diagLeftToRightKey].push(card);
+ cardsDiagonalRightToLeft[diagRightToLeftKey].push(card);
+ });
+
+ for (const key in cardsSameX) {
+ cardsSameX[key].sort((card1, card2) => {
+ return card1.y - card2.y; // Sort in ascending order of y
+ });
+ }
+
+ for (const key in cardsSameY) {
+ cardsSameY[key].sort((card1, card2) => {
+ return card1.x - card2.x; // Sort in ascending order of x
+ });
+ }
+
+ for (const key in cardsDiagonalLeftToRight) {
+ cardsDiagonalLeftToRight[key].sort(
+ (card1, card2) => card1.x - card2.x,
+ );
+ }
+ for (const key in cardsDiagonalRightToLeft) {
+ cardsDiagonalRightToLeft[key].sort(
+ (card1, card2) => card1.x - card2.x,
+ );
+ }
+
+ for (const i in cardsSameX) {
+ let series: Card[] = [];
+ let counter = 0;
+
+ const cards = cardsSameX[i];
+
+ cards.forEach((card) => {
+ if (series.length === 0) {
+ series.push(card);
+ counter = 1;
+ } else {
+ if (
+ card.y === series[counter - 1].y + 1 &&
+ card.color === series[counter - 1].color
+ ) {
+ series.push(card);
+ counter++;
+
+ if (series.length >= length) {
+ seriesOfCards.push(series);
+
+ series = [];
+ counter = 0;
+ }
+ } else {
+ if (series.length >= length) {
+ seriesOfCards.push(series);
+ }
+
+ series = [card];
+ counter = 1;
+ }
+ }
+ });
+ }
+
+ for (const i in cardsSameY) {
+ let series: Card[] = [];
+ let counter = 0;
+
+ const cards = cardsSameY[i];
+
+ cards.forEach((card) => {
+ if (series.length === 0) {
+ series.push(card);
+ counter = 1;
+ } else {
+ if (
+ card.x === series[counter - 1].x + 1 &&
+ card.color === series[counter - 1].color
+ ) {
+ series.push(card);
+ counter++;
+
+ if (series.length >= length) {
+ seriesOfCards.push(series);
+
+ series = [];
+ counter = 0;
+ }
+ } else {
+ if (series.length >= length) {
+ seriesOfCards.push(series);
+ }
+
+ series = [card];
+ counter = 1;
+ }
+ }
+ });
+ }
+
+ for (const key in cardsDiagonalLeftToRight) {
+ const series = this.findSeriesInDiagonal(
+ cardsDiagonalLeftToRight[key],
+ length,
+ true,
+ );
+ seriesOfCards.push(...series);
+ }
+ for (const key in cardsDiagonalRightToLeft) {
+ const series = this.findSeriesInDiagonal(
+ cardsDiagonalRightToLeft[key],
+ length,
+ false,
+ );
+ seriesOfCards.push(...series);
+ }
+
+ return seriesOfCards;
+ }
+
+ private findSeriesInDiagonal(
+ cards: Card[],
+ length: number,
+ isLeftToRight: boolean,
+ ): Card[][] {
+ const series: Card[][] = [];
+ let tempSeries: Card[] = [];
+
+ for (let i = 0; i < cards.length; i++) {
+ if (
+ tempSeries.length === 0 ||
+ (this.isNextInDiagonal(
+ tempSeries[tempSeries.length - 1],
+ cards[i],
+ isLeftToRight,
+ ) &&
+ this.isSamePlayerAndColor(tempSeries[0], cards[i]))
+ ) {
+ tempSeries.push(cards[i]);
+ } else {
+ if (tempSeries.length >= length) {
+ series.push([...tempSeries]);
+ }
+ tempSeries = [cards[i]];
+ }
+ }
+
+ if (tempSeries.length >= length) {
+ series.push([...tempSeries]);
+ }
+
+ return series;
+ }
+
+ private isNextInDiagonal(
+ prevCard: Card,
+ currentCard: Card,
+ isLeftToRight: boolean,
+ ): boolean {
+ return isLeftToRight
+ ? currentCard.x === prevCard.x + 1 &&
+ currentCard.y === prevCard.y + 1
+ : currentCard.x === prevCard.x + 1 &&
+ currentCard.y === prevCard.y - 1;
+ }
+
+ private isSamePlayerAndColor(card1: Card, card2: Card): boolean {
+ return (
+ card1.color === card2.color &&
+ card1.playedBy?.id === card2.playedBy?.id
+ );
+ }
+
+ /**
+ * Retrieves the number of players in the game.
+ * @returns {number} The number of players in the game.
+ */
+ public nbrPlayers(): number {
+ return this._players.length;
+ }
+
+ /**
+ * Retrieve the player whose turn it is.
+ * @returns {Player|undefined} The player whose turn it is or undefined if no player has the turn.
+ */
+ public getPlayerTurn(): Player | undefined {
+ return this._players.find((player) => player.isTurn === true);
+ }
+
+ /**
+ * Randomizes the player whose turn it is and returns the player.
+ * @param {boolean} onlyWinners Whether to only randomize the player whose turn it is among the winner(s) of the game. (if there are any, otherwise it will be randomized among all players)
+ * @returns {Player} The player whose turn it is.
+ */
+ public randomizePlayerTurn(onlyWinners: boolean = false): Player {
+ let playerWithTurn: Player;
+
+ if (onlyWinners && this._winners.length > 0) {
+ playerWithTurn =
+ this._winners[
+ Math.floor(Math.random() * 1000) % this._winners.length
+ ];
+ } else {
+ playerWithTurn =
+ this._players[
+ Math.floor(Math.random() * 1000) % this._players.length
+ ];
+ }
+
+ playerWithTurn.isTurn = true;
+
+ return playerWithTurn;
+ }
+
+ /**
+ * Select the player whose turn it is based on the number of times they have been the first player.
+ * @param {boolean} byMax Whether to select the player with the most times as the first player or the least times.
+ * @returns {Player} The player whose turn it is.
+ */
+ public playerTurnByNbrFirstPlayer(byMax: boolean = false): Player {
+ // Initialize to the first player as a default
+ let playerWithTurn: Player = this._players[0];
+
+ // Initialize to the lowest possible value if byMax is true, otherwise initialize to the highest possible value
+ let nbrFirstPlayer: number = byMax ? -Infinity : Infinity;
+
+ this._players.forEach((player) => {
+ if (byMax) {
+ if (player.nbrFirstPlayer > nbrFirstPlayer) {
+ playerWithTurn = player;
+
+ nbrFirstPlayer = player.nbrFirstPlayer;
+ }
+ } else {
+ if (player.nbrFirstPlayer < nbrFirstPlayer) {
+ playerWithTurn = player;
+
+ nbrFirstPlayer = player.nbrFirstPlayer;
+ }
+ }
+ });
+
+ playerWithTurn.isTurn = true;
+
+ return playerWithTurn;
+ }
+
+ /**
+ * Advances the turn number by 1.
+ */
+ public addTurn(): void {
+ this._turn++;
+ }
+
+ /**
+ * Update the win type.
+ * @param {WinType} winType The type of win achieved.
+ */
+ public updateWinType(winType: WinType): void {
+ this._winType = winType;
+ }
+
+ /**
+ * Updates the array of winner(s) of the game.
+ * @param {Player[]} winner An array of Player objects who have won the game.
+ */
+ public updateWinner(winner: Player[]): void {
+ this._winners = winner;
+ }
+
+ /**
+ * Updates the array of loser(s) of the game.
+ * @param {Player[]} loser An array of Player objects who have lost the game.
+ */
+ public updateLoser(loser: Player[]): void {
+ this._losers = loser;
+ }
+
+ /**
+ * Plays a turn of the game.
+ * @param {boolean} auto Whether to play the turn automatically.
+ * @param {boolean} displayEachPlayerTurn Whether to display the board after each player's turn.
+ */
+ public async doATurn(
+ auto: boolean = false,
+ displayEachPlayerTurn: boolean = false,
+ ): Promise {
+ if (this._winType !== WinType.None) {
+ throw new Error("The game is already over.");
+ }
+
+ let playerTurn = 0;
+
+ for (const player of this._players) {
+ if (this.isGameOver()) {
+ return;
+ }
+
+ player.isTurn = true;
+
+ // * do not use const card here because it will be false
+ // * and this will cause an error with the code below
+ // * (i.e if (card) { ... } will be false)
+
+ if (!player.cardInHand()) {
+ if (!player.drawCard()) {
+ const gameVictory = this.checkVictory();
+
+ this.endGame(
+ gameVictory.winType,
+ gameVictory.winners,
+ gameVictory.losers,
+ );
+
+ return;
+ }
+ } else {
+ throw new Error("The player already has a card in hand.");
+ }
+
+ const card = player.cardInHand();
+
+ if (card) {
+ let x = 0;
+ let y = 0;
+
+ let cardCanBePlaced: boolean;
+
+ let firstTimeToDemandeCoordinates = true;
+
+ do {
+ if (this.boardIsEmpty()) {
+ cardCanBePlaced = this.cardCanBePlaced(card, x, y);
+ } else {
+ let coordinates: Coordinates | false;
+
+ if (auto) {
+ coordinates = this.determineCardCoordinates(
+ card.value,
+ );
+ } else {
+ coordinates = await player.askCoordinates(
+ card,
+ firstTimeToDemandeCoordinates,
+ );
+
+ if (
+ coordinates &&
+ (coordinates.x === Infinity ||
+ coordinates.y === Infinity)
+ ) {
+ coordinates = this.determineCardCoordinates(
+ card.value,
+ );
+ }
+ }
+
+ if (coordinates) {
+ x = coordinates.x;
+ y = coordinates.y;
+ } else {
+ const gameVictory = this.checkVictory();
+
+ this.endGame(
+ gameVictory.winType,
+ gameVictory.winners,
+ gameVictory.losers,
+ );
+
+ return;
+ }
+
+ cardCanBePlaced = this.cardCanBePlaced(card, x, y);
+
+ if (!cardCanBePlaced) {
+ firstTimeToDemandeCoordinates = false;
+ }
+ }
+ } while (!cardCanBePlaced);
+
+ const playResult = this.playCard(
+ player,
+ playerTurn,
+ card,
+ x,
+ y,
+ );
+
+ if (playResult !== "The card was successfully placed.") {
+ this.endGame(WinType.Drop, [], this._players);
+ return;
+ }
+
+ if (displayEachPlayerTurn) {
+ this.displayBoard();
+ }
+
+ const gameVictory = this.checkVictory();
+
+ if (gameVictory.winType !== WinType.None) {
+ this.endGame(
+ gameVictory.winType,
+ gameVictory.winners,
+ gameVictory.losers,
+ );
+ return;
+ }
+
+ player.removeCardFromHand();
+ } else {
+ throw new Error("The player does not have a card in hand.");
+ }
+
+ player.isTurn = false;
+
+ playerTurn++;
+ }
+
+ playerTurn = 0;
+
+ this.addTurn();
+
+ this._players[0].isTurn = true;
+ }
+
+ /**
+ * Determines the coordinates of the card on the board.
+ * @param {number} cardValue The value of the card to place on the board.
+ * @returns {Coordinates|false} The coordinates of the card on the board or `false` if the card is no coordinates are available.
+ */
+ public determineCardCoordinates(cardValue: number): Coordinates | false {
+ const availableCoordinates = this.availableCoordinates(cardValue);
+
+ if (availableCoordinates.length === 0) {
+ return false;
+ }
+
+ // Select a random coordinate
+ const coordinates: Coordinates =
+ availableCoordinates[
+ Math.floor(Math.random() * availableCoordinates.length)
+ ];
+
+ return coordinates;
+ }
+
+ /**
+ * Returns the available coordinates on the board.
+ * @param {number} cardValue The value of the card to place on the board.
+ * @returns {Coordinates[]} An array of available coordinates on the board.
+ */
+ public availableCoordinates(cardValue: number): Coordinates[] {
+ const maxX = 5;
+ const minX = -5;
+ const maxY = 5;
+ const minY = -5;
+
+ const availableCoordinates: Coordinates[] = [];
+
+ // For each card on the board, add the available coordinates around it (empty spaces)
+ // And if a space is occupied by a card with a value less than the card to place, add it to the available coordinates
+
+ this._cards.forEach((cardOnBoard) => {
+ const x = cardOnBoard.x;
+ const y = cardOnBoard.y;
+
+ // Add the available coordinates around the card on the board
+
+ const possibleCoordinates: Coordinates[] = [];
+
+ possibleCoordinates.push({
+ x: x,
+ y: y,
+ });
+
+ if (x + 1 <= maxX) {
+ possibleCoordinates.push({
+ x: x + 1,
+ y: y,
+ });
+ }
+
+ if (x - 1 >= minX) {
+ possibleCoordinates.push({
+ x: x - 1,
+ y: y,
+ });
+ }
+
+ if (y + 1 <= maxY) {
+ possibleCoordinates.push({
+ x: x,
+ y: y + 1,
+ });
+ }
+
+ if (y - 1 >= minY) {
+ possibleCoordinates.push({
+ x: x,
+ y: y - 1,
+ });
+ }
+
+ if (x + 1 <= maxX && y + 1 <= maxY) {
+ possibleCoordinates.push({
+ x: x + 1,
+ y: y + 1,
+ });
+ }
+
+ if (x - 1 >= minX && y - 1 >= minY) {
+ possibleCoordinates.push({
+ x: x - 1,
+ y: y - 1,
+ });
+ }
+
+ if (x + 1 <= maxX && y - 1 >= minY) {
+ possibleCoordinates.push({
+ x: x + 1,
+ y: y - 1,
+ });
+ }
+
+ if (x - 1 >= minX && y + 1 <= maxY) {
+ possibleCoordinates.push({
+ x: x - 1,
+ y: y + 1,
+ });
+ }
+
+ // Remove the coordinates that are already occupied by a card with a value higher or equal to the card to place
+
+ const availableCoordinatesAroundCard = possibleCoordinates.filter(
+ (coordinates) => {
+ let canPlaceCard = true;
+
+ this._cards.forEach((cardOnBoard) => {
+ if (
+ cardOnBoard.x === coordinates.x &&
+ cardOnBoard.y === coordinates.y
+ ) {
+ if (cardOnBoard.value >= cardValue) {
+ canPlaceCard = false;
+ }
+ }
+ });
+
+ return canPlaceCard;
+ },
+ );
+
+ availableCoordinates.push(...availableCoordinatesAroundCard);
+ });
+
+ // Remove the coordinates that create a line of +6 cards (i.e., 7 cards in a row)
+ const validCoordinates = availableCoordinates.filter(
+ (coordinates) => !this.lineExceedsLimit(coordinates, 6),
+ );
+
+ return validCoordinates;
+ }
+
+ private lineExceedsLimit(coordinates: Coordinates, limit: number): boolean {
+ let extremeMaxX = 0;
+ let extremeMinX = 0;
+ let extremeMaxY = 0;
+ let extremeMinY = 0;
+
+ this._cards.forEach((cardOnBoard) => {
+ if (cardOnBoard.x > extremeMaxX) {
+ extremeMaxX = cardOnBoard.x;
+ }
+
+ if (cardOnBoard.x < extremeMinX) {
+ extremeMinX = cardOnBoard.x;
+ }
+
+ if (cardOnBoard.y > extremeMaxY) {
+ extremeMaxY = cardOnBoard.y;
+ }
+
+ if (cardOnBoard.y < extremeMinY) {
+ extremeMinY = cardOnBoard.y;
+ }
+ });
+
+ const xDistance = extremeMaxX - extremeMinX;
+ const yDistance = extremeMaxY - extremeMinY;
+
+ if (xDistance >= limit || yDistance >= limit) {
+ return true;
+ }
+
+ const {x, y} = coordinates;
+
+ if (x > extremeMaxX) {
+ extremeMaxX = x;
+ }
+
+ if (x < extremeMinX) {
+ extremeMinX = x;
+ }
+
+ if (y > extremeMaxY) {
+ extremeMaxY = y;
+ }
+
+ if (y < extremeMinY) {
+ extremeMinY = y;
+ }
+
+ const newXDistance = extremeMaxX - extremeMinX;
+ const newYDistance = extremeMaxY - extremeMinY;
+
+ if (newXDistance >= limit || newYDistance >= limit) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Displays the cards on the board in the console.
+ * @returns {void}
+ * @memberof Board
+ */
+ public displayBoard(): void {
+ const size = 13;
+ const halfSize = Math.floor(size / 2);
+ let displayString = "";
+
+ // Create the first row with column indices
+ for (let j = -halfSize; j <= halfSize; j++) {
+ if (j === -halfSize) {
+ displayString += "yx";
+ }
+
+ displayString += j >= 0 ? ` 0${j} ` : ` ${j} `;
+
+ if (j === halfSize) {
+ displayString += "x";
+ }
+ }
+
+ displayString += "\n";
+
+ // Fill the table with values or spaces
+ for (let i = -halfSize; i <= halfSize; i++) {
+ for (let j = -halfSize; j <= halfSize; j++) {
+ const filteredCards = this._cards.filter(
+ (card: Card) => card.x === j && card.y === i,
+ );
+
+ let card;
+ if (filteredCards.length > 0) {
+ card = filteredCards.reduce(
+ (highestCard: Card, currentCard: Card) => {
+ return currentCard.value > highestCard.value
+ ? currentCard
+ : highestCard;
+ },
+ );
+ } else {
+ card = null;
+ }
+
+ if (j === -halfSize) {
+ // Add the line index to the beginning of each line
+ displayString += i >= 0 ? `0${i}` : `${i}`;
+ }
+
+ const cardColor = card ? card.color : "";
+
+ const colorReset = "\x1b[0m";
+ // const colorBright = "\x1b[1m";
+ // const colorDim = "\x1b[2m";
+
+ const colorWhite = "\x1b[37m";
+ const colorPink = "\x1b[35m";
+
+ const colorRed = "\x1b[31m";
+ // const colorGreen = "\x1b[32m";
+ const colorGreen = "\x1b[92m"; // Bright green
+ const colorYellow = "\x1b[33m";
+ // const colorBlue = "\x1b[34m";
+ const colorBlue = "\x1b[94m"; // Bright blue
+
+ let coloredTextCard = "";
+
+ if (card) {
+ switch (cardColor) {
+ case "red":
+ coloredTextCard = `${colorRed} ${card.color[0]}${card.value} ${colorReset}`;
+ break;
+ case "green":
+ coloredTextCard = `${colorGreen} ${card.color[0]}${card.value} ${colorReset}`;
+ break;
+ case "yellow":
+ coloredTextCard = `${colorYellow} ${card.color[0]}${card.value} ${colorReset}`;
+ break;
+ case "blue":
+ coloredTextCard = `${colorBlue} ${card.color[0]}${card.value} ${colorReset}`;
+ break;
+ default:
+ coloredTextCard = ` ${card.color[0]}${card.value} `;
+ break;
+ }
+ }
+
+ // displayString += card
+ // ? coloredTextCard
+ // : `${colorWhite} .. ${colorReset}`; // Use ".." for empty fields
+
+ if (card) {
+ displayString += coloredTextCard;
+ } else {
+ // Check if the empty field is adjacent to any card
+ displayString += this.isEmptyFieldAdjacent(j, i)
+ ? `${colorWhite} .. ${colorReset}`
+ : `${colorPink} XX ${colorReset}`;
+ }
+ }
+
+ if (i === halfSize) {
+ displayString += "\ny";
+ }
+
+ displayString += "\n";
+ }
+
+ // Display table
+ console.log(displayString);
+
+ console.log(
+ `Played cards: ${this._cards.length}`,
+ `Cards remaining: ${72 - this._cards.length}`,
+ "\n",
+ );
+ }
+
+ private isEmptyFieldAdjacent(x: number, y: number): boolean {
+ const adjacentOffsets = [
+ {dx: -1, dy: -1},
+ {dx: 0, dy: -1},
+ {dx: 1, dy: -1},
+ {dx: -1, dy: 0},
+ {dx: 1, dy: 0},
+ {dx: -1, dy: 1},
+ {dx: 0, dy: 1},
+ {dx: 1, dy: 1},
+ ];
+
+ return adjacentOffsets.some((offset) => {
+ return this._cards.some(
+ (card) => card.x === x + offset.dx && card.y === y + offset.dy,
+ );
+ });
+ }
+
+ /**
+ * Returns whether the board is empty.
+ * @returns {boolean} `true` if the board is empty, `false` otherwise.
+ */
+ public boardIsEmpty(): boolean {
+ return this._cards.length === 0;
+ }
+
+ /**
+ * Returns whether the board is full.
+ * @returns {boolean} `true` if the board is full, `false` otherwise.
+ */
+ public boardIsFull(): boolean {
+ return this._cards.length === 72;
+ }
+
+ /**
+ * Returns whether the game is over.
+ * @returns {boolean} `true` if the game is over, `false` otherwise.
+ */
+ public isGameOver(): boolean {
+ return this._winType !== WinType.None;
+ }
+
+ /**
+ * Ends the game and determines the winner(s) and loser(s).
+ * @param {WinType} winType The type of win.
+ * @param {Player[]} winners The winner(s) of the game.
+ * @param {Player[]} losers The loser(s) of the game.
+ */
+ private endGame(
+ winType: WinType,
+ winners: Player[],
+ losers: Player[],
+ ): void {
+ this.updateWinType(winType);
+
+ this.updateLoser(losers);
+
+ this.updateWinner(winners);
+
+ if (winType === WinType.Win) {
+ this.addPoints();
+ }
+ }
+
+ /**
+ * Adds points to the winner(s) of the game.
+ * @param {Player[]} players The player(s) to add points to. By default, the winner(s) of the game.
+ * @param {number} pointsToAdd The number of points to add. By default, 1.
+ */
+ private addPoints(
+ players: Player[] = this._winners,
+ pointsToAdd: number = 1,
+ ): void {
+ players.forEach((player) => {
+ player.addPoints(pointsToAdd);
+ });
+ }
+
+ /**
+ * Resets the board to its initial state.
+ */
+ public reset(): void {
+ this.regenerateId();
+
+ this._cards = [];
+
+ this._players.forEach((player) => {
+ player.reset();
+ });
+
+ this._turn = 0;
+
+ this._winType = WinType.None;
+
+ // const playerWithTurn = this.randomizePlayerTurn();
+ const playerWithTurn = this.playerTurnByNbrFirstPlayer();
+
+ playerWithTurn.nbrFirstPlayer++;
+
+ this._winners = [];
+
+ this._losers = [];
+
+ this.cardDistribution();
+ }
+}
+
+/**
+ * The options for the board.
+ */
+type BoardOptions = {
+ /**
+ * The number of players in the game.
+ * @default 2
+ * @type {number}
+ * @memberof BoardOptions
+ */
+ nbrPlayers: number;
+
+ /**
+ * The options for the players.
+ * @type {PlayerOptions[]}
+ * @memberof BoardOptions
+ */
+ listPlayerOptions: PlayerOptions[];
+};
+
+/**
+ * The type of win achieved.
+ */
+enum WinType {
+ /**
+ * A player has won the game.
+ * @type {string}
+ */
+ Win = "Win",
+
+ /**
+ * The game is a draw.
+ * @type {string}
+ */
+ Draw = "Draw",
+
+ /**
+ * A player has dropped out of the game.
+ * @type {string}
+ */
+ Drop = "Drop",
+
+ /**
+ * No player has won the game.
+ * @type {string}
+ */
+ None = "None",
+}
+
+export default Board;
+
+export {Board, BoardOptions, WinType};
diff --git a/src/game/Card.ts b/src/game/Card.ts
new file mode 100644
index 0000000..dc6aeff
--- /dev/null
+++ b/src/game/Card.ts
@@ -0,0 +1,154 @@
+import BaseId from "./BaseId";
+import Player from "./Player";
+
+/**
+ * Represents a card in a card game.
+ * Inherits a unique ID from BaseId class
+ * and has a color, value, coordinates, turn played, position played, and player who played it.
+ */
+class Card extends BaseId {
+ /**
+ * The color attribute of the card, represented as a string.
+ * @type {string}
+ */
+ public readonly color: string;
+
+ /**
+ * The numerical value of the card.
+ * @type {number}
+ */
+ public readonly value: number;
+
+ /**
+ * The coordinates of the card on the board.
+ * @type {Coordinates}
+ */
+ public coordinates: Coordinates;
+
+ /**
+ * The x-coordinate of the card on the board.
+ * @type {number}
+ */
+ public get x(): number {
+ return this.coordinates.x;
+ }
+ public set x(x: number) {
+ this.coordinates.x = x;
+ }
+
+ /**
+ * The y-coordinate of the card on the board.
+ * @type {number}
+ */
+ public get y(): number {
+ return this.coordinates.y;
+ }
+ public set y(y: number) {
+ this.coordinates.y = y;
+ }
+
+ /**
+ * The turn the card was played on.
+ * @type {number}
+ */
+ public playedTurn: number;
+
+ /**
+ * The position of the card in the turn it was played on.
+ * @type {number}
+ */
+ public playedIn: number;
+
+ /**
+ * The player who played the card.
+ * @type {Player}
+ */
+ public playedBy?: Player;
+
+ /**
+ * Displays the ID of the Card instance in the console.
+ */
+ public listIds(): void {
+ this.displayId();
+ }
+
+ /**
+ * Constructs a new card with the specified color and value.
+ * @param {string} color - The color of the card.
+ * @param {number} value - The numerical value of the card.
+ * @param {number} x - The x-coordinate of the card on the board.
+ * @param {number} y - The y-coordinate of the card on the board.
+ * @param {number} playedTurn - The turn the card was played on.
+ * @param {number} playedIn - The position of the card in the turn it was played on.
+ * @param {Player} playerBy - The player who played the card.
+ */
+ constructor(
+ color: string,
+ value: number,
+ x: number = NaN,
+ y: number = NaN,
+ playedTurn: number = -1,
+ playedIn: number = -1,
+ playerBy?: Player,
+ ) {
+ super();
+
+ this.color = color;
+
+ this.value = value;
+
+ this.coordinates = {x, y};
+
+ this.playedTurn = playedTurn;
+
+ this.playedIn = playedIn;
+
+ this.playedBy = playerBy;
+ }
+
+ public static build(
+ color: string,
+ value: number,
+ x: number,
+ y: number,
+ playedTurn: number,
+ playedIn: number,
+ playerBy?: Player,
+ ): Card {
+ return new Card(color, value, x, y, playedTurn, playedIn, playerBy);
+ }
+
+ /**
+ * Returns the coordinates of the card on the board.
+ * @param {boolean} isString Whether to return the coordinates as a string or as an object.
+ * @returns {string | Coordinates} The coordinates of the card on the board.
+ */
+ public getCoordinates(isString: boolean = true): Coordinates | string {
+ if (isString) {
+ return `(${this.coordinates.x}, ${this.coordinates.y})`;
+ } else {
+ return this.coordinates;
+ }
+ }
+
+ /**
+ * Returns whether the card has the same coordinates as the given card.
+ * @param {Card} card The card to compare to.
+ * @returns {boolean} `true` if the cards have the same coordinates, `false` otherwise.
+ */
+ public sameCoordinates(card: Card): boolean {
+ return (
+ this.coordinates.x === card.coordinates.x &&
+ this.coordinates.y === card.coordinates.y
+ );
+ }
+}
+
+type Coordinates = {
+ x: number;
+ y: number;
+};
+
+export default Card;
+
+export {Card, Coordinates};
diff --git a/src/game/CreatePunto.ts b/src/game/CreatePunto.ts
new file mode 100644
index 0000000..fdbc659
--- /dev/null
+++ b/src/game/CreatePunto.ts
@@ -0,0 +1,56 @@
+import {BoardOptions} from "./Board";
+import Punto, {PuntoOptions} from "./Punto";
+
+class CreatePunto {
+ private _puntoOptions: PuntoOptions;
+
+ private _punto: Punto | null = null;
+
+ constructor(puntoOptions: PuntoOptions) {
+ this._puntoOptions = puntoOptions;
+ }
+
+ //#region listOfBoardOptions
+ public getBoardOption(): BoardOptions {
+ return this._puntoOptions.boardOption;
+ }
+
+ public setBoardOption(boardOptions: BoardOptions): void {
+ this._puntoOptions.boardOption = boardOptions;
+ }
+ //#endregion
+
+ //#region punto
+ public getPunto(): Punto | null {
+ return this._punto;
+ }
+
+ public createPunto(): void {
+ this._punto = new Punto(this._puntoOptions);
+ }
+
+ public displayPunto(verbose: boolean = false): void {
+ if (this._punto !== null) {
+ console.log("Punto ID: ", this._punto.id, "\n");
+
+ if (verbose) {
+ const thisBoard = this._punto.board;
+
+ console.log("Board ID: ", thisBoard.id);
+
+ console.log("Number of players: ", thisBoard.nbrPlayers());
+
+ thisBoard.players.forEach((player) => {
+ console.log("Name: ", player.name);
+ });
+
+ console.log("\n");
+ }
+ } else {
+ console.log("Punto is null");
+ }
+ }
+ //#endregion
+}
+
+export default CreatePunto;
diff --git a/src/game/Player.ts b/src/game/Player.ts
new file mode 100644
index 0000000..79b9220
--- /dev/null
+++ b/src/game/Player.ts
@@ -0,0 +1,245 @@
+import Interface from "../interface/Interface";
+import BaseId from "./BaseId";
+import Card, {Coordinates} from "./Card";
+
+/**
+ * Represents a player in a card game. Each player is assigned a unique ID from the BaseId class,
+ * has a name, a hand of cards, a points tally, and a flag to indicate if it's their turn.
+ */
+class Player extends BaseId {
+ /**
+ * The name of the player.
+ * @type {string}
+ */
+ private _name: string;
+ public get name(): string {
+ return this._name;
+ }
+ public set name(name: string) {
+ this._name = name;
+ }
+
+ /**
+ * Deck of cards yet to be drawn.
+ * @type {Card[]}
+ */
+ private _deck: Card[] = [];
+ public get deck(): Card[] {
+ return this._deck;
+ }
+ public set deck(deck: Card[]) {
+ this._deck = deck;
+ }
+
+ /**
+ * Hand of cards currently held by the player.
+ * @type {Card}
+ */
+ private _hand: Card | null = null;
+ public get hand(): Card | null {
+ return this._hand;
+ }
+ public set hand(hand: Card) {
+ this._hand = hand;
+ }
+
+ /**
+ * The number of points the player has accumulated.
+ * @type {number}
+ */
+ private _points: number;
+ public get points(): number {
+ return this._points;
+ }
+
+ /**
+ * A boolean indicating whether it is this player's turn.
+ * @type {boolean}
+ */
+ public isTurn: boolean;
+
+ /**
+ * The color attribute of the card, represented as a string.
+ */
+ public color: string[] = [];
+
+ /**
+ * The number of times the player was the first to play.
+ */
+ public nbrFirstPlayer = 0;
+
+ /**
+ * Displays the ID of the Player instance in the console
+ * and calls the listIds method to display the IDs of the Card instances.
+ */
+ public listIds(): void {
+ this.displayId();
+
+ this._hand?.listIds();
+ }
+
+ /**
+ * Constructs a new player with the provided name, points and isTurn flag.
+ * @param {PlayerOptions} playerOptions An object containing the options for the player.
+ */
+ constructor(playerOptions: PlayerOptions) {
+ super();
+
+ const {name, points, isTurn} = playerOptions;
+
+ this._name = name;
+
+ this._points = points ?? 0;
+
+ this.isTurn = isTurn ?? false;
+ }
+
+ public static build(
+ name: string,
+ deck: Card[],
+ hand: Card | null,
+ points: number,
+ isTurn: boolean,
+ color: string[],
+ nbrFirstPlayer: number,
+ ): Player {
+ const playerOptions = {
+ name,
+ points,
+ isTurn,
+ };
+
+ const player = new Player(playerOptions);
+
+ player._deck = deck;
+ player._hand = hand;
+ player.color = color;
+ player.nbrFirstPlayer = nbrFirstPlayer;
+
+ return player;
+ }
+
+ /**
+ * Fills the deck with cards.
+ * @param {Card[]} cards An array of Card objects to fill the deck with.
+ */
+ public fillDeck(cards: Card[]): void {
+ this._deck.push(...cards);
+
+ this.shuffleDeck();
+ }
+
+ /**
+ * Shuffles the deck of cards.
+ */
+ public shuffleDeck(): void {
+ for (let i = this._deck.length - 1; i > 0; i--) {
+ const j = Math.floor(Math.random() * (i + 1));
+
+ [this._deck[i], this._deck[j]] = [this._deck[j], this._deck[i]];
+ }
+ }
+
+ /**
+ * Draws a card from the deck and adds it to the player's hand.
+ * @returns {boolean} `true` if the card was successfully drawn, `false` otherwise.
+ */
+ public drawCard(): boolean {
+ const drawnCard = this._deck.pop();
+
+ if (drawnCard) {
+ this._hand = drawnCard;
+
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Returns the actual card from the player's hand.
+ * @returns {Card|false} The card from the player's hand, or `false` if the hand is empty.
+ */
+ public cardInHand(): Card | false {
+ const card = this._hand;
+
+ if (card) {
+ return card;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Removes the card from the player's hand.
+ * @returns {boolean} `true` if the card was successfully removed, `false` otherwise.
+ */
+ public removeCardFromHand(): boolean {
+ if (this._hand) {
+ this._hand = null;
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Sets the player's points to the given value.
+ * @param {number} points The new points value for the player.
+ */
+ public addPoints(points: number): void {
+ this._points += points;
+ }
+
+ /**
+ * Resets the player's deck, hand, and isTurn flag.
+ */
+ public reset(): void {
+ this._deck = [];
+ this._hand = null;
+ this._points = 0;
+ this.isTurn = false;
+ this.color = [];
+ }
+
+ /**
+ * Asks the player for the coordinates of the card they want to play.
+ * @param {Card} card The card the player wants to play.
+ * @param {boolean} firstTimeToDemandeCoordinates Whether it is the first time the player is asked for coordinates.
+ * @returns {Promise} The coordinates of the card the player wants to play.
+ */
+ public async askCoordinates(
+ card: Card,
+ firstTimeToDemandeCoordinates: boolean,
+ ): Promise {
+ return new Promise((resolve) => {
+ const questionInterface = new Interface();
+
+ const options = {
+ firstTimeToDemandeCoordinates,
+ playerName: this._name,
+ cardColor: card.color,
+ cardValue: card.value,
+ };
+
+ questionInterface
+ .getCoordinates(options)
+ .then((coordinates) => {
+ resolve(coordinates);
+ })
+ .catch(() => {
+ resolve(false);
+ });
+ });
+ }
+}
+
+type PlayerOptions = {
+ name: string;
+ points?: number;
+ isTurn?: boolean;
+};
+
+export default Player;
+
+export {Player, PlayerOptions};
diff --git a/src/game/Punto.ts b/src/game/Punto.ts
new file mode 100644
index 0000000..fff9b5d
--- /dev/null
+++ b/src/game/Punto.ts
@@ -0,0 +1,217 @@
+import BaseId from "./BaseId";
+import Board, {BoardOptions} from "./Board";
+
+/**
+ * The Punto class, which is the main class of the game.
+ * It extends BaseId to inherit ID management capabilities.
+ */
+class Punto extends BaseId {
+ /**
+ * The board of the game, represented by an instance of Board.
+ * @type {Board}
+ */
+ private _board: Board;
+ public get board(): Board {
+ return this._board;
+ }
+
+ /**
+ * The mode in which the board is displayed.
+ * @type {DisplayBoardMode}
+ */
+ public displayBoardMode: DisplayBoardMode;
+
+ /**
+ * A boolean indicating whether the result of the game should be displayed.
+ * @type {boolean}
+ */
+ public displayResultOfGame: boolean;
+
+ /**
+ * A boolean indicating whether the game is played automatically.
+ * @type {boolean}
+ */
+ public auto: boolean;
+
+ /**
+ * Displays the ID of the Punto instance in the console
+ * and calls the listIds method to display the IDs of the Board instances.
+ */
+ public listIds(): void {
+ this.displayId();
+
+ this._board.listIds();
+ }
+
+ /**
+ * The constructor for the Punto class.
+ * It initializes the game board.
+ * @param {PuntoOptions} puntoOptions An object containing the options for the game.
+ */
+ constructor(puntoOptions: PuntoOptions) {
+ super();
+
+ const {displayBoard, displayResultOfGame, boardOption} = puntoOptions;
+
+ this.displayBoardMode = displayBoard ?? DisplayBoardMode.noDisplay;
+
+ this.displayResultOfGame = displayResultOfGame ?? false;
+
+ this._board = new Board(boardOption);
+
+ this.auto = puntoOptions.auto ?? false;
+ }
+
+ public static build(
+ displayBoard: DisplayBoardMode,
+ displayResultOfGame: boolean,
+ boardOption: BoardOptions,
+ auto: boolean,
+ ): Punto {
+ return new Punto({
+ displayBoard,
+ displayResultOfGame,
+ boardOption,
+ auto,
+ });
+ }
+
+ /**
+ * Plays a turn of the game.
+ * @param {number} nbrTurn The number of turns to play.
+ */
+ private async playATurn(nbrTurn: number): Promise {
+ let displayEachPlayerTurn = false;
+
+ if (this.displayBoardMode === DisplayBoardMode.eachPlayerTurn) {
+ displayEachPlayerTurn = true;
+ }
+
+ for (let i = 0; i < nbrTurn; i++) {
+ if (this._board.isGameOver()) {
+ break;
+ }
+
+ await this._board.doATurn(this.auto, displayEachPlayerTurn);
+
+ if (this.displayBoardMode === DisplayBoardMode.eachBoardTurn) {
+ this._board.displayBoard();
+ }
+ }
+
+ if (
+ this.displayBoardMode === DisplayBoardMode.startAndEnd ||
+ this.displayBoardMode === DisplayBoardMode.onlyEnd
+ ) {
+ this._board.displayBoard();
+ }
+
+ if (this.displayResultOfGame) {
+ this.displayResult();
+ }
+ }
+
+ private async playUntilGameOver(): Promise {
+ let displayEachPlayerTurn = false;
+
+ if (this.displayBoardMode === DisplayBoardMode.eachPlayerTurn) {
+ displayEachPlayerTurn = true;
+ }
+
+ while (!this._board.isGameOver()) {
+ await this._board.doATurn(this.auto, displayEachPlayerTurn);
+
+ if (this.displayBoardMode === DisplayBoardMode.eachBoardTurn) {
+ this._board.displayBoard();
+ }
+ }
+
+ if (
+ this.displayBoardMode === DisplayBoardMode.startAndEnd ||
+ this.displayBoardMode === DisplayBoardMode.onlyEnd
+ ) {
+ this._board.displayBoard();
+ }
+
+ if (this.displayResultOfGame) {
+ this.displayResult();
+ }
+ }
+
+ /**
+ * Plays a game of Punto.
+ * @param {number} nbrTurn The number of turns to play. If -1, the game is played until the end.
+ */
+ public async playGame(nbrTurn: number = -1): Promise {
+ if (this.displayBoardMode === DisplayBoardMode.startAndEnd) {
+ this._board.displayBoard();
+ }
+
+ if (nbrTurn <= 0) {
+ await this.playUntilGameOver();
+ } else {
+ await this.playATurn(nbrTurn);
+ }
+ }
+
+ /**
+ * Displays the board in the console.
+ */
+ public displayBoard(): void {
+ this._board.displayBoard();
+ }
+
+ /**
+ * Displays the result of the game in the console.
+ */
+ private displayResult(): void {
+ if (this._board.winners.length === 1) {
+ console.log(`The winner is ${this._board.winners[0].name}.`);
+ } else {
+ console.log(
+ `The winners are ${this._board.winners
+ .map((player) => player.name)
+ .join(", ")}.`,
+ );
+ }
+
+ if (this._board.losers.length === 1) {
+ console.log(`The loser is ${this._board.losers[0].name}.`);
+ } else {
+ console.log(
+ `The losers are ${this._board.losers
+ .map((player) => player.name)
+ .join(", ")}.`,
+ );
+ }
+
+ console.log(`The game lasted ${this._board.turn} turns.\n`);
+ }
+
+ /**
+ * Resets the game.
+ */
+ public reset(): void {
+ this.regenerateId();
+ this._board.reset();
+ }
+}
+
+enum DisplayBoardMode {
+ "eachBoardTurn",
+ "eachPlayerTurn",
+ "startAndEnd",
+ "onlyEnd",
+ "noDisplay",
+}
+
+type PuntoOptions = {
+ displayBoard?: DisplayBoardMode;
+ displayResultOfGame?: boolean;
+ boardOption: BoardOptions;
+ auto?: boolean;
+};
+
+export default Punto;
+
+export {Punto, PuntoOptions, DisplayBoardMode};
diff --git a/src/interface/Interface.ts b/src/interface/Interface.ts
new file mode 100644
index 0000000..85620c7
--- /dev/null
+++ b/src/interface/Interface.ts
@@ -0,0 +1,1582 @@
+import * as readlinePromises from "node:readline/promises";
+import {Coordinates} from "../game/Card";
+import ReadlineSingleton from "./ReadlineSingleton";
+import {PlayerOptions} from "../game/Player";
+import {BoardOptions} from "../game/Board";
+import CreatePunto from "../game/CreatePunto";
+import Punto, {PuntoOptions, DisplayBoardMode} from "../game/Punto";
+import DBWrapper, {DBList} from "../db/DBWrapper";
+import UserManager from "../entities/User";
+import {ResultStatus} from "../db/Result";
+import GameManager from "../entities/Game";
+import Neo4jManager from "../db/Neo4jManager";
+
+enum InterfaceType {
+ None = "None",
+ Console = "Console",
+ Web = "Web",
+}
+
+/**
+ * Represents the interface to the user.
+ */
+class Interface {
+ private _state: IInterfaceState;
+
+ constructor() {
+ this._state = this.determineInterfaceType();
+ }
+
+ private determineInterfaceType(): IInterfaceState {
+ if (typeof window === "object") {
+ return new WebState();
+ } else if (typeof process === "object") {
+ return new ConsoleState();
+ } else {
+ return new NoneState();
+ }
+ }
+
+ /**
+ * Gets the type of interface.
+ * @param isString Determines whether to return the interface type as a string or as an enum.
+ * @returns The type of interface.
+ */
+ public getInterfaceType(isString: boolean = false): InterfaceType | string {
+ return this._state.getInterfaceType(isString);
+ }
+
+ /**
+ * Gets the coordinates of the card to play.
+ * @param options Options for the coordinates.
+ * @returns The coordinates of the card to play. If the player refused to play, returns false.
+ */
+ public async getCoordinates(
+ options: unknown = {},
+ ): Promise {
+ return await this._state.getCoordinates(options);
+ }
+
+ /**
+ * Launches the program.
+ * @param game Determines whether the game is launched or not. If not, the program will launch in a test mode.
+ */
+ public async launch(game: boolean): Promise {
+ return await this._state.launch(game);
+ }
+}
+
+/**
+ * Interface for interfaces states.
+ */
+interface IInterfaceState {
+ /**
+ * Instance of DBWrapper.
+ * @see DBWrapper
+ */
+ dbWrapper: DBWrapper;
+
+ /**
+ * Gets the type of interface.
+ * @param {boolean} isString Determines whether to return the interface type as a string or as an enum. Default: false.
+ * @returns {InterfaceType | string} The type of interface.
+ */
+ getInterfaceType(isString: boolean): InterfaceType | string;
+
+ /**
+ * Gets the coordinates of the card to play.
+ * @param {unknown} options Options for the coordinates.
+ * @returns {Promise} The coordinates of the card to play. If the player refused to play, returns false.
+ */
+ getCoordinates(options: unknown): Promise;
+
+ /**
+ * Launches the program.
+ * @param {boolean} game Determines whether the game is launched or not. If not, the program will launch in a test mode.
+ */
+ launch(game: boolean): Promise;
+}
+
+/**
+ * Represents the console interface.
+ */
+class ConsoleState implements IInterfaceState {
+ /**
+ * Instance of DBWrapper.
+ * This is a public and readonly member of ConsoleState.
+ * For more information, refer to DBWrapper documentation.
+ * @public
+ * @readonly
+ * @type {DBWrapper}
+ * @memberof ConsoleState
+ * @see DBWrapper
+ */
+ public readonly dbWrapper: DBWrapper = DBWrapper.getInstance();
+
+ /**
+ * Determines whether the terminal supports user input.
+ * @private
+ * @type {boolean}
+ * @memberof ConsoleState
+ */
+ private _terminalSupportUserInput: boolean = false;
+
+ /**
+ * Determines whether the terminal supports user input.
+ * @public
+ * @readonly
+ * @type {boolean}
+ * @memberof ConsoleState
+ */
+ public get terminalSupportUserInput(): boolean {
+ return this._terminalSupportUserInput;
+ }
+
+ /**
+ * Instance of readline.Interface.
+ * @private
+ * @type {readline.Interface}
+ * @memberof ConsoleState
+ */
+ private _rl: readlinePromises.Interface =
+ ReadlineSingleton.getReadlineInterface();
+
+ /**
+ * Instance of readline.Interface.
+ * This is a public and readonly member of ConsoleState.
+ * For more information, refer to Node.js documentation.
+ * @public
+ * @readonly
+ * @type {readline.Interface}
+ * @memberof ConsoleState
+ */
+ public get rl(): readlinePromises.Interface {
+ return this._rl;
+ }
+
+ /**
+ * Gets the type of interface.
+ * @param {boolean} isString Determines whether to return the interface type as a string or as an enum. Default: false.
+ * @returns {InterfaceType | string} The type of interface.
+ * @see InterfaceType
+ */
+ public getInterfaceType(isString: boolean): InterfaceType | string {
+ if (isString) {
+ return InterfaceType[InterfaceType.Console];
+ } else {
+ return InterfaceType.Console;
+ }
+ }
+
+ /**
+ * Gets the coordinates of the card to play.
+ * @param {unknown} options Options for the coordinates.
+ * @returns {Promise} The coordinates of the card to play. If the player refused to play, returns false.
+ */
+ public async getCoordinates(
+ options: unknown = {},
+ ): Promise {
+ const {
+ firstTimeToDemandeCoordinates,
+ playerName,
+ cardColor,
+ cardValue,
+ } = options as {
+ firstTimeToDemandeCoordinates: boolean | undefined;
+ playerName: string | undefined;
+ cardColor: string | undefined;
+ cardValue: number | undefined;
+ };
+
+ await this.sleep();
+
+ if (!firstTimeToDemandeCoordinates) {
+ this.writeInConsole(
+ "The coordinates you entered are invalid\nYou can refuse to play if you don't have any coordinates available",
+ 2,
+ 1,
+ );
+
+ await this.sleep();
+ } else {
+ this.writeInConsole(`${playerName}'s turn`, 2);
+
+ await this.sleep();
+ }
+
+ this.writeInConsole(`Card to play: ${cardColor} ${cardValue}`, 2);
+
+ await this.sleep();
+
+ return new Promise((resolve) => {
+ const questionX = async () => {
+ const answerX = await this.askQuestion(
+ 'X ("auto" for auto play of refuse if no x is available): ',
+ );
+
+ if (answerX === false) {
+ this.writeInConsole("You refused to play", 2, 1);
+
+ await this.sleep();
+
+ resolve(false);
+ } else if (answerX === "auto") {
+ this.writeInConsole("Auto play", 2, 1);
+
+ await this.sleep();
+
+ questionY(Infinity);
+ } else {
+ const x = Number.parseInt(answerX);
+
+ if (Number.isNaN(x)) {
+ this.writeInConsole(
+ "Please enter a valid number",
+ 2,
+ 1,
+ );
+
+ await this.sleep();
+
+ questionX();
+ } else {
+ questionY(x);
+ }
+ }
+ };
+
+ const questionY = async (x: number) => {
+ if (x === Infinity) {
+ resolve({x, y: Infinity});
+ return;
+ }
+
+ const answerY = await this.askQuestion(
+ 'Y ("auto" for auto play or refuse if no y is available): ',
+ );
+
+ if (answerY === false) {
+ this.writeInConsole("You refused to play", 2, 1);
+
+ await this.sleep();
+
+ resolve(false);
+ } else if (answerY === "auto") {
+ this.writeInConsole("Auto play", 2, 1);
+
+ await this.sleep();
+
+ resolve({x, y: Infinity});
+ } else {
+ const y = Number.parseInt(answerY);
+
+ if (Number.isNaN(y)) {
+ this.writeInConsole(
+ "Please enter a valid number",
+ 2,
+ 1,
+ );
+
+ await this.sleep();
+
+ questionY(x);
+ } else {
+ this.writeInConsole(
+ `Coordinates entered: (${x}, ${y})`,
+ 2,
+ 1,
+ );
+
+ await this.sleep();
+
+ resolve({x, y});
+ }
+ }
+ };
+
+ this.writeInConsole(
+ "Please enter the coordinates of the card you want to play",
+ 2,
+ );
+
+ questionX();
+ });
+ }
+
+ /**
+ * Sleeps for `ms` milliseconds.
+ * @param {number} ms The number of milliseconds to sleep.
+ * @returns {Promise} A promise that resolves when the sleep is over.
+ *
+ * Use it with `await`, else the sleep will be ignored.
+ *
+ * @example
+ * await this.sleep(); // Sleeps for 200 milliseconds
+ * await this.sleep(1000); // Sleeps for 1 second
+ *
+ */
+ private async sleep(ms: number = 200): Promise {
+ return new Promise((resolve) => setTimeout(resolve, ms));
+ }
+
+ /**
+ * Writes a message in the console.
+ * @param {string} message The message to write.
+ * @param {number} nbrLineBreakAfter The number of line breaks to add after the message. Default: 0.
+ * @param {number} nbrLineBreakBefore The number of line breaks to add before the message. Default: 0.
+ * @returns {void}
+ */
+ private writeInConsole(
+ message: string,
+ nbrLineBreakAfter: number = 0,
+ nbrLineBreakBefore: number = 0,
+ ): void {
+ if (this._terminalSupportUserInput) {
+ const messageToWrite =
+ "\n".repeat(nbrLineBreakBefore) +
+ message +
+ "\n".repeat(nbrLineBreakAfter);
+ this._rl.write(messageToWrite);
+ } else {
+ const messageToWrite =
+ "\n".repeat(nbrLineBreakBefore) +
+ message +
+ (nbrLineBreakAfter !== 0
+ ? "\n".repeat(nbrLineBreakAfter - 1)
+ : "");
+ console.log(messageToWrite);
+ }
+ }
+
+ /**
+ * Launches the program.
+ * @param {boolean} game Determines whether the game is launched or not. If not, the program will launch in a test mode.
+ * @returns {Promise} A promise that resolves when the program has been launched and closed.
+ */
+ public async launch(game: boolean): Promise {
+ if (game) {
+ process.stdout.write("\x1b]0;PuntoDB\x07");
+
+ if (process.stdin.isTTY) {
+ this._terminalSupportUserInput = true;
+
+ this.writeInConsole("Terminal support user input", 1, 1);
+
+ const chooseProgramMode = async () => {
+ this.writeInConsole(
+ "Type 'exit' to exit the program",
+ 2,
+ 1,
+ );
+
+ await this.sleep();
+
+ this.writeInConsole(
+ "Type 'game' to play a game (not saved by default)",
+ 2,
+ );
+
+ await this.sleep();
+
+ this.writeInConsole(
+ "Type 'db' to use database commands (for example, toggle saving in the database)",
+ 2,
+ );
+
+ await this.sleep();
+
+ const answer = await this.askQuestion(
+ "What do you want to do? ",
+ );
+
+ if (answer === false) {
+ return;
+ } else if (answer.toLowerCase() === "game") {
+ this.writeInConsole("Game mode", 2, 1);
+ await this.launchGameUserInput();
+ } else if (answer.toLowerCase() === "db") {
+ this.writeInConsole("DB mode", 2, 1);
+ await this.launchDBUserInput();
+ } else {
+ this.writeInConsole(
+ `Unknown command "${answer}"`,
+ 1,
+ 1,
+ );
+ }
+
+ await this.sleep();
+
+ await chooseProgramMode();
+ };
+
+ await chooseProgramMode();
+ } else {
+ this.writeInConsole(
+ "Terminal doesn't support user input",
+ 2,
+ 1,
+ );
+
+ await this.launchGameNoUserInput();
+ }
+ } else {
+ process.stdout.write("\x1b]0;Test\x07");
+
+ this.writeInConsole("Test mode", 1);
+
+ await this.testMode();
+ }
+
+ await this.dbWrapper.close();
+
+ this._rl.close();
+
+ return;
+ }
+
+ /**
+ * Launches the game with user input.
+ * @returns {Promise} A promise that resolves when the game has been launched and closed.
+ */
+ private async launchGameUserInput(): Promise {
+ const answer = await this.askQuestion(
+ "Would you like to play Punto? (Y / n / g[number]) ",
+ );
+
+ if (answer === false) {
+ return;
+ }
+
+ if (answer.toLowerCase().startsWith("g")) {
+ const numberOfGames = Number.parseInt(answer.slice(1));
+
+ if (Number.isNaN(numberOfGames)) {
+ this.writeInConsole("Please enter a valid number", 2, 1);
+
+ await this.launchGameUserInput();
+
+ return;
+ } else {
+ await this.generateGame(numberOfGames);
+
+ await this.launchGameUserInput();
+
+ return;
+ }
+ }
+
+ await this.playGame();
+
+ await this.launchGameUserInput();
+
+ return;
+ }
+
+ /**
+ * Asks a question to the user.
+ * @param {string} question The question to ask.
+ * @returns {Promise} The answer to the question. If the answer is false, the user refused.
+ */
+ private async askQuestion(question: string): Promise {
+ const refuseAnswer = [
+ "bye",
+
+ "exit",
+
+ "false",
+
+ "n",
+ "no",
+
+ "q",
+ "quit",
+
+ "refuse",
+
+ "stop",
+ ];
+
+ // this.writeInConsole(`Refuse answer: ${refuseAnswer.join(", ")}`, 1);
+
+ const answer = await this._rl.question(question);
+
+ if (refuseAnswer.includes(answer.toLowerCase())) {
+ return false;
+ }
+
+ return answer;
+ }
+
+ /**
+ * Plays the punto.
+ * @param punto The punto to play.
+ */
+ private async playPunto(punto: Punto): Promise {
+ await punto.playGame();
+ }
+
+ /**
+ * Resets the punto.
+ * @param punto The punto to reset.
+ */
+ private resetPunto(punto: Punto): void {
+ punto.reset();
+ }
+
+ /**
+ * Displays the game results.
+ * @param {Punto} punto The punto to display the results of.
+ * @returns {Promise} A promise that resolves when the results have been displayed.
+ */
+ private async displayGameResult(punto: Punto): Promise {
+ this.writeInConsole("Game results:", 1);
+
+ await this.sleep();
+
+ // let totalPoints = 0;
+ // let totalParties = 0;
+
+ punto.board.players.forEach((player) => {
+ this.writeInConsole(
+ `${player.name}: ${player.points} | nbrFirstPlayer : ${player.nbrFirstPlayer}`,
+ 1,
+ );
+
+ // totalPoints += player.points;
+ // totalParties += player.nbrFirstPlayer;
+ });
+
+ // this.writeInConsole(
+ // `Total games won: ${totalPoints} / ${totalParties}`,
+ // 2,
+ // 1,
+ // );
+
+ this.writeInConsole("", 1);
+ }
+
+ /**
+ * Generates and plays `numberOfGames` games.
+ * @param {number} numberOfGames The number of games to generate and play.
+ * @returns {Promise} A promise that resolves when the games have been generated and played.
+ */
+ private async generateGame(numberOfGames: number): Promise {
+ let nbrPlayers = 2;
+
+ const condToValidateNbrPlayers = (answerNbrPlayers: number) => {
+ // Number.isFinite(n) // true if n is a number, false if n is Infinity or NaN or not a number (string, boolean, object, etc.)
+ return (
+ Number.isNaN(answerNbrPlayers) ||
+ answerNbrPlayers < 2 ||
+ answerNbrPlayers > 4
+ );
+ };
+
+ do {
+ const answerNbrPlayers = await this.askQuestion(
+ "Number of players (blank for 2): ",
+ );
+
+ if (answerNbrPlayers === false) {
+ return;
+ } else if (answerNbrPlayers === "") {
+ nbrPlayers = 2;
+ } else {
+ nbrPlayers = Number.parseInt(answerNbrPlayers);
+
+ if (condToValidateNbrPlayers(nbrPlayers)) {
+ this.writeInConsole(
+ "Please enter a valid number (between 2 and 4)",
+ 2,
+ 1,
+ );
+ }
+ }
+ } while (condToValidateNbrPlayers(nbrPlayers));
+
+ const listPlayerOptions: PlayerOptions[] = [];
+
+ let noPlayerFirstTurn = true;
+
+ for (let i = 0; i < nbrPlayers; i++) {
+ const answerPlayerName = await this.askQuestion(
+ `Player ${i + 1} name (blank for "Player ${i + 1}"): `,
+ );
+
+ if (answerPlayerName === false) {
+ return;
+ }
+
+ const playerName =
+ answerPlayerName === "" ? `Player ${i + 1}` : answerPlayerName;
+
+ let firstTurn = false;
+
+ if (noPlayerFirstTurn) {
+ const answerPlayerFirstTurn = await this.askQuestion(
+ `Is ${playerName} the first player? (Y / n) `,
+ );
+
+ if (
+ answerPlayerFirstTurn !== false &&
+ (answerPlayerFirstTurn === "" ||
+ answerPlayerFirstTurn.toLowerCase() === "y")
+ ) {
+ firstTurn = true;
+ noPlayerFirstTurn = false;
+ }
+ }
+
+ const playerOptions: PlayerOptions = {
+ name: playerName,
+ isTurn: firstTurn,
+ };
+
+ listPlayerOptions.push(playerOptions);
+ }
+
+ const boardOptions: BoardOptions = {
+ nbrPlayers: nbrPlayers,
+ listPlayerOptions: listPlayerOptions,
+ };
+
+ const puntoOptions: PuntoOptions = {
+ displayBoard: DisplayBoardMode.noDisplay,
+ displayResultOfGame: false,
+ boardOption: boardOptions,
+ auto: true,
+ };
+
+ const createPunto = new CreatePunto(puntoOptions);
+
+ createPunto.createPunto();
+
+ const punto = createPunto.getPunto();
+
+ if (punto === null) {
+ console.error("Punto is null");
+ process.exit(1);
+ }
+
+ const startTime = Date.now();
+
+ this.writeInConsole(`Playing ${numberOfGames} games...`, 1, 1);
+
+ for (let i = 0; i < numberOfGames; i++) {
+ await this.playPunto(punto);
+
+ await this.savePunto(punto);
+
+ this.resetPunto(punto);
+
+ process.stdout.write(`\rGame ${i + 1}`);
+ }
+
+ const endTime = Date.now();
+
+ this.writeInConsole(
+ `Seconds elapsed: ${(endTime - startTime) / 1000}`,
+ 2,
+ 2,
+ );
+
+ // await this.displayGameResult(punto); // Not displaying because player points it's now reset
+ }
+
+ /**
+ * Plays the punto.
+ * @returns {Promise} A promise that resolves when the game has been played.
+ */
+ private async playGame(): Promise {
+ let nbrPlayers = 2;
+
+ const condToValidateNbrPlayers = (answerNbrPlayers: number) => {
+ // Number.isFinite(n) // true if n is a number, false if n is Infinity or NaN or not a number (string, boolean, object, etc.)
+ return (
+ Number.isNaN(answerNbrPlayers) ||
+ answerNbrPlayers < 2 ||
+ answerNbrPlayers > 4
+ );
+ };
+
+ do {
+ const answerNbrPlayers = await this.askQuestion(
+ "Number of players (blank for 2): ",
+ );
+
+ if (answerNbrPlayers === false) {
+ return;
+ } else if (answerNbrPlayers === "") {
+ nbrPlayers = 2;
+ } else {
+ nbrPlayers = Number.parseInt(answerNbrPlayers);
+
+ if (condToValidateNbrPlayers(nbrPlayers)) {
+ this.writeInConsole(
+ "Please enter a valid number (between 2 and 4)",
+ 2,
+ 1,
+ );
+ }
+ }
+ } while (condToValidateNbrPlayers(nbrPlayers));
+
+ /**
+ * Contains the options of the players.
+ * @type {PlayerOptions[]}
+ */
+ const listPlayerOptions: PlayerOptions[] = [];
+
+ /**
+ * Determines if a player has already been designated as the first player.
+ * @type {boolean}
+ */
+ let noPlayerFirstTurn: boolean = true;
+
+ /**
+ * Contains the names of the players already entered.
+ * @type {string[]}
+ */
+ const busyPlayerNames: string[] = [];
+
+ for (let i = 0; i < nbrPlayers; i++) {
+ const answerPlayerName = await this.askQuestion(
+ `Player ${i + 1} name (blank for "Player ${i + 1}"): `,
+ );
+
+ if (answerPlayerName === false) {
+ return;
+ }
+
+ const playerName: string =
+ answerPlayerName === "" ? `Player ${i + 1}` : answerPlayerName;
+
+ if (busyPlayerNames.includes(playerName)) {
+ this.writeInConsole(
+ `The name "${playerName}" is already used`,
+ 2,
+ 1,
+ );
+
+ i--; // To ask the same player name again
+
+ continue;
+ } else {
+ busyPlayerNames.push(playerName);
+ }
+
+ /**
+ * Determines if the player is the first player.
+ * @type {boolean}
+ */
+ let firstTurn: boolean = false;
+
+ if (noPlayerFirstTurn) {
+ const answerPlayerFirstTurn = await this.askQuestion(
+ `Is ${playerName} the first player? (Y / n) `,
+ );
+
+ if (
+ answerPlayerFirstTurn !== false &&
+ (answerPlayerFirstTurn === "" ||
+ answerPlayerFirstTurn.toLowerCase() === "y")
+ ) {
+ firstTurn = true;
+ noPlayerFirstTurn = false;
+ }
+ }
+
+ const playerOptions: PlayerOptions = {
+ name: playerName,
+ isTurn: firstTurn,
+ };
+
+ listPlayerOptions.push(playerOptions);
+ }
+
+ this.writeInConsole("Starting game...", 2, 1);
+
+ await this.sleep();
+
+ const boardOptions: BoardOptions = {
+ nbrPlayers: nbrPlayers,
+ listPlayerOptions: listPlayerOptions,
+ };
+
+ const puntoOptions: PuntoOptions = {
+ displayBoard: DisplayBoardMode.eachPlayerTurn,
+ displayResultOfGame: true,
+ boardOption: boardOptions,
+ auto: false,
+ };
+
+ const createPunto = new CreatePunto(puntoOptions);
+
+ createPunto.createPunto();
+
+ const punto = createPunto.getPunto();
+
+ if (punto === null) {
+ this.writeInConsole(
+ "The game hasn't been correctly initialized. Please try again.",
+ 2,
+ 1,
+ );
+ return;
+ }
+
+ if (punto.board.boardIsEmpty()) {
+ this.writeInConsole(
+ "The board is empty, the first card will be played automatically by the first player in (0, 0)",
+ 2,
+ );
+
+ await this.sleep();
+ }
+
+ await this.playPunto(punto);
+
+ await this.savePunto(punto);
+
+ this.resetPunto(punto);
+
+ await this.sleep();
+
+ await this.displayGameResult(punto);
+ }
+
+ /**
+ * Plays the punto without user input.
+ * @returns {Promise} A promise that resolves when the game has been played.
+ */
+ private async launchGameNoUserInput(): Promise {
+ this.writeInConsole("Auto play", 2);
+
+ await this.sleep();
+
+ const nbrPlayers = 2;
+
+ const player1: PlayerOptions = {
+ name: "Player 1",
+ isTurn: true,
+ };
+
+ const player2: PlayerOptions = {
+ name: "Player 2",
+ };
+
+ const player3: PlayerOptions = {
+ name: "Player 3",
+ };
+
+ const player4: PlayerOptions = {
+ name: "Player 4",
+ };
+
+ this.writeInConsole("Starting game...", 2);
+
+ await this.sleep();
+
+ const boardOptions: BoardOptions = {
+ nbrPlayers: nbrPlayers,
+ listPlayerOptions: [player1, player2, player3, player4],
+ };
+
+ const puntoOptions: PuntoOptions = {
+ displayBoard: DisplayBoardMode.onlyEnd,
+ displayResultOfGame: true,
+ boardOption: boardOptions,
+ auto: true,
+ };
+
+ const createPunto = new CreatePunto(puntoOptions);
+
+ createPunto.createPunto();
+
+ const punto = createPunto.getPunto();
+
+ if (punto === null) {
+ console.error("Punto is null");
+ process.exit(1);
+ }
+
+ await this.playPunto(punto);
+
+ await this.savePunto(punto);
+
+ this.resetPunto(punto);
+
+ await this.sleep();
+
+ await this.displayGameResult(punto);
+ }
+
+ /**
+ * Saves the punto in the database(s).
+ * @param {Punto} punto The punto to save.
+ * @returns {Promise} A promise that resolves when the punto has been saved.
+ */
+ private async savePunto(punto: Punto): Promise {
+ // Sauvegarder les joueurs
+
+ const players = punto.board.players;
+
+ for (let i = 0; i < players.length; i++) {
+ const player = players[i];
+ const findResults = await UserManager.find(player.name);
+
+ const isArrayWithData = (data: unknown): boolean => {
+ return Array.isArray(data) && data.length > 0;
+ };
+
+ // Vérifier si l'utilisateur existe dans chaque base de données
+ const userExistsInMySQL = isArrayWithData(
+ findResults.mySqlRepo?.data,
+ );
+ const userExistsInSQLite = isArrayWithData(
+ findResults.sqliteRepo?.data,
+ );
+ const userExistsInMongo = isArrayWithData(
+ findResults.mongoRepo?.data,
+ );
+
+ // Si l'utilisateur n'existe dans aucune base de données ou s'il n'existe pas dans toutes les bases de données
+ if (
+ !userExistsInMySQL ||
+ !userExistsInSQLite ||
+ !userExistsInMongo
+ ) {
+ // Construire ou reconstruire l'utilisateur
+ const user =
+ userExistsInMySQL || userExistsInSQLite || userExistsInMongo
+ ? await UserManager.build(player.name)
+ : new UserManager(this.dbWrapper, player.name);
+
+ if (
+ userExistsInMySQL ||
+ userExistsInSQLite ||
+ userExistsInMongo
+ ) {
+ await user.rebuild();
+ }
+
+ const saveResultsUsers = await user.save();
+
+ const mySqlUserStatus = saveResultsUsers.mySqlRepo?.status;
+ const sqliteUserStatus = saveResultsUsers.sqliteRepo?.status;
+ const mongoUserStatus = saveResultsUsers.mongoRepo?.status;
+
+ // Backup error handling
+ if (
+ mySqlUserStatus !== undefined &&
+ mySqlUserStatus !== ResultStatus.Success
+ ) {
+ console.error(saveResultsUsers.mySqlRepo?.error);
+ }
+ if (
+ sqliteUserStatus !== undefined &&
+ sqliteUserStatus !== ResultStatus.Success
+ ) {
+ console.error(saveResultsUsers.sqliteRepo?.error);
+ }
+ if (
+ mongoUserStatus !== undefined &&
+ mongoUserStatus !== ResultStatus.Success
+ ) {
+ console.error(saveResultsUsers.mongoRepo?.error);
+ }
+ } else {
+ // L'utilisateur existe dans toutes les bases de données, aucune action n'est requise
+ }
+ }
+
+ // Sauvegarder le jeu
+
+ const board = punto.board;
+
+ const game = new GameManager(this.dbWrapper);
+
+ await game.buildEntities(board);
+
+ const saveResultsGame = await game.save();
+
+ // Save for Neo4j
+ if (
+ this.dbWrapper.Neo4jConnection &&
+ this.dbWrapper.dbToUse.includes(DBList.Neo4j)
+ ) {
+ const neo4jManager = Neo4jManager.getInstance(
+ this.dbWrapper.Neo4jConnection,
+ );
+
+ await neo4jManager.createIfNotExist(punto.board);
+ }
+
+ const mySqlGameStatus = saveResultsGame.mySqlRepo?.status;
+ const sqliteGameStatus = saveResultsGame.sqliteRepo?.status;
+ const mongoGameStatus = saveResultsGame.mongoRepo?.status;
+
+ // Backup error handling
+ if (
+ mySqlGameStatus !== undefined &&
+ mySqlGameStatus !== ResultStatus.Success
+ ) {
+ console.error(saveResultsGame.mySqlRepo?.error);
+ }
+ if (
+ sqliteGameStatus !== undefined &&
+ sqliteGameStatus !== ResultStatus.Success
+ ) {
+ console.error(saveResultsGame.sqliteRepo?.error);
+ }
+ if (
+ mongoGameStatus !== undefined &&
+ mongoGameStatus !== ResultStatus.Success
+ ) {
+ console.error(saveResultsGame.mongoRepo?.error);
+ }
+ }
+
+ /**
+ * Launches the database mode with user input.
+ * @returns {Promise} A promise that resolves when the database mode has been launched and closed.
+ */
+ private async launchDBUserInput(): Promise {
+ // const answer = await this.askQuestion(
+ // "Use databases commands? (Y / n) ",
+ // );
+
+ // if (answer === false) {
+ // this.writeInConsole("Database mode canceled", 2, 1);
+ // return;
+ // }
+
+ let useMySQL: boolean = false;
+ let useSQLite: boolean = false;
+ let useMongo: boolean = false;
+ let useNeo4j: boolean = false;
+
+ const dbUsed = this.dbWrapper.dbToUse;
+
+ if (dbUsed === "All") {
+ useMySQL = true;
+ useSQLite = true;
+ useMongo = true;
+ useNeo4j = true;
+ } else {
+ if (dbUsed.includes(DBList.MySql)) {
+ useMySQL = true;
+ }
+ if (dbUsed.includes(DBList.SQLite)) {
+ useSQLite = true;
+ }
+ if (dbUsed.includes(DBList.Mongo)) {
+ useMongo = true;
+ }
+ if (dbUsed.includes(DBList.Neo4j)) {
+ useNeo4j = true;
+ }
+ }
+
+ this.writeInConsole("Type 'exit' to exit the database mode", 2);
+
+ await this.sleep();
+
+ this.writeInConsole(
+ "Activated databases will be used for game saving." +
+ "\nDeactivated databases will not be used for game saving.",
+ 1,
+ );
+
+ const writeDBUsed = () => {
+ if (useMySQL && useSQLite && useMongo && useNeo4j) {
+ this.writeInConsole("MySQL", 1);
+ this.writeInConsole("SQLite", 1);
+ this.writeInConsole("MongoDB", 1);
+ this.writeInConsole("Neo4j", 1);
+ } else if (!useMySQL && !useSQLite && !useMongo && !useNeo4j) {
+ this.writeInConsole("None", 1);
+ } else {
+ if (useMySQL) {
+ this.writeInConsole("MySQL", 1);
+ }
+ if (useSQLite) {
+ this.writeInConsole("SQLite", 1);
+ }
+ if (useMongo) {
+ this.writeInConsole("MongoDB", 1);
+ }
+ if (useNeo4j) {
+ this.writeInConsole("Neo4j", 1);
+ }
+ }
+ };
+
+ this.writeInConsole("Currently used databases:", 2, 1);
+
+ writeDBUsed();
+
+ await this.sleep();
+
+ this.writeInConsole("Type 'mysql' to toggle MySQL (ON/OFF)", 1, 1);
+
+ await this.sleep();
+
+ this.writeInConsole("Type 'sqlite' to toggle SQLite (ON/OFF)", 1);
+
+ await this.sleep();
+
+ this.writeInConsole("Type 'mongo' to toggle MongoDB (ON/OFF)", 1);
+
+ await this.sleep();
+
+ this.writeInConsole("Type 'neo4j' to toggle Neo4j (ON/OFF)", 1);
+
+ await this.sleep();
+
+ this.writeInConsole(
+ "You can activate multiple databases. Enter the name of each one you wish to toggle.",
+ 1,
+ 1,
+ );
+
+ await this.sleep();
+
+ this.writeInConsole("Type 'all' to toggle all databases (ON/OFF)", 1);
+
+ await this.sleep();
+
+ this.writeInConsole("Type 'empty' to empty the active databases", 1, 1);
+
+ await this.sleep();
+
+ this.writeInConsole("Type 'transfer' to transfer data", 2, 1);
+
+ await this.sleep();
+
+ const chooseDB = async () => {
+ const answer = await this.askQuestion("What do you want to do? ");
+
+ if (answer === false) {
+ return;
+ } else if (answer.toLowerCase() === "mysql") {
+ if (useMySQL) {
+ await this.dbWrapper.close([DBList.MySql]);
+ this.writeInConsole("MySQL has been deactivated.", 2);
+ useMySQL = false;
+ } else {
+ await this.dbWrapper.init([DBList.MySql]);
+ this.writeInConsole("MySQL has been activated.", 2);
+ useMySQL = true;
+ }
+ } else if (answer.toLowerCase() === "sqlite") {
+ if (useSQLite) {
+ await this.dbWrapper.close([DBList.SQLite]);
+ this.writeInConsole("SQLite has been deactivated.", 2);
+ useSQLite = false;
+ } else {
+ await this.dbWrapper.init([DBList.SQLite]);
+ this.writeInConsole("SQLite has been activated.", 2);
+ useSQLite = true;
+ }
+ } else if (answer.toLowerCase() === "mongo") {
+ if (useMongo) {
+ await this.dbWrapper.close([DBList.Mongo]);
+ this.writeInConsole("MongoDB has been deactivated.", 2);
+ useMongo = false;
+ } else {
+ await this.dbWrapper.init([DBList.Mongo]);
+ this.writeInConsole("MongoDB has been activated.", 2);
+ useMongo = true;
+ }
+ } else if (answer.toLowerCase() === "neo4j") {
+ if (useNeo4j) {
+ await this.dbWrapper.close([DBList.Neo4j]);
+ this.writeInConsole("Neo4j has been deactivated.", 2);
+ useNeo4j = false;
+ } else {
+ await this.dbWrapper.init([DBList.Neo4j]);
+ this.writeInConsole("Neo4j has been activated.", 2);
+ useNeo4j = true;
+ }
+ } else if (answer.toLowerCase() === "all") {
+ if (useMySQL || useSQLite || useMongo || useNeo4j) {
+ await this.dbWrapper.close();
+ this.writeInConsole(
+ "All databases have been deactivated.",
+ 2,
+ );
+ useMySQL = false;
+ useSQLite = false;
+ useMongo = false;
+ useNeo4j = false;
+ } else {
+ await this.dbWrapper.init();
+ this.writeInConsole(
+ "All databases have been activated.",
+ 2,
+ );
+ useMySQL = true;
+ useSQLite = true;
+ useMongo = true;
+ useNeo4j = true;
+ }
+ } else if (answer.toLowerCase() === "empty") {
+ const gameResult = await GameManager.removeAll();
+ const userResult = await UserManager.removeAll();
+
+ if (this.dbWrapper.Neo4jConnection) {
+ const neo4jManager = Neo4jManager.getInstance(
+ this.dbWrapper.Neo4jConnection,
+ );
+
+ neo4jManager.deleteAll();
+
+ this.writeInConsole("Neo4j has been emptied.", 1);
+ }
+
+ const mySqlGameResultStatus = gameResult.mySqlRepo?.status;
+ const sqliteGameResultStatus = gameResult.sqliteRepo?.status;
+ const mongoGameResultStatus = gameResult.mongoRepo?.status;
+
+ const mySqlUserResultStatus = userResult.mySqlRepo?.status;
+ const sqliteUserResultStatus = userResult.sqliteRepo?.status;
+ const mongoUserResultStatus = userResult.mongoRepo?.status;
+
+ // Backup error handling
+ if (
+ mySqlGameResultStatus !== undefined &&
+ mySqlGameResultStatus !== ResultStatus.Success &&
+ mySqlUserResultStatus !== undefined &&
+ mySqlUserResultStatus !== ResultStatus.Success
+ ) {
+ console.error(gameResult.mySqlRepo?.error);
+ console.error(userResult.mySqlRepo?.error);
+ } else {
+ if (useMySQL) {
+ this.writeInConsole("MySQL has been emptied.", 1);
+ }
+ }
+
+ if (
+ sqliteGameResultStatus !== undefined &&
+ sqliteGameResultStatus !== ResultStatus.Success &&
+ sqliteUserResultStatus !== undefined &&
+ sqliteUserResultStatus !== ResultStatus.Success
+ ) {
+ console.error(gameResult.sqliteRepo?.error);
+ console.error(userResult.sqliteRepo?.error);
+ } else {
+ if (useSQLite) {
+ this.writeInConsole("SQLite has been emptied.", 1);
+ }
+ }
+
+ if (
+ mongoGameResultStatus !== undefined &&
+ mongoGameResultStatus !== ResultStatus.Success &&
+ mongoUserResultStatus !== undefined &&
+ mongoUserResultStatus !== ResultStatus.Success
+ ) {
+ // * Silently ignore error code 26 (NamespaceNotFound)
+ // * because it means that the collection is already empty
+ if (
+ (gameResult.mongoRepo?.error as {code: number}).code !==
+ 26 &&
+ (userResult.mongoRepo?.error as {code: number}).code !==
+ 26
+ ) {
+ console.error(gameResult.mongoRepo?.error);
+ console.error(userResult.mongoRepo?.error);
+ }
+ } else {
+ if (useMongo) {
+ this.writeInConsole("MongoDB has been emptied.", 1);
+ }
+ }
+
+ this.writeInConsole("", 1);
+
+ await this.sleep();
+ } else if (answer.toLowerCase() === "transfer") {
+ this.writeInConsole(
+ "Type the name of the source database",
+ 1,
+ 1,
+ );
+
+ await this.sleep();
+
+ let sourceDB: DBList | undefined;
+
+ let firstAskSourceDB = true;
+
+ do {
+ if (!firstAskSourceDB) {
+ this.writeInConsole(
+ "Please enter a valid database name",
+ 2,
+ 1,
+ );
+ }
+
+ const answerSourceDB = await this.askQuestion(
+ "Source database: ",
+ );
+
+ if (answerSourceDB === false) {
+ return;
+ }
+
+ switch (answerSourceDB.toLowerCase()) {
+ case "mysql":
+ sourceDB = DBList.MySql;
+ break;
+ case "sqlite":
+ sourceDB = DBList.SQLite;
+ break;
+ case "mongo":
+ sourceDB = DBList.Mongo;
+ break;
+ case "neo4j":
+ sourceDB = DBList.Neo4j;
+ break;
+ default:
+ break;
+ }
+
+ if (firstAskSourceDB) {
+ firstAskSourceDB = false;
+ }
+ } while (
+ sourceDB !== DBList.MySql &&
+ sourceDB !== DBList.SQLite &&
+ sourceDB !== DBList.Mongo &&
+ sourceDB !== DBList.Neo4j
+ );
+
+ this.writeInConsole(
+ "Type the name of the destination database",
+ 1,
+ 1,
+ );
+
+ await this.sleep();
+
+ let destinationDB: DBList | undefined;
+
+ let firstAskDestinationDB = true;
+
+ do {
+ if (!firstAskDestinationDB) {
+ this.writeInConsole(
+ "Please enter a valid database name (different from the source database)",
+ 2,
+ 1,
+ );
+ }
+
+ const answerDestinationDB = await this.askQuestion(
+ "Destination database: ",
+ );
+
+ if (answerDestinationDB === false) {
+ return;
+ }
+
+ switch (answerDestinationDB.toLowerCase()) {
+ case "mysql":
+ destinationDB = DBList.MySql;
+ break;
+ case "sqlite":
+ destinationDB = DBList.SQLite;
+ break;
+ case "mongo":
+ destinationDB = DBList.Mongo;
+ break;
+ case "neo4j":
+ destinationDB = DBList.Neo4j;
+ break;
+ default:
+ break;
+ }
+
+ if (firstAskDestinationDB) {
+ firstAskDestinationDB = false;
+ }
+ } while (
+ destinationDB !== DBList.MySql &&
+ destinationDB !== DBList.SQLite &&
+ destinationDB !== DBList.Mongo &&
+ destinationDB !== DBList.Neo4j &&
+ destinationDB !== sourceDB
+ );
+
+ const transferResult = await this.dbWrapper.transfer(
+ sourceDB,
+ destinationDB,
+ );
+
+ if (transferResult.status === ResultStatus.Success) {
+ this.writeInConsole(
+ `${sourceDB} has been transfered to ${destinationDB}`,
+ 1,
+ );
+ } else {
+ this.writeInConsole(`Error: ${transferResult.error}`, 1);
+ }
+
+ await this.sleep();
+
+ this.writeInConsole("", 1);
+ } else {
+ this.writeInConsole(`Unknown command "${answer}"`, 1, 1);
+ }
+
+ await this.sleep();
+
+ await chooseDB();
+ };
+
+ await chooseDB();
+
+ this.writeInConsole("Used databases:", 2, 1);
+
+ writeDBUsed();
+
+ await this.sleep();
+
+ this.writeInConsole("Database mode ended", 1, 1);
+ }
+
+ /**
+ * Launches the test mode.
+ * @returns {Promise} A promise that resolves when the test mode has been launched and closed.
+ */
+ private async testMode(): Promise {
+ console.log("No test to do");
+ }
+}
+
+/**
+ * Represents the web interface.
+ * Currently not implemented.
+ */
+class WebState implements IInterfaceState {
+ /**
+ * Instance of DBWrapper.
+ * This is a public and readonly member of WebState.
+ * For more information, refer to DBWrapper documentation.
+ * @public
+ * @readonly
+ * @type {DBWrapper}
+ * @memberof WebState
+ * @see DBWrapper
+ */
+ public readonly dbWrapper: DBWrapper = DBWrapper.getInstance();
+
+ /**
+ * Gets the type of interface.
+ * @param {boolean} isString Determines whether to return the interface type as a string or as an enum. Default: false.
+ * @returns {InterfaceType | string} The type of interface.
+ */
+ public getInterfaceType(isString: boolean): InterfaceType | string {
+ if (isString) {
+ return InterfaceType[InterfaceType.Web];
+ } else {
+ return InterfaceType.Web;
+ }
+ }
+
+ /**
+ * Gets the coordinates of the card to play.
+ * @param {unknown} options Options for the coordinates.
+ * @returns {Promise} The coordinates of the card to play. If the player refused to play, returns false.
+ */
+ public async getCoordinates(
+ options: unknown = {},
+ ): Promise {
+ const {firstTimeToDemandeCoordinates} = options as {
+ firstTimeToDemandeCoordinates: boolean | undefined;
+ };
+
+ console.log(firstTimeToDemandeCoordinates);
+
+ return Promise.reject("Not implemented");
+ }
+
+ /**
+ * Launches the program.
+ * @param {boolean} game Determines whether the game is launched or not. If not, the program will launch in a test mode.
+ * @returns {Promise} A promise that resolves when the program has been launched and closed.
+ */
+ public async launch(game: boolean): Promise {
+ return Promise.reject(`game = ${game}, Not implemented`);
+ }
+}
+
+/**
+ * Represents state when no interface is available.
+ * Using this state will throw an error or reject a promise.
+ */
+class NoneState implements IInterfaceState {
+ /**
+ * Instance of DBWrapper.
+ * This is a public and readonly member of NoneState.
+ * For more information, refer to DBWrapper documentation.
+ * @public
+ * @readonly
+ * @type {DBWrapper}
+ * @memberof NoneState
+ * @see DBWrapper
+ */
+ public readonly dbWrapper: DBWrapper = DBWrapper.getInstance();
+
+ /**
+ * Gets the type of interface.
+ * @param {boolean} isString Determines whether to return the interface type as a string or as an enum. Default: false.
+ * @returns {InterfaceType | string} The type of interface.
+ */
+ public getInterfaceType(isString: boolean): InterfaceType | string {
+ if (isString) {
+ return InterfaceType[InterfaceType.None];
+ } else {
+ return InterfaceType.None;
+ }
+ }
+
+ /**
+ * Rejects the promise to get the coordinates.
+ * @param {unknown} options Parameters for respect the interface.
+ * @returns {Promise} A promise that rejects with an error.
+ * @memberof NoneState
+ * @see Interface.getCoordinates
+ */
+ public async getCoordinates(
+ options: unknown = {},
+ ): Promise {
+ const {firstTimeToDemandeCoordinates} = options as {
+ firstTimeToDemandeCoordinates: boolean | undefined;
+ };
+
+ console.log(firstTimeToDemandeCoordinates);
+
+ return Promise.reject("No interface available");
+ }
+
+ /**
+ * Rejects the promise to launch the program.
+ * @param {boolean} game Parameters for respect the interface.
+ * @returns {Promise} A promise that rejects with an error.
+ * @memberof NoneState
+ * @see Interface.launch
+ */
+ public async launch(game: boolean): Promise {
+ return Promise.reject(`game = ${game}, No interface available`);
+ }
+}
+
+export default Interface;
+
+export {Interface, InterfaceType, IInterfaceState};
diff --git a/src/interface/ReadlineSingleton.ts b/src/interface/ReadlineSingleton.ts
new file mode 100644
index 0000000..0a9aae0
--- /dev/null
+++ b/src/interface/ReadlineSingleton.ts
@@ -0,0 +1,33 @@
+import * as readlinePromises from "node:readline/promises";
+
+class ReadlineSingleton {
+ private static instance: readlinePromises.Interface;
+
+ private constructor() {}
+
+ public static getReadlineInterface(): readlinePromises.Interface {
+ if (!ReadlineSingleton.instance) {
+ ReadlineSingleton.instance = readlinePromises.createInterface({
+ input: process.stdin,
+ output: process.stdout,
+ terminal: true,
+ prompt: ">> ",
+ });
+
+ ReadlineSingleton.instance.on("close", () => {
+ ReadlineSingleton.instance.write("\nBye bye !\n\n");
+
+ process.stdin.unref();
+ process.exit(0);
+ });
+
+ ReadlineSingleton.instance.on("SIGINT", () => {
+ ReadlineSingleton.instance.close();
+ });
+ }
+
+ return ReadlineSingleton.instance;
+ }
+}
+
+export default ReadlineSingleton;
diff --git a/src/main.ts b/src/main.ts
new file mode 100644
index 0000000..bc3d57f
--- /dev/null
+++ b/src/main.ts
@@ -0,0 +1,11 @@
+import Interface from "./interface/Interface";
+
+const mainGame = true;
+
+console.clear();
+
+(async () => {
+ const gameInterface = new Interface();
+
+ await gameInterface.launch(mainGame);
+})();
diff --git a/src/tests/DB.test.ts b/src/tests/DB.test.ts
new file mode 100644
index 0000000..f21e765
--- /dev/null
+++ b/src/tests/DB.test.ts
@@ -0,0 +1,281 @@
+import {testAssert} from "./Tests.test";
+import DBWrapper, {DBList} from "../db/DBWrapper";
+import User from "../entities/User";
+import {ResultStatus} from "../db/Result";
+
+export async function testAllDB(n: number = 1) {
+ await testDBMongoConnection(n);
+ await testDBMySqlConnection(n);
+ await testDBSQLiteConnection(n);
+ await testDBMongoUser(n);
+ await testDBMySqlUser(n);
+ await testDBSqliteUser(n);
+ await testDBUsers(n);
+}
+
+async function testDBUsers(n: number = 1) {
+ for (let nbrTest = 0; nbrTest < n; nbrTest++) {
+ const dbWrapper = DBWrapper.getInstance();
+
+ await dbWrapper.init();
+
+ const user = new User(dbWrapper, "John Doe");
+
+ const saveResults1 = await user.save();
+
+ const nameMySql = (saveResults1.mySqlRepo?.data as {name: string}).name;
+ const nameSqlite = (saveResults1.sqliteRepo?.data as {name: string})
+ .name;
+ const nameMongo = (saveResults1.mongoRepo?.data as {name: string}).name;
+
+ testAssert(
+ "testDBUsersSave",
+ saveResults1.mySqlRepo?.status === ResultStatus.Success &&
+ nameMySql === "John Doe" &&
+ saveResults1.sqliteRepo?.status === ResultStatus.Success &&
+ nameSqlite === "John Doe" &&
+ saveResults1.mongoRepo?.status === ResultStatus.Success &&
+ nameMongo === "John Doe",
+ "User not saved",
+ );
+
+ user.name = "John Doe updated";
+
+ const saveResults2 = await user.save();
+
+ const nameMySqlUpdated = (
+ saveResults2.mySqlRepo?.data as {name: string}
+ ).name;
+ const nameSqliteUpdated = (
+ saveResults2.sqliteRepo?.data as {name: string}
+ ).name;
+ const nameMongoUpdated = (
+ saveResults2.mongoRepo?.data as {name: string}
+ ).name;
+
+ testAssert(
+ "testDBUsersUpdate",
+ saveResults2.mySqlRepo?.status === ResultStatus.Success &&
+ nameMySqlUpdated === "John Doe updated" &&
+ saveResults2.sqliteRepo?.status === ResultStatus.Success &&
+ nameSqliteUpdated === "John Doe updated" &&
+ saveResults2.mongoRepo?.status === ResultStatus.Success &&
+ nameMongoUpdated === "John Doe updated",
+ "User not saved",
+ );
+
+ const removeResults = await user.remove();
+
+ testAssert(
+ "testDBUsersDelete",
+ removeResults.mySqlRepo?.status === ResultStatus.Success &&
+ removeResults.sqliteRepo?.status === ResultStatus.Success &&
+ removeResults.mongoRepo?.status === ResultStatus.Success,
+ "User not deleted",
+ );
+
+ await dbWrapper.close();
+ }
+}
+
+async function testDBSqliteUser(n: number = 1) {
+ for (let nbrTest = 0; nbrTest < n; nbrTest++) {
+ const dbWrapper = DBWrapper.getInstance();
+
+ await dbWrapper.init([DBList.SQLite]);
+
+ const user = new User(dbWrapper, "John Doe");
+
+ const saveResults1 = await user.save();
+
+ const nameSqlite = (saveResults1.sqliteRepo?.data as {name: string})
+ .name;
+
+ testAssert(
+ "testDBUsersSave",
+ saveResults1.sqliteRepo?.status === ResultStatus.Success &&
+ nameSqlite === "John Doe",
+ "User not saved",
+ );
+
+ user.name = "John Doe updated";
+
+ const saveResults2 = await user.save();
+
+ const nameSqliteUpdated = (
+ saveResults2.sqliteRepo?.data as {name: string}
+ ).name;
+
+ testAssert(
+ "testDBUsersUpdate",
+ saveResults2.sqliteRepo?.status === ResultStatus.Success &&
+ nameSqliteUpdated === "John Doe updated",
+ "User not saved",
+ );
+
+ const removeResults = await user.remove();
+
+ testAssert(
+ "testDBUsersDelete",
+ removeResults.sqliteRepo?.status === ResultStatus.Success,
+ "User not deleted",
+ );
+
+ await dbWrapper.close();
+ }
+}
+
+async function testDBMySqlUser(n: number = 1) {
+ for (let nbrTest = 0; nbrTest < n; nbrTest++) {
+ const dbWrapper = DBWrapper.getInstance();
+
+ await dbWrapper.init([DBList.MySql]);
+
+ const user = new User(dbWrapper, "John Doe");
+
+ const saveResults1 = await user.save();
+
+ const nameMySql = (saveResults1.mySqlRepo?.data as {name: string}).name;
+
+ testAssert(
+ "testDBUsersSave",
+ saveResults1.mySqlRepo?.status === ResultStatus.Success &&
+ nameMySql === "John Doe",
+ "User not saved",
+ );
+
+ user.name = "John Doe updated";
+
+ const saveResults2 = await user.save();
+
+ const nameMySqlUpdated = (
+ saveResults2.mySqlRepo?.data as {name: string}
+ ).name;
+
+ testAssert(
+ "testDBUsersUpdate",
+ saveResults2.mySqlRepo?.status === ResultStatus.Success &&
+ nameMySqlUpdated === "John Doe updated",
+ "User not saved",
+ );
+
+ const removeResults = await user.remove();
+
+ testAssert(
+ "testDBUsersDelete",
+ removeResults.mySqlRepo?.status === ResultStatus.Success,
+ "User not deleted",
+ );
+
+ await dbWrapper.close();
+ }
+}
+
+async function testDBMongoUser(n: number = 1) {
+ for (let nbrTest = 0; nbrTest < n; nbrTest++) {
+ const dbWrapper = DBWrapper.getInstance();
+
+ await dbWrapper.init([DBList.Mongo]);
+
+ const user = new User(dbWrapper, "John Doe");
+
+ const saveResults1 = await user.save();
+
+ const nameMongo = (saveResults1.mongoRepo?.data as {name: string}).name;
+
+ testAssert(
+ "testDBUsersSave",
+ saveResults1.mongoRepo?.status === ResultStatus.Success &&
+ nameMongo === "John Doe",
+ "User not saved",
+ );
+
+ user.name = "John Doe updated";
+
+ const saveResults2 = await user.save();
+
+ const nameMongoUpdated = (
+ saveResults2.mongoRepo?.data as {name: string}
+ ).name;
+
+ testAssert(
+ "testDBUsersUpdate",
+ saveResults2.mongoRepo?.status === ResultStatus.Success &&
+ nameMongoUpdated === "John Doe updated",
+ "User not saved",
+ );
+
+ const removeResults = await user.remove();
+
+ testAssert(
+ "testDBUsersDelete",
+ removeResults.mongoRepo?.status === ResultStatus.Success,
+ "User not deleted",
+ );
+
+ await dbWrapper.close();
+ }
+}
+
+async function testDBSQLiteConnection(n: number = 1) {
+ n = 1;
+ for (let nbrTest = 0; nbrTest < n; nbrTest++) {
+ const dbWrapper = DBWrapper.getInstance();
+
+ testAssert(
+ "testInitDBSQLite",
+ await dbWrapper.init([DBList.SQLite]),
+ "Initialization of DB failed",
+ );
+
+ testAssert(
+ "testConnectionDBSQLite",
+ dbWrapper.SqliteConnection?.isInitialized ? true : false,
+ "Connection to DB is not initialized",
+ );
+
+ await dbWrapper.close();
+ }
+}
+
+async function testDBMySqlConnection(n: number = 1) {
+ n = 1;
+ for (let nbrTest = 0; nbrTest < n; nbrTest++) {
+ const dbWrapper = DBWrapper.getInstance();
+
+ testAssert(
+ "testInitDBMySql",
+ await dbWrapper.init([DBList.MySql]),
+ "Initialization of DB failed",
+ );
+
+ testAssert(
+ "testConnectionDBMySql",
+ dbWrapper.MySqlConnection?.isInitialized ? true : false,
+ "Connection to DB is not initialized",
+ );
+
+ await dbWrapper.close();
+ }
+}
+
+async function testDBMongoConnection(n: number = 1) {
+ n = 1;
+ for (let nbrTest = 0; nbrTest < n; nbrTest++) {
+ const dbWrapper = DBWrapper.getInstance();
+
+ testAssert(
+ "testInitDBMongo",
+ await dbWrapper.init([DBList.Mongo]),
+ "Initialization of DB failed",
+ );
+
+ testAssert(
+ "testConnectionDBMongo",
+ dbWrapper.MongoConnection?.isInitialized ? true : false,
+ "Connection to DB is not initialized",
+ );
+
+ await dbWrapper.close();
+ }
+}
diff --git a/src/tests/Game.test.ts b/src/tests/Game.test.ts
new file mode 100644
index 0000000..669fe8d
--- /dev/null
+++ b/src/tests/Game.test.ts
@@ -0,0 +1,730 @@
+import {testAssert} from "./Tests.test";
+import {BoardOptions} from "../game/Board";
+import CreatePunto from "../game/CreatePunto";
+import {PuntoOptions} from "../game/Punto";
+
+export function testAllPunto(n: number = 1) {
+ testPuntoPlayerName(n);
+ testPuntoCardsCorrect2Players(n);
+ testPuntoCardsCorrect4Players(n);
+ testPuntoCardsCorrect3Players(n);
+ testPuntoImpossiblePlayerNumber(n);
+ testPuntoNumberOfTotalCards(n);
+ testPuntoPlaceCards(n);
+}
+
+function testPuntoPlaceCards(n: number = 1) {
+ for (let nbrTest = 0; nbrTest < n; nbrTest++) {
+ const boardOptions: BoardOptions = {
+ nbrPlayers: 2,
+ listPlayerOptions: [
+ {
+ name: "Player 1",
+ points: 0,
+ isTurn: true,
+ },
+ {
+ name: "Player 2",
+ points: 0,
+ isTurn: false,
+ },
+ ],
+ };
+
+ const puntoOptions: PuntoOptions = {
+ boardOption: boardOptions,
+ };
+
+ const createPunto = new CreatePunto(puntoOptions);
+
+ createPunto.createPunto();
+
+ const punto = createPunto.getPunto();
+
+ const board = punto?.board;
+
+ if (board === undefined) {
+ throw new Error("Board is undefined");
+ }
+
+ const player = board.getPlayerTurn();
+
+ if (player === undefined) {
+ throw new Error("Player is undefined");
+ }
+
+ const playerDeck = player.deck;
+
+ const card1 = playerDeck.find((card) => card.value < 9);
+
+ if (card1 === undefined) {
+ throw new Error("Card1 is undefined");
+ }
+
+ player.hand = card1;
+
+ const playResult1 = board.playCard(player, 0, card1, 0, 0);
+
+ testAssert(
+ "playResult1",
+ playResult1 === "The card was successfully placed.",
+ "The play result should be 'The card was successfully placed.', but it is '" +
+ playResult1 +
+ "'.",
+ );
+
+ const playResult2 = board.playCard(player, 0, card1, 0, 0);
+
+ testAssert(
+ "playResult2",
+ playResult2 === "The player does not have the card in hand.",
+ "The play result should be 'The player does not have the card in hand.', but it is '" +
+ playResult2 +
+ "'.",
+ );
+
+ const card2 = playerDeck.find((card) => card.value <= card1.value);
+
+ if (card2 === undefined) {
+ throw new Error("Card2 is undefined");
+ }
+
+ player.hand = card2;
+
+ const playResult3 = board.playCard(player, 0, card2, 0, 0);
+
+ testAssert(
+ "playResult3",
+ playResult3 === "The card could not be placed.",
+ "The play result should be 'The card could not be placed.', but it is '" +
+ playResult3 +
+ "'.",
+ );
+
+ testAssert(
+ "board.cards.length === 1",
+ board.cards.length === 1,
+ "The board should have 1 cards, but it has " +
+ board.cards.length +
+ " cards.",
+ );
+
+ testAssert(
+ "board.cards[0].x === 0 && board.cards[0].y === 0",
+ board.cards[0].x === 0 && board.cards[0].y === 0,
+ "The card should be at the coordinates (0, 0), but it is at the coordinates (" +
+ board.cards[0].x +
+ ", " +
+ board.cards[0].y +
+ ").",
+ );
+
+ const card3 = playerDeck.find((card) => card.value > card1.value);
+
+ if (card3 === undefined) {
+ throw new Error("Card3 is undefined");
+ }
+
+ player.hand = card3;
+
+ const playResult4 = board.playCard(player, 1, card3, 0, 0);
+
+ testAssert(
+ "playResult4",
+ playResult4 === "The card was successfully placed.",
+ "The play result should be 'The card was successfully placed.', but it is '" +
+ playResult4 +
+ "'.",
+ );
+
+ testAssert(
+ "board.cards.length === 2",
+ board.cards.length === 2,
+ "The board should have 2 cards, but it has " +
+ board.cards.length +
+ " cards.",
+ );
+
+ testAssert(
+ "board.cards[1].x === 0 && board.cards[1].y === 0",
+ board.cards[1].x === 0 && board.cards[1].y === 0,
+ "The card should be at the coordinates (0, 0), but it is at the coordinates (" +
+ board.cards[1].x +
+ ", " +
+ board.cards[1].y +
+ ").",
+ );
+
+ const drawnCardResult4 = player.drawCard();
+
+ if (drawnCardResult4 === false) {
+ throw new Error("Drawn card result is false");
+ }
+
+ const card4 = player.cardInHand();
+
+ if (card4 === false) {
+ throw new Error("Card is false");
+ }
+
+ const playResult5 = board.playCard(player, 2, card4, 1, 0);
+
+ testAssert(
+ "playResult5",
+ playResult5 === "The card was successfully placed.",
+ "The play result should be 'The card was successfully placed.', but it is '" +
+ playResult5 +
+ "'.",
+ );
+
+ testAssert(
+ "board.cards.length === 3",
+ board.cards.length === 3,
+ "The board should have 3 cards, but it has " +
+ board.cards.length +
+ " cards.",
+ );
+
+ testAssert(
+ "board.cards[2].x === 1 && board.cards[2].y === 0",
+ board.cards[2].x === 1 && board.cards[2].y === 0,
+ "The card should be at the coordinates (1, 0), but it is at the coordinates (" +
+ board.cards[2].x +
+ ", " +
+ board.cards[2].y +
+ ").",
+ );
+ }
+}
+
+function testPuntoNumberOfTotalCards(n: number = 1) {
+ for (let nbrTest = 0; nbrTest < n; nbrTest++) {
+ const boardOptions2Players: BoardOptions = {
+ nbrPlayers: 2,
+ listPlayerOptions: [
+ {
+ name: "Player 1",
+ points: 0,
+ isTurn: true,
+ },
+ {
+ name: "Player 2",
+ points: 0,
+ isTurn: false,
+ },
+ ],
+ };
+
+ const puntoOptions2Players: PuntoOptions = {
+ boardOption: boardOptions2Players,
+ };
+
+ const createPunto2Players = new CreatePunto(puntoOptions2Players);
+
+ createPunto2Players.createPunto();
+
+ const board2Players = createPunto2Players.getPunto()?.board;
+
+ const totalCards2Players = board2Players?.players.reduce(
+ (acc, player) => acc + player.deck.length,
+ 0,
+ );
+
+ testAssert(
+ "totalCards2Players === 72",
+ totalCards2Players === 72,
+ "The total number of cards should be 72, but it is " +
+ totalCards2Players +
+ ".",
+ );
+
+ const boardOptions3Players: BoardOptions = {
+ nbrPlayers: 3,
+ listPlayerOptions: [
+ {
+ name: "Player 3",
+ points: 0,
+ isTurn: false,
+ },
+ {
+ name: "Player 4",
+ points: 0,
+ isTurn: false,
+ },
+ {
+ name: "Player 5",
+ points: 0,
+ isTurn: false,
+ },
+ ],
+ };
+
+ const puntoOptions3Players: PuntoOptions = {
+ boardOption: boardOptions3Players,
+ };
+
+ const createPunto3Players = new CreatePunto(puntoOptions3Players);
+
+ createPunto3Players.createPunto();
+
+ const board3Players = createPunto3Players.getPunto()?.board;
+
+ const totalCards3Players = board3Players?.players.reduce(
+ (acc, player) => acc + player.deck.length,
+ 0,
+ );
+
+ testAssert(
+ "totalCards3Players === 72",
+ totalCards3Players === 72,
+ "The total number of cards should be 72, but it is " +
+ totalCards3Players +
+ ".",
+ );
+
+ const boardOptions4Players: BoardOptions = {
+ nbrPlayers: 4,
+ listPlayerOptions: [
+ {
+ name: "Player 6",
+ points: 0,
+ isTurn: false,
+ },
+ {
+ name: "Player 7",
+ points: 0,
+ isTurn: false,
+ },
+ {
+ name: "Player 8",
+ points: 0,
+ isTurn: false,
+ },
+ {
+ name: "Player 9",
+ points: 0,
+ isTurn: false,
+ },
+ ],
+ };
+
+ const puntoOptions4Players: PuntoOptions = {
+ boardOption: boardOptions4Players,
+ };
+
+ const createPunto4Players = new CreatePunto(puntoOptions4Players);
+
+ createPunto4Players.createPunto();
+
+ const board4Players = createPunto4Players.getPunto()?.board;
+
+ const totalCards4Players = board4Players?.players.reduce(
+ (acc, player) => acc + player.deck.length,
+ 0,
+ );
+
+ testAssert(
+ "totalCards4Players === 72",
+ totalCards4Players === 72,
+ "The total number of cards should be 72, but it is " +
+ totalCards4Players +
+ ".",
+ );
+ }
+}
+
+function testPuntoImpossiblePlayerNumber(n: number = 1) {
+ for (let nbrTest = 0; nbrTest < n; nbrTest++) {
+ const boardOptions1Player: BoardOptions = {
+ nbrPlayers: 1,
+ listPlayerOptions: [
+ {
+ name: "Player 1",
+ points: 0,
+ isTurn: true,
+ },
+ {
+ name: "Player 2",
+ points: 0,
+ isTurn: false,
+ },
+ ],
+ };
+
+ const puntoOptions1Player: PuntoOptions = {
+ boardOption: boardOptions1Player,
+ };
+
+ const createPunto1Player = new CreatePunto(puntoOptions1Player);
+
+ try {
+ createPunto1Player.createPunto();
+ } catch (error) {
+ const thisError = error as Error;
+
+ testAssert(
+ "thisError.message === 'The number of players must be between 2 and 4.'",
+ thisError.message ===
+ "The number of players must be between 2 and 4.",
+ "The error message should be 'The number of players must be between 2 and 4.', but it is '" +
+ thisError.message +
+ "'.",
+ );
+ }
+
+ const boardOptions5Players: BoardOptions = {
+ nbrPlayers: 5,
+ listPlayerOptions: [
+ {
+ name: "Player 1",
+ points: 0,
+ isTurn: true,
+ },
+ {
+ name: "Player 2",
+ points: 0,
+ isTurn: false,
+ },
+ {
+ name: "Player 3",
+ points: 0,
+ isTurn: false,
+ },
+ {
+ name: "Player 4",
+ points: 0,
+ isTurn: false,
+ },
+ {
+ name: "Player 5",
+ points: 0,
+ isTurn: false,
+ },
+ ],
+ };
+
+ const puntoOptions5Players: PuntoOptions = {
+ boardOption: boardOptions5Players,
+ };
+
+ const createPunto5Players = new CreatePunto(puntoOptions5Players);
+
+ try {
+ createPunto5Players.createPunto();
+ } catch (error) {
+ const thisError = error as Error;
+
+ testAssert(
+ "thisError.message === 'The number of players must be between 2 and 4.'",
+ thisError.message ===
+ "The number of players must be between 2 and 4.",
+ "The error message should be 'The number of players must be between 2 and 4.', but it is '" +
+ thisError.message +
+ "'.",
+ );
+ }
+ }
+}
+
+function testPuntoCardsCorrect3Players(n: number = 1) {
+ for (let nbrTest = 0; nbrTest < n; nbrTest++) {
+ const boardOptions: BoardOptions = {
+ nbrPlayers: 3,
+ listPlayerOptions: [
+ {
+ name: "Player 3",
+ points: 0,
+ isTurn: false,
+ },
+ {
+ name: "Player 4",
+ points: 0,
+ isTurn: false,
+ },
+ {
+ name: "Player 5",
+ points: 0,
+ isTurn: false,
+ },
+ ],
+ };
+
+ const puntoOptions: PuntoOptions = {
+ boardOption: boardOptions,
+ };
+
+ const createPunto = new CreatePunto(puntoOptions);
+
+ createPunto.createPunto();
+
+ createPunto.getPunto()?.board.players.forEach((player) => {
+ const playerDeck = player.deck;
+
+ testAssert(
+ "playerDeck.length === 24",
+ playerDeck.length === 24,
+ "The deck should have 24 cards, but it has " +
+ playerDeck.length +
+ " cards.",
+ );
+
+ // Check if each color has 2 cards of each number
+ const colors: string[] = player.color;
+
+ colors.forEach((color) => {
+ const cardsByColor = playerDeck.filter(
+ (card) => card.color === color,
+ );
+
+ if (player.color.includes(color)) {
+ testAssert(
+ "cardsByColor.length === 18",
+ cardsByColor.length === 18,
+ "The color " +
+ color +
+ " should have 18 cards, but it has " +
+ cardsByColor.length +
+ " cards.",
+ );
+ } else {
+ testAssert(
+ "cardsByColor.length === 6",
+ cardsByColor.length === 6,
+ "The color " +
+ color +
+ " should have 6 cards, but it has " +
+ cardsByColor.length +
+ " cards.",
+ );
+ }
+
+ const numbers: number[] = [1, 2, 3, 4, 5, 6, 7, 8, 9];
+
+ numbers.forEach((number) => {
+ const cards = playerDeck.filter(
+ (card) => card.color === color && card.value === number,
+ );
+
+ testAssert(
+ "cards.length === 2",
+ cards.length === 2,
+ "The color " +
+ color +
+ " and the number " +
+ number +
+ " should have 2 cards, but it has " +
+ cards.length +
+ " cards.",
+ );
+ });
+ });
+ });
+ }
+}
+
+function testPuntoCardsCorrect4Players(n: number = 1) {
+ for (let nbrTest = 0; nbrTest < n; nbrTest++) {
+ const boardOptions: BoardOptions = {
+ nbrPlayers: 4,
+ listPlayerOptions: [
+ {
+ name: "Player 6",
+ points: 0,
+ isTurn: false,
+ },
+ {
+ name: "Player 7",
+ points: 0,
+ isTurn: false,
+ },
+ {
+ name: "Player 8",
+ points: 0,
+ isTurn: false,
+ },
+ {
+ name: "Player 9",
+ points: 0,
+ isTurn: false,
+ },
+ ],
+ };
+
+ const puntoOptions: PuntoOptions = {
+ boardOption: boardOptions,
+ };
+
+ const createPunto = new CreatePunto(puntoOptions);
+
+ createPunto.createPunto();
+
+ createPunto.getPunto()?.board.players.forEach((player) => {
+ const playerDeck = player.deck;
+
+ testAssert(
+ "playerDeck.length === 18",
+ playerDeck.length === 18,
+ "The deck should have 18 cards, but it has " +
+ playerDeck.length +
+ " cards.",
+ );
+
+ // Check if each color has 2 cards of each number
+ const colors: string[] = player.color;
+
+ colors.forEach((color) => {
+ const cards = playerDeck.filter((card) => card.color === color);
+
+ testAssert(
+ "cards.length === 18 4Players",
+ cards.length === 18,
+ "The color " +
+ color +
+ " should have 18 cards, but it has " +
+ cards.length +
+ " cards.",
+ );
+
+ const numbers: number[] = [1, 2, 3, 4, 5, 6, 7, 8, 9];
+
+ numbers.forEach((number) => {
+ const cards = playerDeck.filter(
+ (card) => card.color === color && card.value === number,
+ );
+
+ testAssert(
+ "cards.length === 2 4Players",
+ cards.length === 2,
+ "The color " +
+ color +
+ " and the number " +
+ number +
+ " should have 2 cards, but it has " +
+ cards.length +
+ " cards.",
+ );
+ });
+ });
+ });
+ }
+}
+
+function testPuntoCardsCorrect2Players(n: number = 1) {
+ for (let nbrTest = 0; nbrTest < n; nbrTest++) {
+ const boardOptions: BoardOptions = {
+ nbrPlayers: 2,
+ listPlayerOptions: [
+ {
+ name: "Player 1",
+ points: 0,
+ isTurn: true,
+ },
+ {
+ name: "Player 2",
+ points: 0,
+ isTurn: false,
+ },
+ ],
+ };
+
+ const puntoOptions: PuntoOptions = {
+ boardOption: boardOptions,
+ };
+
+ const createPunto = new CreatePunto(puntoOptions);
+
+ createPunto.createPunto();
+
+ createPunto.getPunto()?.board.players.forEach((player) => {
+ const playerDeck = player.deck;
+
+ testAssert(
+ "playerDeck.length === 36",
+ playerDeck.length === 36,
+ "The deck should have 36 cards, but it has " +
+ playerDeck.length +
+ " cards.",
+ );
+
+ // Check if each color has 2 cards of each number
+ const colors: string[] = player.color;
+
+ colors.forEach((color) => {
+ const cards = playerDeck.filter((card) => card.color === color);
+
+ testAssert(
+ "cards.length === 18 2Players",
+ cards.length === 18,
+ "The color " +
+ color +
+ " should have 18 cards, but it has " +
+ cards.length +
+ " cards.",
+ );
+
+ const numbers: number[] = [1, 2, 3, 4, 5, 6, 7, 8, 9];
+
+ numbers.forEach((number) => {
+ const cards = playerDeck.filter(
+ (card) => card.color === color && card.value === number,
+ );
+
+ testAssert(
+ "cards.length === 2 2Players",
+ cards.length === 2,
+ "The color " +
+ color +
+ " and the number " +
+ number +
+ " should have 2 cards, but it has " +
+ cards.length +
+ " cards.",
+ );
+ });
+ });
+ });
+ }
+}
+
+function testPuntoPlayerName(n: number = 1) {
+ for (let nbrTest = 0; nbrTest < n; nbrTest++) {
+ const boardOptions: BoardOptions = {
+ nbrPlayers: 2,
+ listPlayerOptions: [
+ {
+ name: "Player 1",
+ points: 0,
+ isTurn: true,
+ },
+ {
+ name: "Player 2",
+ points: 0,
+ isTurn: false,
+ },
+ ],
+ };
+
+ const puntoOptions: PuntoOptions = {
+ boardOption: boardOptions,
+ };
+
+ const createPunto = new CreatePunto(puntoOptions);
+
+ createPunto.createPunto();
+
+ const playersList = createPunto.getPunto()?.board.players;
+
+ // Test if all names is correct
+ ["Player 1", "Player 2"].forEach((name, index) => {
+ testAssert(
+ "playersList?.[index].name === name",
+ playersList?.[index].name === name,
+ "The name should be " +
+ name +
+ ", but it is " +
+ playersList?.[index].name +
+ ".",
+ );
+ });
+ }
+}
diff --git a/src/tests/Tests.test.ts b/src/tests/Tests.test.ts
new file mode 100644
index 0000000..c727563
--- /dev/null
+++ b/src/tests/Tests.test.ts
@@ -0,0 +1,48 @@
+import {AssertionError, ok} from "assert";
+import {testAllPunto} from "./Game.test";
+import {testAllDB} from "./DB.test";
+
+const testGame = true;
+const testDB = true;
+
+let nbrTestPassed = 0;
+
+export function testAssert(id: string, condition: boolean, message: string) {
+ try {
+ ok(condition, message);
+
+ // console.log("Test passed");
+
+ nbrTestPassed++;
+ } catch (error) {
+ const assertError = error as AssertionError;
+
+ console.log("Number of tests passed: ", nbrTestPassed);
+
+ console.error(
+ "Test failed: ",
+ id,
+ "\n",
+ assertError.message,
+ "\n",
+ "Expected: ",
+ assertError.expected,
+ "\n",
+ "Actual: ",
+ assertError.actual,
+ "\n",
+ "With operator: ",
+ assertError.operator,
+ );
+
+ process.exit(1);
+ }
+}
+
+(async () => {
+ if (testGame) testAllPunto();
+
+ if (testDB) await testAllDB();
+
+ console.log("Number of tests passed: ", nbrTestPassed);
+})();
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..8573d7d
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,22 @@
+{
+ "compilerOptions": {
+ "target": "ESNext",
+ "module": "commonjs",
+ "strict": true,
+ "esModuleInterop": true,
+ "rootDir": "./src",
+ "outDir": "./dist",
+ "skipLibCheck": true,
+ "forceConsistentCasingInFileNames": true,
+ "removeComments": true,
+ "allowJs": true, // pour autoriser l'utilisation de fichiers JavaScript
+ "resolveJsonModule": true, // pour utiliser des fichiers JSON avec TypeScript
+ "moduleResolution": "node", // pour imiter la résolution de modules de Node.js
+ "noImplicitAny": true, // pour activer des vérifications de type plus strictes
+ "sourceMap": true, // pour produire des fichiers .map pour le débogage
+ "experimentalDecorators": true, // pour activer les décorateurs
+ "emitDecoratorMetadata": true // pour activer les décorateurs
+ },
+ "include": ["src/**/*.ts", "src/**/*.js"],
+ "exclude": ["node_modules", "src/**/*.test.ts"]
+}