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"] +}