diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 00c4c7468c568c935a3eb46ec1cb6de75b5adbd8..61453a5d3480f43d8e76c02346bca9ff47d780b1 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,4 +1,5 @@ stages: + - test - build - release @@ -8,6 +9,25 @@ services: before_script: - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY +test: + image: node:16.14-alpine + stage: test + only: + refs: + - main + - rc + - beta + - develop + # This matches maintenance branches + - /^(([0-9]+)\.)?([0-9]+)\.x/ + # This matches pre-releases + - /^([0-9]+)\.([0-9]+)\.([0-9]+)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+)?$/ + before_script: [] + script: + - cd ./api + - npm ci --development + - npm run test + build: image: docker:dind stage: build diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 008e616b96d56238afd73e1c470204928f684fd7..0000000000000000000000000000000000000000 --- a/LICENSE +++ /dev/null @@ -1,661 +0,0 @@ - GNU AFFERO GENERAL PUBLIC LICENSE - Version 3, 19 November 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU Affero General Public License is a free, copyleft license for -software and other kinds of works, specifically designed to ensure -cooperation with the community in the case of network server software. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -our General Public Licenses are intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - Developers that use our General Public Licenses protect your rights -with two steps: (1) assert copyright on the software, and (2) offer -you this License which gives you legal permission to copy, distribute -and/or modify the software. - - A secondary benefit of defending all users' freedom is that -improvements made in alternate versions of the program, if they -receive widespread use, become available for other developers to -incorporate. Many developers of free software are heartened and -encouraged by the resulting cooperation. However, in the case of -software used on network servers, this result may fail to come about. -The GNU General Public License permits making a modified version and -letting the public access it on a server without ever releasing its -source code to the public. - - The GNU Affero General Public License is designed specifically to -ensure that, in such cases, the modified source code becomes available -to the community. It requires the operator of a network server to -provide the source code of the modified version running there to the -users of that server. Therefore, public use of a modified version, on -a publicly accessible server, gives the public access to the source -code of the modified version. - - An older license, called the Affero General Public License and -published by Affero, was designed to accomplish similar goals. This is -a different license, not a version of the Affero GPL, but Affero has -released a new version of the Affero GPL which permits relicensing under -this license. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU Affero General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Remote Network Interaction; Use with the GNU General Public License. - - Notwithstanding any other provision of this License, if you modify the -Program, your modified version must prominently offer all users -interacting with it remotely through a computer network (if your version -supports such interaction) an opportunity to receive the Corresponding -Source of your version by providing access to the Corresponding Source -from a network server at no charge, through some standard or customary -means of facilitating copying of software. This Corresponding Source -shall include the Corresponding Source for any work covered by version 3 -of the GNU General Public License that is incorporated pursuant to the -following paragraph. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the work with which it is combined will remain governed by version -3 of the GNU General Public License. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU Affero General Public License from time to time. Such new versions -will be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU Affero General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU Affero General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU Affero General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - gateway - Copyright (C) 2022 sibmip - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see <https://www.gnu.org/licenses/>. - -Also add information on how to contact you by electronic and paper mail. - - If your software can interact with users remotely through a computer -network, you should also make sure that it provides a way for users to -get its source. For example, if your program is a web application, its -interface could display a "Source" link that leads users to an archive -of the code. There are many ways you could offer source, and different -solutions will be better for different programs; see section 13 for the -specific requirements. - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU AGPL, see -<https://www.gnu.org/licenses/>. diff --git a/api/assets/engines/default/tos.md b/api/assets/engines/default/tos.md index 349e0e06f62508b9b73c01f2adaedcedb26e4ce7..14d64935ba67ff1a8f80aeb76bcbededfdf8cfbd 100644 --- a/api/assets/engines/default/tos.md +++ b/api/assets/engines/default/tos.md @@ -1,200 +1,251 @@ -HBP Medical Informatics Platform -================================ - -Terms and Conditions for Service --------------------------------- - -1. Ownership and Operation of Website - - The Website is owned and operated by the Ecole Polytechnique Fédérale de Lausanne, Human Brain Project Coordination Office (EPFL-HBPPCO), Switzerland hereinafter described as EPFL-HBPPCO or “Service Providerâ€. - -2. Scope of the Terms of Service - - The following terms and conditions of this Agreement govern all use of the collab.humanbrainproject.eu website and all content, services and products available at or through the website, including, but not limited to, collab.humanbrainproject.eu and services.humanbrainproject.eu. The following terms do _not_ apply to services and web based applications offered by third-parties through the Collaboratory App extension system. - - -**Access to this HBP Medical Informatics Platform ("Platform") is provided to you under these terms and conditions for service and any amendments or supplements to them (collectively referred to as the "Terms & Conditions") that may be posted on the Platform from time to time. Your use of the Platform, or any other services or content provided through the Platform, shall be deemed to constitute your consent to be legally bound by the Terms & Conditions, which shall be enforceable in the same way as if you had signed the Terms & Conditions.** - -_If you do not accept the Terms & Conditions when applying for registration or thereafter, when using the Platform, you are not permitted to access or use the Platform or to submit or post any materials on it._ - -Besides the law applicable to these Terms & Conditions, this Platform and data available on this Platform are also subject to European legislations and regulations as well as to the ethical principles of Horizon 2020, the EU Framework programme for research and innovation ([https://ec.europa.eu/programmes/horizon2020/](https://ec.europa.eu/programmes/horizon2020/)). - -The Medical Informatics Platform is a privacy preserving platform that complies with the European regulation, based on Privacy By Design and by Default. The reference text is the REGULATION (EU) 2016/679 OF THE EUROPEAN PARLIAMENT AND OF THE COUNCIL of 27 April 2016, on the protection of natural persons with regard to the processing of personal data and on the free movement of such data, and repealing Directive 95/46/EC (General Data Protection Regulation) (henceforth ‘GDPR’). - -1. Put at Disposal and Operation of Platform - - The Platform is put at disposal and operated by the CHUV, Lausanne, as the Coordinator of the HBP Sub-Project 8 ("Coordinator"). - -2. Scope of the Terms & Conditions - - These Terms & Conditions govern all use of the Platform and all content, services and products available at or through the Platform. - -3. Definition of Terms - - The following definitions include terms used in European legislations and regulations and introduce new ones, specific to this Platform. - - 1. Approved Use means the access to and use of the Platform for non-commercial and non-competitive use for any Users’ research activities in the field of HBP and approved by the Scientific Committee. - 1. Beneficiary means any party to the HBP Agreements. - 1. MIP is the name given to the Medical Informatics Platform developed by the HBP. - 1. Consortium means the group that consists of all the parties being part of the HBP Agreements but does not include the European Commission ("EC") - 1. Data Providers. Individuals and/or institutions that produce and make available Data Sets on the Platform to the Data Users. - 1. Coordinator means the intermediary between the Consortium and the EC as stated in the HBP Agreements. - 1. Data Set. Digital data, either raw or derived, including but not limited to research and scientific data as well as metadata provided to the Platform by Data Providers. Data Set may contain software and algorithms. - 1. Data Subject. A natural or legal person whose personal data (i.e. all information relating to an identified or identifiable person) is processed. - 1. Data User. Individual and/or institution to which access to Data Sets on the Platform is granted, subject to the acceptance of the Terms & Conditions by such individuals and/or the institutions, and which are the Beneficiaries, the EC, EU Institutions and other EU bodies as well as the EU Member States. - 1. HBP is the Human Brain Project under the FET Integrated Project (FP7 Grant Agreement no. 604102, i.e. its ramp-up phase), and any following continuation of the project under Horizon 2020. - 1. HBP Agreements mean the agreements concluded in respect of the HBP, such as FP7 Grant Agreement no. 604102, the Consortium Agreement under the FP7 Grant Agreement no. 604102, the Framework Partnership Agreement no. 650003, and their subsequent agreements. - 1. Personal Data is defined as any information relating to an identified or identifiable natural or legal person, including institutions. An identifiable person is a person who can be identified, directly or indirectly, by reference to an identification number or factors specific to his or her physical, physiological, mental, economic, cultural or social identity. Personal Data does not include publicly available information that has not been combined with non- Personal Data, nor does it include information that has been anonymized. -4. Access to Platform - - The Platform intends to provide functionality designed to enable and enhance collaboration and team science in the field of the HBP. All this research shall, as far as possible, be monitored to ensure the ethical use of data and responsible research and innovation with active roles for scientists, philosophers, ethicists, policy makers and members of society. - - Access to the Platform is reserved to Data Users for activities falling under the Approved Use only. Beneficiaries must also comply with the HBP Agreements in their use of the Platform. - - 1. Login Credentials - - When you first register to use the Platform, you will be prompted to create a user name and password. You must keep your password secret and not give it to anyone else or let them use your account. You must inform us immediately if you suspect any unauthorized use of or access to your password or account. - - The Coordinator and the Beneficiaries that participated in the specific action related to the Platform will not be responsible if you suffer any harm or loss because you do not keep your password secret. - - 2. Registration Data and Process - - Registration will take place the first time you wish to access the Platform. The following registration information is required directly or by proxy prior to accessing the Platform: - - * name - * e-mail address; and - * motivation for using the platform. -5. Contributor's Confirmations and Liabilities - - Data Providers confirm that they have signed the Data Sharing Agreement and complied with the legal and ethical requirements, ensuring the data shared is compliant with all EU and member-state regulation and practices. - - The Coordinator may at its discretion review compliance of the Data Providers with these confirmations and liabilities. Non-compliant Data Sets may be removed from the Platform. - -6. Terms on Use - -7. Rights of Access and Rights of Use - - Use of (including access to) the MIP by the Users is restricted to the Approved Use. - - Data Sets are provided for use by the Data Users only. Any rights to sub-license are excluded, if not expressly agreed with the Contributor. - - The Data User is permitted to produce and distribute derived works from Data Sets provided that those derivatives are released for the Approved Use. Any other uses for the Data Sets or its derived products will require explicit permission from the Contributor. - - Software and algorithms contained in Data Sets are made available pursuant to the terms of their respective license agreements. For a use outside the HBP, a Contributor may require that the Data Sets are subject to licensing and/or must be kept confidential as provided for by these Terms & Conditions. This requirement must be expressly indicated in each Data Set. In such a case, licensing terms will be directly agreed between the Contributor and the involved Data Users. - -8. Other Limitations of Use - +EBRAINS Medical Informatics Platform (MIP) +=== + + + +General Terms and Conditions +--- + + + +**Preamble Ownership and Operation of the Website** + +The Website is owned and operated by the EBRAINS AISBL, Belgium, the Coordinator (hereinafter described as "Coordinator") of the HBP and the EBRAINS Research Infrastructure, hereinafter described as "EBRAINS". + +Information about how EBRAINS uses cookies and other similar technology on this website can be found under [https://ebrains.eu/terms/#cookies](https://ebrains.eu/terms/#cookies). + +EBRAINS is committed to the safe, transparent and confidential collection and processing of your personal data. [https://ebrains.eu/terms/#privacy-statement](https://ebrains.eu/terms/#privacy-statement) + +**Access to this EBRAINS Medical Informatics Platform ("MIP") is provided to you under these General Terms & Conditions (**[**https://ebrains.eu/terms/#general-terms-of-use**](https://ebrains.eu/terms/#general-terms-of-use)**) and any amendments or supplements to them (collectively referred to as the "Terms & Conditions") that may be posted on the website from time to time.** + +**Your use of the MIP, or any other services or content provided through the MIP, shall be deemed to constitute your consent to be legally bound by the Terms & Conditions, which shall be enforceable in the same way as if you had signed the Terms & Conditions.** + +<u>_If you do not accept the Terms & Conditions</u> when applying for registration or thereafter, when using the MIP, <u>you are not permitted to access or use the MIP or to submit or post any materials on it_</u>. + +Besides the law applicable to these Terms & Conditions, the MIP and data available on the MIP are also subject to European legislations and regulations as well as to the ethical principles of Horizon 2020, the EU Framework programme for research and innovation ([https://ec.europa.eu/programmes/horizon2020/](https://ec.europa.eu/programmes/horizon2020/)). + +The Medical Informatics Platform is a privacy-preserving platform that complies with the European regulation, based on Privacy By Design and by Default. The reference text is the REGULATION (EU) 2016/679 OF THE EUROPEAN PARLIAMENT AND OF THE COUNCIL of 27 April 2016, on the protection of natural persons with regard to the processing of personal data and on the free movement of such data, and repealing Directive 95/46/EC (General Data Protection Regulation, henceforth 'GDPR'). + +1. **Operation of the MIP** + + The MIP is provided and operated by the Centre Hospitalier Universitaire Vaudois located in Lausanne (hereinafter described as "CHUV"), as coordinator of the Medical Informatics Platform. + +1. **Scope** + + These Terms & Conditions govern all use of the MIP and all content, services and products available at or through the MIP. + + These Terms & Conditions **does not** cover any aspect of **data sharing or data transfer**. These aspects are covered in separate agreements, the "Data Sharing Agreement" and "Data Transfer Agreement". + +1. **Definitions** + + The following definitions include terms used in European legislations and regulations and introduce new ones, specific to the MIP. + + 1. **Approved Use** means the access to and use of the MIP for non-commercial and non-competitive use for any users' research activities in the field of HBP and EBRAINS and approved by the MIP`s Scientific Committee. + 2. **Beneficiary** means any party to the HBP Agreements. + 3. **Consortium** means the group that consists of all the parties being part of the HBP Agreements but does not include the European Commission ("EC") + 4. **Coordinator** means the intermediary between the Consortium and the EC as stated in the HBP Agreements. + 5. **Data Providers** meansindividuals and/or institutions that produce and make available Data Sets on the MIP to the Data Users. + 6. **Data Set** means digital data, either raw or derived, including but not limited to research and scientific data as well as metadata provided to the MIP by Data Providers. + 7. **Data Subject** means a natural or legal person whose Personal Data is processed. + 8. **Data Use Agreement (EBRAINS Data Use Agreement)** identifies your legal responsibilities when accessing pseudonymised human data [https://ebrains.eu/terms/#data-use-agreement](https://ebrains.eu/terms/#data-use-agreement) + 9. **Data User** means individual and/or institution to which access to Data Sets on the MIP is granted, subject to the acceptance of the Terms & Conditions by such individuals and/or the institutions, and which are the Beneficiaries, the EC, EU Institutions and other EU bodies as well as the EU Member States. + 10. **EBRAINS** is the HBP legacy as a sustainable European Research Infrastructure, that was launched in 2020 during HBP SGA3, the HBP`s last specific grant agreement. + 11. **HBP Agreements** mean the agreements concluded in respect of the HBP, such as FP7 Grant Agreement no. 604102, the Consortium Agreement under the FP7 Grant Agreement no. 604102, the Framework Partnership Agreement no. 650003, and their subsequent agreements. + 12. **HBP** is the Human Brain Project under the FET Integrated Project (FP7 Grant Agreement no. 604102, i.e. its ramp-up phase), and any following continuation of the project under Horizon 2020. + 13. **MIP** is the name given to the Medical Informatics Platform developed by the HBP. MIP Technical Documentation is [available here](https://github.com/HBPMedical/mip-docs). + 14. **Personal Data** is defined as any information relating to an identified or identifiable natural or legal person, including institutions. An identifiable person is a person who can be identified, directly or indirectly, by reference to an identification number or factors specific to his or her physical, physiological, mental, economic, cultural or social identity. Personal Data does not include publicly available information that has not been combined with non- Personal Data, nor does it include information that has been anonymised. + +1. **Access to the MIP** + + The MIP intends to provide functionality designed to enable and enhance collaboration and team science in the field of the HBP and EBRAINS. All this research shall, as far as possible, be monitored to ensure the ethical use of data and responsible research and innovation with active roles for scientists, philosophers, ethicists, policy makers and members of society. + + Access to the MIP is reserved to Data Users for activities falling under the Approved Use only. Data Users shall also comply with the EBRAINS Access Policy ([https://ebrains.eu/terms/#access-policy](https://ebrains.eu/terms/#access-policy)) in their use of the MIP. + + 1. Login Credentials + + When you first register to use the MIP, you will be prompted to create an EBRAINS Account ([https://ebrains.eu/register](https://ebrains.eu/register)) with a user name and password. The password should be as "strong" as possible. You must keep your password secret and not give it to anyone else nor let anyone else use your account. You must inform us immediately if you suspect any unauthorised use of or access to your password or account. + + The Coordinator and the Beneficiaries that participated in the specific action related to the MIP will not be responsible if you suffer any harm or loss because you do not keep your password secret. + + 1. Registration Data and Process + + Registration will take place the first time you wish to access the MIP. The following registration information is required directly or by proxy prior to accessing the MIP: + + - EBRAINS account ([https://ebrains.eu/register](https://ebrains.eu/register)) + - Name + - Institutional e-mail address; and + - Motivation for using the MIP. + +1. **Data Provider's warranty and liabilities** + + Data Providers warrants that they have signed the Data Sharing Agreement or Data Transfer Agreement and complied with the relevant legal and ethical requirements, ensuring the data shared is compliant with all EU and member-state regulations and practices. + + The Coordinator and CHUV may at their discretion review compliance of the Data Providers with these warranties and liabilities. Non-compliant Data Sets will be removed from the MIP. + + Data Provider is also responsible for ensuring that any data stored in the MIP FEDERATED NODE has been anonymized. + +1. **Use of the MIP** + + Use of (including access to) the MIP by the Data Users is restricted to the Approved Use. + + In return for your warranty to comply with these Terms and Conditions, we grant you a limited, non-transferable, non-sublicensable, personalized, non-exclusive, revocable right to access the MIP under the conditions set forth in these Terms and Conditions. + + The MIP and its content is protected by the copyright laws. Software and algorithms contained in the MIP are made available pursuant to the terms of their respective licenses. Data User is responsible for complying with the terms of the Licenses (https://github.com/HBPMedical/mip-docs). + + All appropriate copyright and other notices and legends shall be retained on the software and the algorithms, and cannot be removed. + + Such notices shall be reproduced on all authorized copies of the software and related documentation including in any scientific publications. + + Data User warrants that he will not attempt to discover or access the source code of the MIP, in whole or in part, unless it has been expressly published by its owner and released into the public. + + The Data User shall not distribute the MIP to third parties, or make them available in any other way. + + Unless required by applicable mandatory laws or authorized by the present Terms & Conditions, the Data User shall not modify, disassemble, reverse engineer or decompile the MIP or any parts thereof. The Data User, agree not to modify the MIP, in whole or in part, in anyway whatsoever. + + The Coordinator and CHUV reserve the right to change a specific aspect or a specific feature by updating the MIP, resetting that feature and suspending the service or support for that feature. These changes may affect your activities on the MIP. Changes may include the removal, modification or reset of the features you use. + + Any updates thereof shall be subject to these Terms & Conditions or the applicable license terms specifically negotiated with CHUV. + +1. **Use of the Data Sets** + + Data Sets are provided for use by the Data Users only and pursuant to the Approved Use. Any rights to sub-license or copy the Data Sets are excluded, if not expressly agreed with the Data Provider and CHUV. + + The Data User is allowed to produce and distribute derived works from Data Sets provided that those derivatives are released for the Approved Use and they include the appropriate citation and/or acknowledgement according to provision 9 and 10 below. Any other uses for the Data Sets or its derived products will require written consent from the Data Provider and CHUV. + + Unless expressly granted under this Terms & Conditions or agreed with the Data Provider, the Data User shall not distribute the Data Sets to third parties, or make them available in any other way. + +1. **Other Limitations of Use** + The following additional limitations apply to the use of the Data Sets by the Data Users: - - 1. Data Users may receive access to de-identified or aggregated Data Sets and in such cases, they will not attempt to establish the identity of, or attempt to contact any of the Data Subjects; - 2. Data Users may not carry out any calculations, operations or transactions that may interrupt, destroy or restrict the functionality of the operation of the Platform or of any program, computer or means of telecommunications; - 3. Data Users may not use the Data Sets for high risk activities such as the operation of nuclear facilities, air traffic control, or life support systems, where the use or failure of the Services could lead to death, personal injury, or environmental damage. - 4. Data Users commit to require from anyone of their team who utilizes the Data Sets, or anyone with whom they share the Data Sets, to accept and comply with these Terms & Conditions; and - 5. Data Users commit to comply with any additional rules and regulations imposed by their institutions and their institutional review board in accessing and using the Data Sets. - -9. Citations - + + 1. Data Users may receive access to de-identified or aggregated Data Sets and in such cases, Data Users shall not attempt to establish the identity of, or attempt to contact any of the Data Subjects; + 2. Data Users shall not carry out any calculations, operations or transactions that may interrupt, destroy or restrict the functionality of the operation of the MIP or of any program, computer or means of telecommunications; + 3. Data Users shall not circumvent, disable, or otherwise manipulate any of the MIP's security features or any features preventing or restricting the use or copying of the content that is available via MIP. + 4. Data Users shall not use the Data Sets for high-risk activities such as the operation of nuclear facilities, air traffic control, or life support systems, where the use or failure of the services could lead to death, personal injury, or environmental damage. + 5. Data Users shall not use the MIP as a clinical diagnostic tool. The MIP was not designed as a clinical diagnostic software. + 6. Data User shall not provide false or misleading information in the User account details, + 7. Data User shall not allow anyone else to use the MIP on his behalf or in his place. Data Users commit to require from anyone of their team who utilizes the Data Sets, or anyone with whom they share the Data Sets, to accept and comply with these Terms & Conditions; and + 8. Data User shall not use the MIP if we have temporarily suspended his right of use, or forbidden to continue using it, + 9. Data User shall not misuse, modify, interfere with, hack or disrupt the MIP, intercept messages, + 10. Data User shall not infiltrate the MIP with viruses, trojans, worms, logic bombs or anything else that might harm the MIP or any other user of the MIP, + 11. Data User shall not extract data from the MIP except as permitted under these Terms & Conditions, + 12. Data User shall not act in any unlawful or illegal manner. Data Users commit to comply with any additional applicable law, or any rules and regulations imposed by their institutions and their institutional review board in accessing and using the MIP and the Data Sets. + +1. **Citations** + Each Data User agrees to properly cite the Data Sets, including the Data Set Identifier, in any publications or in the metadata of any derived data products that are produced using the Data Sets. - - Citations shall take the following general form: Creator, Year of Data Set Publication, Title of Data Set, Data Set Identifier. Where a paper on the Data Set of its Contributor is available, then this should be cited. Where a date of issue of the Data Set is available, this date should be cited. - -9. Acknowledgements - - Data Users agree to include the following acknowledgment in any of their disseminations and publications, where the Data Sets contributed significantly to their content - - Data in this publication were provided by the HBP and/or received from services operated by the HBP. This project/research received funding from the European Union's Horizon 2020 Framework Programme for Research and Innovation under the Framework Partnership Agreement No. 650003 (HBP FPA and corresponding Specific Grant Agreement number). - - In addition, Data Users agree to include any additional acknowledgment of institutional support or specific funding awards provided in the metadata accompanying any Data Set, including those requested by the Contributor, in any dissemination where the Data Set contributes significantly to its content. - -9. Report of Misuse - - Any and all Data Users commit to report any use or disclosure of the Data Sets non-compliant with these Terms & Conditions of which they become aware as soon as possible, but at the latest within 15 days of becoming aware of such use or disclosure. - - Reports/disclosures should be submitted to the HBP Point of Registration at [https://www.hbp-pore.eu](https://www.hbp-pore.eu). - - Non-compliant Data Sets may be removed from the Platform. - - -General Data Protection Regulation -================================== - -1. Personal Data Policy - - The following privacy rules apply to any form of processing (including but not limited to collection, use and disclosure) of Personal Data through the Platform. - - This Personal Data Policy applies exclusively to this Platform and hence not to any other website the Platform refers to. - -2. Passive Collection of Personal Data - - While you use the Platform, Personal Data may be recorded passively (i.e. without you actively providing them to us), e.g. through use of your Internet Protocol ("IP") addresses and cookies ("Passive Collection"). - - * Internet Protocol - - An IP address is a number which is allocated to your device by your internet service provider in order to enable you to access the Internet. Data is saved automatically when you browse the Platform, whereby it is possible that information indirectly related to your person is collected and combined with your person. - - This information is used to enhance your experience in using the Platform and is not shared with external parties except as aggregated statistics. In the case of aggregated statistics, no personal information or behavioral data is visible. - - * Cookies - - To make this site work properly, we sometimes place small data files called cookies on your device. - - A cookie is a small text file that the Platform saves on your device when you visit the Platform. It enables the Platform to remember your actions and preferences (such as login, language, font size and other display preferences) over a period of time, so you do not have to keep re-entering them whenever you come back to the Platform. - - We may use cookies to remember: - - * if you have agreed (or not) to our use of cookies on the Platform; and - * your display preferences, such as contrast color settings or font size. - - Furthermore, please note that we use Google Analytics to monitor the traffic to our website. For more information read the Google Analytics policy (cf. [https://support.google.com/analytics/answer/6004245](https://support.google.com/analytics/answer/6004245) ). - - Enabling these cookies is not strictly necessary for the Platform to work but it will provide you with a better browsing experience. You can delete or block these cookies, but if you do that some features of the Platform may not work as intended. - - The cookie-related information is not used to identify you personally. These cookies are not used for any purpose other than those described here. - - You can control and/or delete cookies as you wish. For details, see www.aboutcookies.org. You can delete all cookies that are already on your device and you can set most browsers to prevent them from being placed. If you do this, however, you may have to manually adjust some preferences every time you visit the Platform and some services and functionalities may not work. - -3. Control on Personal Data - - You will always be able to check any of your Personal Data. This means that you can always (i) obtain information about your Personal Data on the Platform; (ii) request us to correct or update your Personal Data and (iii) request us to delete or block your Personal Data. - - Where we disclose Personal Data to domestic or international service providers (commissioned data processing) for the purpose of maintenance of the Platform, the service providers are not deemed third parties and are bound by contract to comply with Swiss Data Protection law and this Personal Data Policy. - - We will take appropriate measures to protect Personal Data from loss, misuse and unauthorized access, unauthorized disclosure, changes, deletion or destruction. Nevertheless, please note that the Internet and with that the Platform is never entirely secure or error-free. When sending your Personal Data electronically via the Platform, a secured internet connection (SSL) will be used for the transmission to your device. It is, however, your responsibility to take corresponding safeguard measures in the use of password etc. - -4. Copyright - - The content, organization, graphics, design, compilation, magnetic translation, digital conversion and other matters related to the Platform are protected under applicable copyrights, trademarks and other proprietary (including but not limited to intellectual property) rights. - + + Citations shall take the following general form: + + _Creator, Year of Data Set Publication, Title of Data Set, Data Set Identifier_. + + Where a paper on the Data Set of its Contributor is available, then this should be cited. + + Where a date of issue of the Data Set is available, this date should be cited. + + See also [https://ebrains.eu/terms/#general-terms-of-use](https://ebrains.eu/terms/#general-terms-of-use) + +1. **Acknowledgements** + + Data Users agree to include the following acknowledgment in any of their disseminations and publications, where the Data Sets contributed significantly to their content: + + "_Data in this publication were provided by the HBP and/or received from services operated by EBRAINS, which received funding under the H2020 Framework Partnership Agreement No. 650003_". + + In addition, Data Users agree to include any additional acknowledgment of institutional support or specific funding awards provided in the metadata accompanying any Data Set, including those requested by the Data Provider, in any dissemination where the Data Set contributes significantly to its content. + +1. **Report of Misuse** + + Any and all Data Users commit to report any use or disclosure of the Data Sets non-compliant with these Terms & Conditions of which they become aware as soon as possible, but at the latest within 15 days of becoming aware of such use or disclosure. + + Reports/disclosures should be submitted to the HBP Point of Registration at: [https://www.humanbrainproject.eu/en/social-ethical-reflective/about/register-ethical-concern/](https://www.humanbrainproject.eu/en/social-ethical-reflective/about/register-ethical-concern/). + + Non-compliant Data Sets may be removed from the MIP. + +1. **Personal Data Policy** + + The following privacy rules apply to any form of processing (including but not limited to collection, use and disclosure) of Personal Data through the MIP. + + This Personal Data Policy applies exclusively to this website and hence not to any other website the MIP refers to. + +1. **Passive Collection of Personal Data** + + While you use the MIP, Personal Data may be recorded passively (i.e. without you actively providing them to us), e.g. through use of your Internet Protocol ("IP") addresses and cookies ("Passive Collection"). + + 1. Internet Protocol + + An IP address is a number which is allocated to your device by your internet service provider in order to enable you to access the Internet. Data is saved automatically when you browse the MIP, whereby it is possible that information indirectly related to your person is collected and combined with your person. + + This information is used to enhance your experience in using the MIP and is not shared with external parties except as aggregated statistics. In the case of aggregated statistics, no personal information or behavioral data is visible. + + 1. Cookies + + To make this site work properly, we sometimes place small data files called cookies on your device. + + A cookie is a small text file that the MIP saves on your device when you visit the platform. It enables the MIP to remember your actions and preferences (such as login, language, font size and other display preferences) over a period of time, so you do not have to keep re-entering them whenever you come back to the MIP. + + We may use cookies to remember: + + - if you have agreed (or not) to our use of cookies on the MIP; and + - your display preferences, such as contrast color settings or font size. + + Enabling these cookies is not strictly necessary for the MIP to work but it will provide you with a better browsing experience. You can delete or block these cookies, but if you do that some features of the MIP may not work as intended. + + The cookie-related information is not used to identify you personally. These cookies are not used for any purpose other than those described here. + + You can control and/or delete cookies as you wish. For details, see [www.aboutcookies.org](http://www.aboutcookies.org/) . You can delete all cookies that are already on your device and you can set most browsers to prevent them from being placed. If you do this, however, you may have to manually adjust some preferences every time you visit the MIP and some services and functionalities may not work. + +1. **Control on Personal Data** + + You will always be able to check any of your Personal Data. This means that you can always (i) obtain information about your Personal Data on the MIP; (ii) request us to correct or update your Personal Data and (iii) request us to delete or block your Personal Data. + + Where we disclose Personal Data to domestic or international service providers (commissioned data processing) for the purpose of maintenance of the MIP, the service providers are not deemed third parties and are bound by contract to comply with Swiss Data Protection law and this Personal Data Policy. + + We will take appropriate measures to protect Personal Data from loss, misuse and unauthorized access, unauthorized disclosure, changes, deletion or destruction. Nevertheless, please note that the Internet and with that the MIP is never entirely secure or error-free. When sending your Personal Data electronically via the MIP, a secured internet connection (SSL) will be used for the transmission to your device. It is, however, your responsibility to take corresponding safeguard measures in the use of password etc. + +1. **Copyright** + + The content, organization, graphics, design, compilation, magnetic translation, digital conversion and other matters related to the MIP are protected under applicable copyrights, trademarks and other proprietary (including but not limited to intellectual property) rights. + Subject to statutory allowances, extracts of material from the site may be accessed, downloaded and printed for your personal and non-commercial use within the Approved Use only. - -5. Confidentiality - - Data Sets uploaded to the Platform are considered non-confidential in all cases. - -6. Termination and Liability - - The Coordinator shall have the right to terminate access and use of the Data Sets immediately by written notice upon the Data User's breach of, or non-compliance with, any of the terms of the Terms & Conditions. - - The Data User may be held entirely responsible for any misuse that was caused or encouraged by the Data User's failure to abide by the Terms & Conditions. Consequences from failure to abide by the Terms & Conditions may include legal proceedings. - -7. Disclaimer - - The Data Sets, and any part thereof, as well as the Platform are provided for the Approved Use only. - - The Platform and the Data Sets are provided on an "as is" and "as available" basis. Please note that the Platform and the Data Sets may contain bugs, viruses, errors, problems or other limitations. To the extent permitted by law, the Coordinator and the Beneficiaries that participated in the specific action related to the Platform exclude any warranties (whether expressed or implied) for the Platform and the Data Sets. This includes, but is not limited to the disclaimer of any implied warranties of merchantability and fitness for a particular purpose of the Platform or of any Data Set. - + + Data User's right to use the name or any trademarks, logos, domain names or other characteristic trademarks whatsoever is limited to the scope defined in these Terms & Conditions. + + If Data User provides feedback, ideas or suggestions, or if Data User replies to surveys in connection with our services (hereinafter referred as the "Feedback"), you acknowledge that the Feedback is not confidential and that you grant us a worldwide, non-exclusive, irrevocable, perpetual, royalty free and unlimited license to use your Feedback in any way, for any purpose, and through any medium or technology now known or unknown, whether in whole or in part, and whether as modified or unmodified. We will always use your Feedback in compliance with those Terms & Conditions, our Privacy Policy and any other applicable laws. + +1. **Termination** + + The Coordinator and CHUV shall have the right to terminate access and use of the Data Sets immediately by written notice upon the Data User's breach of, or non-compliance with, any of the terms of the Terms & Conditions. + +1. **No Warranties and Liability** + + The Data Sets, and any part thereof, as well as the MIP are provided for the Approved Use only. + + The Data User may be held entirely responsible for any misuse that was caused or encouraged by the Data User's failure to abide by the Terms & Conditions. Consequences from failure to abide by the Terms & Conditions may include legal proceedings. + + The access is provided without any warranty and without any obligation of result. + + The MIP and the Data Sets are provided on an "AS IS" and "AS AVAILABLE" basis. Please note that the MIP and the Data Sets may contain bugs, viruses, errors, problems or other limitations. The Coordinator, CHUV and the Beneficiaries assumes no liability for or relating to the delay, failure, interruption, or corruption of any data or other information transmitted in connection with the use of the MIP. + + To the extent permitted by law, the Coordinator, CHUV and the Beneficiaries that participated in the specific action related to the MIP exclude any warranties (whether expressed or implied) for the MIP, the algorithms and the Data Sets. This includes, but is not limited to the disclaimer of any implied warranties of ownership, novelty, patentability, originality, accuracy, non-infringement, merchantability, quality or fitness for a particular purpose of the MIP or of any Data Set. + Data Sets may contain advice, opinions, statements or other information by various authors or entities. Reliance upon any such advice, opinion, statement or other information is at your own risk. - - The Coordinator and the Beneficiaries that participated in the specific action related to the Platform disclaims, to the extent permitted by law, all liability and responsibility arising from any use of the Platform or the Data Sets. In particular, but not as a limitation thereof, the Coordinator is and the Beneficiaries that participated in the specific action related to the Platform are not liable for any damages (including damages for loss of business, loss of profits, litigation, or the like), whether based on breach of contract, breach of warranty, tort (including negligence), product liability or otherwise, even if advised of the possibility of such damages. The acknowledgment of exclusion of liability is an essential condition for the Coordinator and the Beneficiaries that participated in the specific action related to the Platform granting access to the Platform and to the Data Sets. This Platform and its services and/or information are provided to Data Users with these limitations only. - - The Coordinator reserves the right to discontinue at any time, temporarily or permanently, your ability to access the Platform as well as to upload Data Sets and/or access them with or without notice, at its sole discretion and for any reason whatsoever. - -8. Applicable Law and Jurisdiction - - The substantive laws of Switzerland, excluding any conflict of law rules, shall apply to any dispute arising out of the access and use of the Platform and of the Data Sets pursuant to these Terms & Conditions. The ordinary courts of Lausanne, Switzerland, shall have exclusive jurisdiction, subject to appeal, if any. - - Law and jurisdiction applicable to the Data Providers, the Data Users and the Beneficiaries pursuant to the HBP Agreements are expressly reserved. - -9. Contact Us - - In case you have any queries, comments or concerns about these Terms & Conditions, please contact: platform@humanbrainproject.eu \ No newline at end of file + + The Coordinator, CHUV and the Beneficiaries that participated in the specific action related to the MIP disclaims, to the extent permitted by law, all liability and responsibility arising from any use of the MIP or the Data Sets. + + In particular, but not as a limitation thereof, the Coordinator, CHUV and the Beneficiaries that participated in the specific action related to the MIP are not liable for any damages (including damages for loss of business, loss of profits, litigation, or the like), whether based on breach of contract, breach of warranty, tort (including negligence), product liability or otherwise, even if advised of the possibility of such damages. The acknowledgment of exclusion of liability is an essential condition for the Coordinator, CHUV and the Beneficiaries that participated in the specific action related to the MIP granting access to the MIP and to the Data Sets. This MIP and its services and/or information are provided to Data Users with these limitations only. + + To the extent admitted by the applicable law, Data User shall indemnify, defend and hold harmless the Coordinator, CHUV and the Beneficiaries, from and against any claims, actions or demands, losses, liabilities, damages, costs, expenses and settlements (including without limitation reasonable attorney and accounting fees), resulting from or alleged to result from, directly or indirectly, your : (a) violation of these Terms & Conditions; (b) access to or use of the MIP and Websites; and (c) provision of other disclosure of any other information or data and the use of same. + + The Coordinator or CHUV reserves the right to discontinue at any time, temporarily or permanently, your ability to access the MIP as well as to upload Data Sets and/or access them with or without notice, at its sole discretion and for any reason whatsoever. + + The websites and the MIP may contain links to third-party websites. Linked sites are not under the control of the Coordinator, CHUV and the Beneficiaries, and the Coordinator, CHUV and the Beneficiaries are not responsible for the content of any linked site. Links are provided as a convenience only, and a link does not imply that the Coordinator, CHUV and the Beneficiaries endorse, sponsor, or are affiliated with the linked site. Data Users use of third-party websites is at his own risk and subject to the terms and conditions of use for such sites; these Terms & Conditions do not apply to other websites. The Coordinator, CHUV and the Beneficiaries disclaims any and all liability for any information, including but without limitation, any medical and health treatment information set forth on linked sites. + +1. **Applicable Law and Jurisdiction** + + The substantive laws of Switzerland, excluding any conflict of law rules, shall apply to any dispute arising out of the access and use of the Platform and of the Data Sets pursuant to these Terms & Conditions. + + The ordinary courts of Lausanne, Switzerland, shall have exclusive jurisdiction, subject to appeal, if any. + +1. **Contact Us** + + In case you have any queries, comments or concerns about these Terms & Conditions, please contact: [support@ebrains.eu](mailto:support@ebrains.eu) \ No newline at end of file diff --git a/api/ormconfig.ts b/api/ormconfig.ts index 27afa05b4dda41d61eeae90c870720df0fd9fa92..c4d8d9f6ab4d251e3de02e035a54e01ca6d2cf26 100644 --- a/api/ormconfig.ts +++ b/api/ormconfig.ts @@ -7,7 +7,7 @@ ConfigModule.forRoot({ load: [dbConfiguration], }); -const config = { +const ormconfig = { ...dbConfiguration(), entities: ['dist/**/*.entity.js', 'dist/**/*.model.js'], migrations: ['dist/migrations/*{.ts,.js}'], @@ -17,4 +17,4 @@ const config = { }, }; -export default config; +export default ormconfig; diff --git a/api/package-lock.json b/api/package-lock.json index a7e5df0f9910ef00e719ef633aafdd5eda4d332d..74d865159a05949fc6f0951cf37a5f60a21ee877 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -21,11 +21,13 @@ "@nestjs/typeorm": "^8.0.3", "apollo-server-express": "^3.6.3", "axios": "^0.21.1", + "cache-manager": "^4.0.1", "cookie-parser": "^1.4.6", "graphql": "^15.5.3", "graphql-type-json": "^0.3.2", "jsonata": "^1.8.5", "passport": "^0.5.2", + "passport-custom": "^1.1.1", "passport-jwt": "^4.0.0", "passport-local": "^1.0.0", "pg": "^8.7.3", @@ -45,6 +47,7 @@ "@semantic-release/changelog": "^6.0.1", "@semantic-release/git": "^10.0.1", "@semantic-release/gitlab": "^7.0.4", + "@types/cache-manager": "^4.0.0", "@types/cookie-parser": "^1.4.2", "@types/express": "^4.17.13", "@types/jest": "^27.0.1", @@ -145,16 +148,16 @@ } }, "node_modules/@angular-devkit/schematics-cli": { - "version": "13.2.5", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics-cli/-/schematics-cli-13.2.5.tgz", - "integrity": "sha512-/3Q1+wtE+l5XXoXX/7157yh4Wpi+FNEryx5gDcfPJchgtovxj28nzquD0vXnvpyr3Wd8OaMwg6vW4EfL82jRKg==", + "version": "13.3.5", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics-cli/-/schematics-cli-13.3.5.tgz", + "integrity": "sha512-ARX20ebtfwzef8GdXIcB6uv0sjTsaEniZyXBFchEKD6kR5EYZVaBL+ZVUbmsU1d0XY///WzW7pqwCyu5H1u+vw==", "dev": true, "dependencies": { - "@angular-devkit/core": "13.2.5", - "@angular-devkit/schematics": "13.2.5", + "@angular-devkit/core": "13.3.5", + "@angular-devkit/schematics": "13.3.5", "ansi-colors": "4.1.1", "inquirer": "8.2.0", - "minimist": "1.2.5", + "minimist": "1.2.6", "symbol-observable": "4.0.0" }, "bin": { @@ -166,6 +169,75 @@ "yarn": ">= 1.13.0" } }, + "node_modules/@angular-devkit/schematics-cli/node_modules/@angular-devkit/core": { + "version": "13.3.5", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-13.3.5.tgz", + "integrity": "sha512-w7vzK4VoYP9rLgxJ2SwEfrkpKybdD+QgQZlsDBzT0C6Ebp7b4gkNcNVFo8EiZvfDl6Yplw2IAP7g7fs3STn0hQ==", + "dev": true, + "dependencies": { + "ajv": "8.9.0", + "ajv-formats": "2.1.1", + "fast-json-stable-stringify": "2.1.0", + "magic-string": "0.25.7", + "rxjs": "6.6.7", + "source-map": "0.7.3" + }, + "engines": { + "node": "^12.20.0 || ^14.15.0 || >=16.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/@angular-devkit/core/node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dev": true, + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/@angular-devkit/schematics": { + "version": "13.3.5", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-13.3.5.tgz", + "integrity": "sha512-0N/kL/Vfx0yVAEwa3HYxNx9wYb+G9r1JrLjJQQzDp+z9LtcojNf7j3oey6NXrDUs1WjVZOa/AIdRl3/DuaoG5w==", + "dev": true, + "dependencies": { + "@angular-devkit/core": "13.3.5", + "jsonc-parser": "3.0.0", + "magic-string": "0.25.7", + "ora": "5.4.1", + "rxjs": "6.6.7" + }, + "engines": { + "node": "^12.20.0 || ^14.15.0 || >=16.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/@angular-devkit/schematics/node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dev": true, + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, "node_modules/@angular-devkit/schematics-cli/node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -207,6 +279,12 @@ "node": ">=8.0.0" } }, + "node_modules/@angular-devkit/schematics-cli/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, "node_modules/@angular-devkit/schematics/node_modules/rxjs": { "version": "6.6.7", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", @@ -883,6 +961,16 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, "node_modules/@cspotcode/source-map-consumer": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz", @@ -1514,20 +1602,20 @@ } }, "node_modules/@nestjs/cli": { - "version": "8.2.2", - "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-8.2.2.tgz", - "integrity": "sha512-ZonmNLCHfTVrZGgYf4mrpivnKGaRzVRAcux+WDbzhQDNIz70s7mdOPShXW1Vpq+7uRJDxlgO1vOMhmg4uEUIDg==", + "version": "8.2.6", + "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-8.2.6.tgz", + "integrity": "sha512-uvwKbUZJmgdJu1D24e+uUqHnwoB/0R9hLfUJjr5pTvLlP/RJugHAdJr7m1dQe92Xzdyi36kBN4Id3RXHgfz1UA==", "dev": true, "dependencies": { - "@angular-devkit/core": "13.2.5", - "@angular-devkit/schematics": "13.2.5", - "@angular-devkit/schematics-cli": "13.2.5", + "@angular-devkit/core": "13.3.5", + "@angular-devkit/schematics": "13.3.5", + "@angular-devkit/schematics-cli": "13.3.5", "@nestjs/schematics": "^8.0.3", "chalk": "3.0.0", "chokidar": "3.5.3", - "cli-table3": "0.6.1", + "cli-table3": "0.6.2", "commander": "4.1.1", - "fork-ts-checker-webpack-plugin": "7.2.1", + "fork-ts-checker-webpack-plugin": "7.2.11", "inquirer": "7.3.3", "node-emoji": "1.11.0", "ora": "5.4.1", @@ -1536,10 +1624,10 @@ "shelljs": "0.8.5", "source-map-support": "0.5.21", "tree-kill": "1.2.2", - "tsconfig-paths": "3.12.0", + "tsconfig-paths": "3.14.1", "tsconfig-paths-webpack-plugin": "3.5.2", - "typescript": "4.6.2", - "webpack": "5.66.0", + "typescript": "4.6.4", + "webpack": "5.72.1", "webpack-node-externals": "3.0.0" }, "bin": { @@ -1550,162 +1638,69 @@ "npm": ">= 6.11.0" } }, - "node_modules/@nestjs/cli/node_modules/@types/estree": { - "version": "0.0.50", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.50.tgz", - "integrity": "sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw==", - "dev": true - }, - "node_modules/@nestjs/cli/node_modules/acorn": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz", - "integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==", + "node_modules/@nestjs/cli/node_modules/@angular-devkit/core": { + "version": "13.3.5", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-13.3.5.tgz", + "integrity": "sha512-w7vzK4VoYP9rLgxJ2SwEfrkpKybdD+QgQZlsDBzT0C6Ebp7b4gkNcNVFo8EiZvfDl6Yplw2IAP7g7fs3STn0hQ==", "dev": true, - "bin": { - "acorn": "bin/acorn" + "dependencies": { + "ajv": "8.9.0", + "ajv-formats": "2.1.1", + "fast-json-stable-stringify": "2.1.0", + "magic-string": "0.25.7", + "rxjs": "6.6.7", + "source-map": "0.7.3" }, "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/@nestjs/cli/node_modules/acorn-import-assertions": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz", - "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==", - "dev": true, - "peerDependencies": { - "acorn": "^8" - } - }, - "node_modules/@nestjs/cli/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "node": "^12.20.0 || ^14.15.0 || >=16.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/@nestjs/cli/node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, "peerDependencies": { - "ajv": "^6.9.1" - } - }, - "node_modules/@nestjs/cli/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "node_modules/@nestjs/cli/node_modules/json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", - "dev": true, - "dependencies": { - "minimist": "^1.2.0" + "chokidar": "^3.5.2" }, - "bin": { - "json5": "lib/cli.js" + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } } }, - "node_modules/@nestjs/cli/node_modules/schema-utils": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", - "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", + "node_modules/@nestjs/cli/node_modules/@angular-devkit/schematics": { + "version": "13.3.5", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-13.3.5.tgz", + "integrity": "sha512-0N/kL/Vfx0yVAEwa3HYxNx9wYb+G9r1JrLjJQQzDp+z9LtcojNf7j3oey6NXrDUs1WjVZOa/AIdRl3/DuaoG5w==", "dev": true, "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" + "@angular-devkit/core": "13.3.5", + "jsonc-parser": "3.0.0", + "magic-string": "0.25.7", + "ora": "5.4.1", + "rxjs": "6.6.7" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/@nestjs/cli/node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", - "dev": true, "engines": { - "node": ">=4" - } - }, - "node_modules/@nestjs/cli/node_modules/tsconfig-paths": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.12.0.tgz", - "integrity": "sha512-e5adrnOYT6zqVnWqZu7i/BQ3BnhzvGbjEjejFXO20lKIKpwTaupkCPgEfv4GZK1IBciJUEhYs3J3p75FdaTFVg==", - "dev": true, - "dependencies": { - "@types/json5": "^0.0.29", - "json5": "^1.0.1", - "minimist": "^1.2.0", - "strip-bom": "^3.0.0" + "node": "^12.20.0 || ^14.15.0 || >=16.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" } }, - "node_modules/@nestjs/cli/node_modules/webpack": { - "version": "5.66.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.66.0.tgz", - "integrity": "sha512-NJNtGT7IKpGzdW7Iwpn/09OXz9inIkeIQ/ibY6B+MdV1x6+uReqz/5z1L89ezWnpPDWpXF0TY5PCYKQdWVn8Vg==", + "node_modules/@nestjs/cli/node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", "dev": true, "dependencies": { - "@types/eslint-scope": "^3.7.0", - "@types/estree": "^0.0.50", - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/wasm-edit": "1.11.1", - "@webassemblyjs/wasm-parser": "1.11.1", - "acorn": "^8.4.1", - "acorn-import-assertions": "^1.7.6", - "browserslist": "^4.14.5", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.8.3", - "es-module-lexer": "^0.9.0", - "eslint-scope": "5.1.1", - "events": "^3.2.0", - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.9", - "json-parse-better-errors": "^1.0.2", - "loader-runner": "^4.2.0", - "mime-types": "^2.1.27", - "neo-async": "^2.6.2", - "schema-utils": "^3.1.0", - "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.1.3", - "watchpack": "^2.3.1", - "webpack-sources": "^3.2.2" - }, - "bin": { - "webpack": "bin/webpack.js" + "tslib": "^1.9.0" }, "engines": { - "node": ">=10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependenciesMeta": { - "webpack-cli": { - "optional": true - } + "npm": ">=2.0.0" } }, + "node_modules/@nestjs/cli/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, "node_modules/@nestjs/common": { "version": "8.4.0", "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-8.4.0.tgz", @@ -2580,6 +2575,12 @@ "@types/node": "*" } }, + "node_modules/@types/cache-manager": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/cache-manager/-/cache-manager-4.0.0.tgz", + "integrity": "sha512-uGnPOCM3PtlqZagds3i8mNyEwKLgZpKgswqmlF2ahmh4D1TN1aLYxYez2PDFDy42IGwLTbuHWSiF62I2jouM7g==", + "dev": true + }, "node_modules/@types/cacheable-request": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.2.tgz", @@ -3335,18 +3336,6 @@ } } }, - "node_modules/ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } - }, "node_modules/ansi-colors": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", @@ -3656,6 +3645,11 @@ "node": ">=8" } }, + "node_modules/async": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz", + "integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==" + }, "node_modules/async-retry": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", @@ -4045,6 +4039,24 @@ "node": ">= 0.8" } }, + "node_modules/cache-manager": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/cache-manager/-/cache-manager-4.0.1.tgz", + "integrity": "sha512-JWdtjdX8e0e6eMehAZsdJvBMvHn/pVQGYUjgzc1ILFH0vtcffb9R7XIEAqfYgEeaVJVCOSP4+dxCius+ciW0RA==", + "dependencies": { + "async": "3.2.3", + "lodash.clonedeep": "^4.5.0", + "lru-cache": "^7.10.1" + } + }, + "node_modules/cache-manager/node_modules/lru-cache": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.10.1.tgz", + "integrity": "sha512-BQuhQxPuRl79J5zSXRP+uNzPOyZw2oFI9JLRQ80XswSvg21KMKNtQza9eF42rfI/3Z40RvzBdXgziEkudzjo8A==", + "engines": { + "node": ">=12" + } + }, "node_modules/cacheable-lookup": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", @@ -4340,9 +4352,9 @@ } }, "node_modules/cli-table3": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.1.tgz", - "integrity": "sha512-w0q/enDHhPLq44ovMGdQeeDLvwxwavsJX7oQGYt/LrBlYsyaxyDnp6z3QzFut/6kLLKnlcUVJLrpB7KBfgG/RA==", + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.2.tgz", + "integrity": "sha512-QyavHCaIC80cMivimWu4aWHilIpiDpfm3hGmqAmXVL1UsnbLuBSMd21hTX6VY4ZSDSM73ESLeF8TOYId3rBTbw==", "dev": true, "dependencies": { "string-width": "^4.2.0" @@ -4351,7 +4363,7 @@ "node": "10.* || >= 12.*" }, "optionalDependencies": { - "colors": "1.4.0" + "@colors/colors": "1.5.0" } }, "node_modules/cli-width": { @@ -4423,16 +4435,6 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, - "node_modules/colors": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", - "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", - "dev": true, - "optional": true, - "engines": { - "node": ">=0.1.90" - } - }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -5272,9 +5274,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.9.2", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.9.2.tgz", - "integrity": "sha512-GIm3fQfwLJ8YZx2smuHpBKkXC1yOk+OBEmKckVyL0i/ea8mqDEykK3ld5dgH1QYPNyT/lIllxV2LULnxCHaHkA==", + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.9.3.tgz", + "integrity": "sha512-Bq9VSor+kjvW3f9/MiiR4eE3XYgOl7/rS8lnSxbRbF3kS0B2r+Y9w5krBWxZgDxASVZbdYrn5wT4j/Wb0J9qow==", "dev": true, "dependencies": { "graceful-fs": "^4.2.4", @@ -6105,9 +6107,9 @@ } }, "node_modules/fork-ts-checker-webpack-plugin": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-7.2.1.tgz", - "integrity": "sha512-uOfQdg/iQ8iokQ64qcbu8iZb114rOmaKLQFu7hU14/eJaKgsP91cQ7ts7v2iiDld6TzDe84Meksha8/MkWiCyw==", + "version": "7.2.11", + "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-7.2.11.tgz", + "integrity": "sha512-2e5+NyTUTE1Xq4fWo7KFEQblCaIvvINQwUX3jRmEGlgCTc1Ecqw/975EfQrQ0GEraxJTnp8KB9d/c8hlCHUMJA==", "dev": true, "dependencies": { "@babel/code-frame": "^7.16.7", @@ -6118,7 +6120,7 @@ "fs-extra": "^10.0.0", "memfs": "^3.4.1", "minimatch": "^3.0.4", - "schema-utils": "4.0.0", + "schema-utils": "^3.1.1", "semver": "^7.3.5", "tapable": "^2.2.1" }, @@ -8622,6 +8624,11 @@ "integrity": "sha1-+CbJtOKoUR2E46yinbBeGk87cqk=", "dev": true }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==" + }, "node_modules/lodash.escaperegexp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", @@ -8924,9 +8931,9 @@ } }, "node_modules/memfs": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.4.1.tgz", - "integrity": "sha512-1c9VPVvW5P7I85c35zAdEr1TD5+F11IToIHIlrVIcflfnzPkJa0ZoYEoEdYDP8KgPFoSZ/opDrUsAoZWym3mtw==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.4.3.tgz", + "integrity": "sha512-eivjfi7Ahr6eQTn44nvTnR60e4a1Fs1Via2kCR5lHo/kyNoiMWaXCNJ/GpSd0ilXas2JSOl9B5FTIhflXu0hlg==", "dev": true, "dependencies": { "fs-monkey": "1.0.3" @@ -9081,9 +9088,9 @@ } }, "node_modules/minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" }, "node_modules/minimist-options": { "version": "4.1.0", @@ -12096,10 +12103,21 @@ "url": "https://github.com/sponsors/jaredhanson" } }, - "node_modules/passport-jwt": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.0.tgz", - "integrity": "sha512-BwC0n2GP/1hMVjR4QpnvqA61TxenUMlmfNjYNgK0ZAs0HK4SOQkHcSv4L328blNTLtHq7DbmvyNJiH+bn6C5Mg==", + "node_modules/passport-custom": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/passport-custom/-/passport-custom-1.1.1.tgz", + "integrity": "sha512-/2m7jUGxmCYvoqenLB9UrmkCgPt64h8ZtV+UtuQklZ/Tn1NpKBeOorCYkB/8lMRoiZ5hUrCoMmDtxCS/d38mlg==", + "dependencies": { + "passport-strategy": "1.x.x" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/passport-jwt": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.0.tgz", + "integrity": "sha512-BwC0n2GP/1hMVjR4QpnvqA61TxenUMlmfNjYNgK0ZAs0HK4SOQkHcSv4L328blNTLtHq7DbmvyNJiH+bn6C5Mg==", "dependencies": { "jsonwebtoken": "^8.2.0", "passport-strategy": "^1.0.0" @@ -13066,24 +13084,54 @@ } }, "node_modules/schema-utils": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz", - "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", + "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", "dev": true, "dependencies": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.8.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.0.0" + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" }, "engines": { - "node": ">= 12.13.0" + "node": ">= 10.13.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/webpack" } }, + "node_modules/schema-utils/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/schema-utils/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/schema-utils/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, "node_modules/semantic-release": { "version": "19.0.2", "resolved": "https://registry.npmjs.org/semantic-release/-/semantic-release-19.0.2.tgz", @@ -14092,55 +14140,6 @@ } } }, - "node_modules/terser-webpack-plugin/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/terser-webpack-plugin/node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "peerDependencies": { - "ajv": "^6.9.1" - } - }, - "node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "node_modules/terser-webpack-plugin/node_modules/schema-utils": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", - "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", - "dev": true, - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, "node_modules/terser-webpack-plugin/node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -14491,14 +14490,14 @@ } }, "node_modules/tsconfig-paths": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.13.0.tgz", - "integrity": "sha512-nWuffZppoaYK0vQ1SQmkSsQzJoHA4s6uzdb2waRpD806x9yfq153AdVsWz4je2qZcW+pENrMQXbGQ3sMCkXuhw==", + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz", + "integrity": "sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ==", "dev": true, "dependencies": { "@types/json5": "^0.0.29", "json5": "^1.0.1", - "minimist": "^1.2.0", + "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, @@ -14834,9 +14833,9 @@ } }, "node_modules/typescript": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.2.tgz", - "integrity": "sha512-HM/hFigTBHZhLXshn9sN37H085+hQGeJHJ/X7LpBWLID/fbc2acUMfU+lGD98X81sKP+pFa9f0DZmCwB9GnbAg==", + "version": "4.6.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.4.tgz", + "integrity": "sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -15054,11 +15053,10 @@ } }, "node_modules/webpack": { - "version": "5.70.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.70.0.tgz", - "integrity": "sha512-ZMWWy8CeuTTjCxbeaQI21xSswseF2oNOwc70QSKNePvmxE7XW36i7vpBMYZFAUHPwQiEbNGCEYIOOlyRbdGmxw==", + "version": "5.72.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.72.1.tgz", + "integrity": "sha512-dXG5zXCLspQR4krZVR6QgajnZOjW2K/djHvdcRaDQvsjV9z9vaW6+ja5dZOYbqBBjF6kGXka/2ZyxNdc+8Jung==", "dev": true, - "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.3", "@types/estree": "^0.0.51", @@ -15069,13 +15067,13 @@ "acorn-import-assertions": "^1.7.6", "browserslist": "^4.14.5", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.9.2", + "enhanced-resolve": "^5.9.3", "es-module-lexer": "^0.9.0", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.2.9", - "json-parse-better-errors": "^1.0.2", + "json-parse-even-better-errors": "^2.3.1", "loader-runner": "^4.2.0", "mime-types": "^2.1.27", "neo-async": "^2.6.2", @@ -15124,7 +15122,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz", "integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==", "dev": true, - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -15137,64 +15134,10 @@ "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz", "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==", "dev": true, - "peer": true, "peerDependencies": { "acorn": "^8" } }, - "node_modules/webpack/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "peer": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/webpack/node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "peer": true, - "peerDependencies": { - "ajv": "^6.9.1" - } - }, - "node_modules/webpack/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "peer": true - }, - "node_modules/webpack/node_modules/schema-utils": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", - "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", - "dev": true, - "peer": true, - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, "node_modules/whatwg-encoding": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", @@ -15572,19 +15515,68 @@ } }, "@angular-devkit/schematics-cli": { - "version": "13.2.5", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics-cli/-/schematics-cli-13.2.5.tgz", - "integrity": "sha512-/3Q1+wtE+l5XXoXX/7157yh4Wpi+FNEryx5gDcfPJchgtovxj28nzquD0vXnvpyr3Wd8OaMwg6vW4EfL82jRKg==", + "version": "13.3.5", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics-cli/-/schematics-cli-13.3.5.tgz", + "integrity": "sha512-ARX20ebtfwzef8GdXIcB6uv0sjTsaEniZyXBFchEKD6kR5EYZVaBL+ZVUbmsU1d0XY///WzW7pqwCyu5H1u+vw==", "dev": true, "requires": { - "@angular-devkit/core": "13.2.5", - "@angular-devkit/schematics": "13.2.5", + "@angular-devkit/core": "13.3.5", + "@angular-devkit/schematics": "13.3.5", "ansi-colors": "4.1.1", "inquirer": "8.2.0", - "minimist": "1.2.5", + "minimist": "1.2.6", "symbol-observable": "4.0.0" }, "dependencies": { + "@angular-devkit/core": { + "version": "13.3.5", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-13.3.5.tgz", + "integrity": "sha512-w7vzK4VoYP9rLgxJ2SwEfrkpKybdD+QgQZlsDBzT0C6Ebp7b4gkNcNVFo8EiZvfDl6Yplw2IAP7g7fs3STn0hQ==", + "dev": true, + "requires": { + "ajv": "8.9.0", + "ajv-formats": "2.1.1", + "fast-json-stable-stringify": "2.1.0", + "magic-string": "0.25.7", + "rxjs": "6.6.7", + "source-map": "0.7.3" + }, + "dependencies": { + "rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + } + } + }, + "@angular-devkit/schematics": { + "version": "13.3.5", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-13.3.5.tgz", + "integrity": "sha512-0N/kL/Vfx0yVAEwa3HYxNx9wYb+G9r1JrLjJQQzDp+z9LtcojNf7j3oey6NXrDUs1WjVZOa/AIdRl3/DuaoG5w==", + "dev": true, + "requires": { + "@angular-devkit/core": "13.3.5", + "jsonc-parser": "3.0.0", + "magic-string": "0.25.7", + "ora": "5.4.1", + "rxjs": "6.6.7" + }, + "dependencies": { + "rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + } + } + }, "chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -15616,6 +15608,12 @@ "strip-ansi": "^6.0.0", "through": "^2.3.6" } + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true } } }, @@ -16128,6 +16126,13 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "optional": true + }, "@cspotcode/source-map-consumer": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz", @@ -16612,20 +16617,20 @@ } }, "@nestjs/cli": { - "version": "8.2.2", - "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-8.2.2.tgz", - "integrity": "sha512-ZonmNLCHfTVrZGgYf4mrpivnKGaRzVRAcux+WDbzhQDNIz70s7mdOPShXW1Vpq+7uRJDxlgO1vOMhmg4uEUIDg==", + "version": "8.2.6", + "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-8.2.6.tgz", + "integrity": "sha512-uvwKbUZJmgdJu1D24e+uUqHnwoB/0R9hLfUJjr5pTvLlP/RJugHAdJr7m1dQe92Xzdyi36kBN4Id3RXHgfz1UA==", "dev": true, "requires": { - "@angular-devkit/core": "13.2.5", - "@angular-devkit/schematics": "13.2.5", - "@angular-devkit/schematics-cli": "13.2.5", + "@angular-devkit/core": "13.3.5", + "@angular-devkit/schematics": "13.3.5", + "@angular-devkit/schematics-cli": "13.3.5", "@nestjs/schematics": "^8.0.3", "chalk": "3.0.0", "chokidar": "3.5.3", - "cli-table3": "0.6.1", + "cli-table3": "0.6.2", "commander": "4.1.1", - "fork-ts-checker-webpack-plugin": "7.2.1", + "fork-ts-checker-webpack-plugin": "7.2.11", "inquirer": "7.3.3", "node-emoji": "1.11.0", "ora": "5.4.1", @@ -16634,126 +16639,54 @@ "shelljs": "0.8.5", "source-map-support": "0.5.21", "tree-kill": "1.2.2", - "tsconfig-paths": "3.12.0", + "tsconfig-paths": "3.14.1", "tsconfig-paths-webpack-plugin": "3.5.2", - "typescript": "4.6.2", - "webpack": "5.66.0", + "typescript": "4.6.4", + "webpack": "5.72.1", "webpack-node-externals": "3.0.0" }, "dependencies": { - "@types/estree": { - "version": "0.0.50", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.50.tgz", - "integrity": "sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw==", - "dev": true - }, - "acorn": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz", - "integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==", - "dev": true - }, - "acorn-import-assertions": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz", - "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==", - "dev": true, - "requires": {} - }, - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "@angular-devkit/core": { + "version": "13.3.5", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-13.3.5.tgz", + "integrity": "sha512-w7vzK4VoYP9rLgxJ2SwEfrkpKybdD+QgQZlsDBzT0C6Ebp7b4gkNcNVFo8EiZvfDl6Yplw2IAP7g7fs3STn0hQ==", "dev": true, "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "ajv": "8.9.0", + "ajv-formats": "2.1.1", + "fast-json-stable-stringify": "2.1.0", + "magic-string": "0.25.7", + "rxjs": "6.6.7", + "source-map": "0.7.3" } }, - "ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "requires": {} - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "@angular-devkit/schematics": { + "version": "13.3.5", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-13.3.5.tgz", + "integrity": "sha512-0N/kL/Vfx0yVAEwa3HYxNx9wYb+G9r1JrLjJQQzDp+z9LtcojNf7j3oey6NXrDUs1WjVZOa/AIdRl3/DuaoG5w==", "dev": true, "requires": { - "minimist": "^1.2.0" + "@angular-devkit/core": "13.3.5", + "jsonc-parser": "3.0.0", + "magic-string": "0.25.7", + "ora": "5.4.1", + "rxjs": "6.6.7" } }, - "schema-utils": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", - "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", + "rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", "dev": true, "requires": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" + "tslib": "^1.9.0" } }, - "strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "dev": true - }, - "tsconfig-paths": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.12.0.tgz", - "integrity": "sha512-e5adrnOYT6zqVnWqZu7i/BQ3BnhzvGbjEjejFXO20lKIKpwTaupkCPgEfv4GZK1IBciJUEhYs3J3p75FdaTFVg==", - "dev": true, - "requires": { - "@types/json5": "^0.0.29", - "json5": "^1.0.1", - "minimist": "^1.2.0", - "strip-bom": "^3.0.0" - } - }, - "webpack": { - "version": "5.66.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.66.0.tgz", - "integrity": "sha512-NJNtGT7IKpGzdW7Iwpn/09OXz9inIkeIQ/ibY6B+MdV1x6+uReqz/5z1L89ezWnpPDWpXF0TY5PCYKQdWVn8Vg==", - "dev": true, - "requires": { - "@types/eslint-scope": "^3.7.0", - "@types/estree": "^0.0.50", - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/wasm-edit": "1.11.1", - "@webassemblyjs/wasm-parser": "1.11.1", - "acorn": "^8.4.1", - "acorn-import-assertions": "^1.7.6", - "browserslist": "^4.14.5", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.8.3", - "es-module-lexer": "^0.9.0", - "eslint-scope": "5.1.1", - "events": "^3.2.0", - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.9", - "json-parse-better-errors": "^1.0.2", - "loader-runner": "^4.2.0", - "mime-types": "^2.1.27", - "neo-async": "^2.6.2", - "schema-utils": "^3.1.0", - "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.1.3", - "watchpack": "^2.3.1", - "webpack-sources": "^3.2.2" - } } } }, @@ -17421,6 +17354,12 @@ "@types/node": "*" } }, + "@types/cache-manager": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/cache-manager/-/cache-manager-4.0.0.tgz", + "integrity": "sha512-uGnPOCM3PtlqZagds3i8mNyEwKLgZpKgswqmlF2ahmh4D1TN1aLYxYez2PDFDy42IGwLTbuHWSiF62I2jouM7g==", + "dev": true + }, "@types/cacheable-request": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.2.tgz", @@ -18070,15 +18009,6 @@ "ajv": "^8.0.0" } }, - "ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.3" - } - }, "ansi-colors": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", @@ -18312,6 +18242,11 @@ "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", "dev": true }, + "async": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz", + "integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==" + }, "async-retry": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", @@ -18618,6 +18553,23 @@ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" }, + "cache-manager": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/cache-manager/-/cache-manager-4.0.1.tgz", + "integrity": "sha512-JWdtjdX8e0e6eMehAZsdJvBMvHn/pVQGYUjgzc1ILFH0vtcffb9R7XIEAqfYgEeaVJVCOSP4+dxCius+ciW0RA==", + "requires": { + "async": "3.2.3", + "lodash.clonedeep": "^4.5.0", + "lru-cache": "^7.10.1" + }, + "dependencies": { + "lru-cache": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.10.1.tgz", + "integrity": "sha512-BQuhQxPuRl79J5zSXRP+uNzPOyZw2oFI9JLRQ80XswSvg21KMKNtQza9eF42rfI/3Z40RvzBdXgziEkudzjo8A==" + } + } + }, "cacheable-lookup": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", @@ -18837,12 +18789,12 @@ "dev": true }, "cli-table3": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.1.tgz", - "integrity": "sha512-w0q/enDHhPLq44ovMGdQeeDLvwxwavsJX7oQGYt/LrBlYsyaxyDnp6z3QzFut/6kLLKnlcUVJLrpB7KBfgG/RA==", + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.2.tgz", + "integrity": "sha512-QyavHCaIC80cMivimWu4aWHilIpiDpfm3hGmqAmXVL1UsnbLuBSMd21hTX6VY4ZSDSM73ESLeF8TOYId3rBTbw==", "dev": true, "requires": { - "colors": "1.4.0", + "@colors/colors": "1.5.0", "string-width": "^4.2.0" } }, @@ -18902,13 +18854,6 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, - "colors": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", - "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", - "dev": true, - "optional": true - }, "combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -19593,9 +19538,9 @@ } }, "enhanced-resolve": { - "version": "5.9.2", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.9.2.tgz", - "integrity": "sha512-GIm3fQfwLJ8YZx2smuHpBKkXC1yOk+OBEmKckVyL0i/ea8mqDEykK3ld5dgH1QYPNyT/lIllxV2LULnxCHaHkA==", + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.9.3.tgz", + "integrity": "sha512-Bq9VSor+kjvW3f9/MiiR4eE3XYgOl7/rS8lnSxbRbF3kS0B2r+Y9w5krBWxZgDxASVZbdYrn5wT4j/Wb0J9qow==", "dev": true, "requires": { "graceful-fs": "^4.2.4", @@ -20225,9 +20170,9 @@ "integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==" }, "fork-ts-checker-webpack-plugin": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-7.2.1.tgz", - "integrity": "sha512-uOfQdg/iQ8iokQ64qcbu8iZb114rOmaKLQFu7hU14/eJaKgsP91cQ7ts7v2iiDld6TzDe84Meksha8/MkWiCyw==", + "version": "7.2.11", + "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-7.2.11.tgz", + "integrity": "sha512-2e5+NyTUTE1Xq4fWo7KFEQblCaIvvINQwUX3jRmEGlgCTc1Ecqw/975EfQrQ0GEraxJTnp8KB9d/c8hlCHUMJA==", "dev": true, "requires": { "@babel/code-frame": "^7.16.7", @@ -20238,7 +20183,7 @@ "fs-extra": "^10.0.0", "memfs": "^3.4.1", "minimatch": "^3.0.4", - "schema-utils": "4.0.0", + "schema-utils": "^3.1.1", "semver": "^7.3.5", "tapable": "^2.2.1" }, @@ -22122,6 +22067,11 @@ "integrity": "sha1-+CbJtOKoUR2E46yinbBeGk87cqk=", "dev": true }, + "lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==" + }, "lodash.escaperegexp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", @@ -22351,9 +22301,9 @@ "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" }, "memfs": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.4.1.tgz", - "integrity": "sha512-1c9VPVvW5P7I85c35zAdEr1TD5+F11IToIHIlrVIcflfnzPkJa0ZoYEoEdYDP8KgPFoSZ/opDrUsAoZWym3mtw==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.4.3.tgz", + "integrity": "sha512-eivjfi7Ahr6eQTn44nvTnR60e4a1Fs1Via2kCR5lHo/kyNoiMWaXCNJ/GpSd0ilXas2JSOl9B5FTIhflXu0hlg==", "dev": true, "requires": { "fs-monkey": "1.0.3" @@ -22462,9 +22412,9 @@ } }, "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" }, "minimist-options": { "version": "4.1.0", @@ -24622,6 +24572,14 @@ "pause": "0.0.1" } }, + "passport-custom": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/passport-custom/-/passport-custom-1.1.1.tgz", + "integrity": "sha512-/2m7jUGxmCYvoqenLB9UrmkCgPt64h8ZtV+UtuQklZ/Tn1NpKBeOorCYkB/8lMRoiZ5hUrCoMmDtxCS/d38mlg==", + "requires": { + "passport-strategy": "1.x.x" + } + }, "passport-jwt": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.0.tgz", @@ -25321,15 +25279,41 @@ } }, "schema-utils": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz", - "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", + "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", "dev": true, "requires": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.8.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.0.0" + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "requires": {} + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + } } }, "semantic-release": { @@ -26121,42 +26105,6 @@ "terser": "^5.7.2" }, "dependencies": { - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "requires": {} - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "schema-utils": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", - "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", - "dev": true, - "requires": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - } - }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -26386,14 +26334,14 @@ } }, "tsconfig-paths": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.13.0.tgz", - "integrity": "sha512-nWuffZppoaYK0vQ1SQmkSsQzJoHA4s6uzdb2waRpD806x9yfq153AdVsWz4je2qZcW+pENrMQXbGQ3sMCkXuhw==", + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz", + "integrity": "sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ==", "dev": true, "requires": { "@types/json5": "^0.0.29", "json5": "^1.0.1", - "minimist": "^1.2.0", + "minimist": "^1.2.6", "strip-bom": "^3.0.0" }, "dependencies": { @@ -26596,9 +26544,9 @@ } }, "typescript": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.2.tgz", - "integrity": "sha512-HM/hFigTBHZhLXshn9sN37H085+hQGeJHJ/X7LpBWLID/fbc2acUMfU+lGD98X81sKP+pFa9f0DZmCwB9GnbAg==", + "version": "4.6.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.4.tgz", + "integrity": "sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg==", "dev": true }, "uglify-js": { @@ -26767,11 +26715,10 @@ "dev": true }, "webpack": { - "version": "5.70.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.70.0.tgz", - "integrity": "sha512-ZMWWy8CeuTTjCxbeaQI21xSswseF2oNOwc70QSKNePvmxE7XW36i7vpBMYZFAUHPwQiEbNGCEYIOOlyRbdGmxw==", + "version": "5.72.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.72.1.tgz", + "integrity": "sha512-dXG5zXCLspQR4krZVR6QgajnZOjW2K/djHvdcRaDQvsjV9z9vaW6+ja5dZOYbqBBjF6kGXka/2ZyxNdc+8Jung==", "dev": true, - "peer": true, "requires": { "@types/eslint-scope": "^3.7.3", "@types/estree": "^0.0.51", @@ -26782,13 +26729,13 @@ "acorn-import-assertions": "^1.7.6", "browserslist": "^4.14.5", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.9.2", + "enhanced-resolve": "^5.9.3", "es-module-lexer": "^0.9.0", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.2.9", - "json-parse-better-errors": "^1.0.2", + "json-parse-even-better-errors": "^2.3.1", "loader-runner": "^4.2.0", "mime-types": "^2.1.27", "neo-async": "^2.6.2", @@ -26803,56 +26750,14 @@ "version": "8.7.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz", "integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==", - "dev": true, - "peer": true + "dev": true }, "acorn-import-assertions": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz", "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==", "dev": true, - "peer": true, "requires": {} - }, - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "peer": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "peer": true, - "requires": {} - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "peer": true - }, - "schema-utils": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", - "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", - "dev": true, - "peer": true, - "requires": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - } } } }, diff --git a/api/package.json b/api/package.json index 481ac4aeefe16a08e350d7df20fbce7e2996b9f9..41ac9d65ba74232143c5a2d9db79e93ef7ba5e2d 100644 --- a/api/package.json +++ b/api/package.json @@ -14,7 +14,7 @@ "start:debug": "nest start --debug --watch", "start:prod": "node dist/main", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", - "test": "jest", + "test": "jest --verbose", "test:watch": "jest --watch", "test:cov": "jest --coverage", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", @@ -38,11 +38,13 @@ "@nestjs/typeorm": "^8.0.3", "apollo-server-express": "^3.6.3", "axios": "^0.21.1", + "cache-manager": "^4.0.1", "cookie-parser": "^1.4.6", "graphql": "^15.5.3", "graphql-type-json": "^0.3.2", "jsonata": "^1.8.5", "passport": "^0.5.2", + "passport-custom": "^1.1.1", "passport-jwt": "^4.0.0", "passport-local": "^1.0.0", "pg": "^8.7.3", @@ -62,6 +64,7 @@ "@semantic-release/changelog": "^6.0.1", "@semantic-release/git": "^10.0.1", "@semantic-release/gitlab": "^7.0.4", + "@types/cache-manager": "^4.0.0", "@types/cookie-parser": "^1.4.2", "@types/express": "^4.17.13", "@types/jest": "^27.0.1", @@ -98,12 +101,12 @@ "^.+\\.(t|j)s$": "ts-jest" }, "collectCoverageFrom": [ - "**/*.(t|j)s", - "!**/*e2e-spec.ts", - "!**/*.module.ts", + "**/*.(t|j)s", + "!**/*e2e-spec.ts", + "!**/*.module.ts", "!**/*.decorator.ts", - "!**/*.model.ts", - "!**/*.input.ts", + "!**/*.model.ts", + "!**/*.input.ts", "!**/jest.config.ts", "!**/main.ts" ], diff --git a/api/src/auth/auth.module.ts b/api/src/auth/auth.module.ts index 805f0dcba28a75eee88f5deaff33cc6ff5c31ad5..936e09cab00920b1f9d4494200b10cce62fb8b06 100644 --- a/api/src/auth/auth.module.ts +++ b/api/src/auth/auth.module.ts @@ -5,6 +5,7 @@ import { PassportModule } from '@nestjs/passport'; import { authConstants } from './auth-constants'; import { AuthResolver } from './auth.resolver'; import { AuthService } from './auth.service'; +import { EngineStrategy } from './strategies/engine.strategy'; import { JwtBearerStrategy } from './strategies/jwt-bearer.strategy'; import { JwtCookiesStrategy } from './strategies/jwt-cookies.strategy'; import { LocalStrategy } from './strategies/local.strategy'; @@ -30,6 +31,7 @@ import { LocalStrategy } from './strategies/local.strategy'; LocalStrategy, JwtBearerStrategy, JwtCookiesStrategy, + EngineStrategy, AuthResolver, ], exports: [AuthService], diff --git a/api/src/auth/auth.resolver.spec.ts b/api/src/auth/auth.resolver.spec.ts index c109dbe8712230015317033214cd8f2870350c7e..7890a9e5bc8f52774599a6a66a14a9a54ee75a04 100644 --- a/api/src/auth/auth.resolver.spec.ts +++ b/api/src/auth/auth.resolver.spec.ts @@ -1,11 +1,9 @@ import { getMockRes } from '@jest-mock/express'; import { Test, TestingModule } from '@nestjs/testing'; import { MockFunctionMetadata, ModuleMocker } from 'jest-mock'; -import LocalService from '../engine/connectors/local/main.connector'; -import { - ENGINE_MODULE_OPTIONS, - ENGINE_SERVICE, -} from '../engine/engine.constants'; +import EngineService from '../engine/engine.service'; +import LocalService from '../engine/connectors/local/local.connector'; +import { ENGINE_MODULE_OPTIONS } from '../engine/engine.constants'; import { User } from '../users/models/user.model'; import { authConstants } from './auth-constants'; import { AuthResolver } from './auth.resolver'; @@ -40,7 +38,7 @@ describe('AuthResolver', () => { const module: TestingModule = await Test.createTestingModule({ providers: [ { - provide: ENGINE_SERVICE, + provide: EngineService, useClass: LocalService, }, { @@ -79,9 +77,9 @@ describe('AuthResolver', () => { expect(data.accessToken).toBe(authData.accessToken); }); - it('logout', () => { + it('logout', async () => { const request: any = jest.fn(); - resolver.logout(request, res, user); + await resolver.logout(request, res, user); expect(mockClearCookie.mock.calls[0][0]).toBe(authConstants.cookie.name); }); diff --git a/api/src/auth/auth.resolver.ts b/api/src/auth/auth.resolver.ts index cfa8e714c43004d503eaf016e2f00a11503990d7..cf3208a99b76554b383a8dd3c181805f38d9ce06 100644 --- a/api/src/auth/auth.resolver.ts +++ b/api/src/auth/auth.resolver.ts @@ -7,22 +7,20 @@ import { import { ConfigService } from '@nestjs/config'; import { Args, Mutation, Resolver } from '@nestjs/graphql'; import { Response, Request } from 'express'; +import { CurrentUser } from '../common/decorators/user.decorator'; import { GQLRequest } from '../common/decorators/gql-request.decoractor'; import { GQLResponse } from '../common/decorators/gql-response.decoractor'; -import { parseToBoolean } from '../common/utilities'; -import { - ENGINE_MODULE_OPTIONS, - ENGINE_SERVICE, -} from '../engine/engine.constants'; -import { IEngineOptions, IEngineService } from '../engine/engine.interfaces'; +import { ENGINE_MODULE_OPTIONS } from '../engine/engine.constants'; import { User } from '../users/models/user.model'; import { authConstants } from './auth-constants'; import { AuthService } from './auth.service'; -import { CurrentUser } from './decorators/user.decorator'; -import { JwtAuthGuard } from './guards/jwt-auth.guard'; +import { GlobalAuthGuard } from './guards/global-auth.guard'; import { LocalAuthGuard } from './guards/local-auth.guard'; import { AuthenticationInput } from './inputs/authentication.input'; import { AuthenticationOutput } from './outputs/authentication.output'; +import { parseToBoolean } from '../common/utils/shared.utils'; +import EngineOptions from '../engine/interfaces/engine-options.interface'; +import EngineService from '../engine/engine.service'; //Custom defined type because Pick<CookieOptions, 'sameSite'> does not work type SameSiteType = boolean | 'lax' | 'strict' | 'none' | undefined; @@ -32,9 +30,9 @@ export class AuthResolver { private readonly logger = new Logger(AuthResolver.name); constructor( - @Inject(ENGINE_SERVICE) private readonly engineService: IEngineService, + private readonly engineService: EngineService, @Inject(ENGINE_MODULE_OPTIONS) - private readonly engineOptions: IEngineOptions, + private readonly engineOptions: EngineOptions, private readonly authService: AuthService, private readonly configService: ConfigService, ) {} @@ -73,16 +71,18 @@ export class AuthResolver { } @Mutation(() => Boolean) - @UseGuards(JwtAuthGuard) - logout( + @UseGuards(GlobalAuthGuard) + async logout( @GQLRequest() req: Request, @GQLResponse() res: Response, @CurrentUser() user: User, - ): boolean { + ): Promise<boolean> { if (user) { this.logger.verbose(`${user.username} logged out`); try { - this.engineService.logout?.(req); + if (this.engineService.has('logout')) { + await this.engineService.logout(req); + } } catch (e) { this.logger.debug( `Service ${this.engineOptions.type} produce an error when logging out ${user.username}`, diff --git a/api/src/auth/auth.service.spec.ts b/api/src/auth/auth.service.spec.ts index 454ce9230371e3a5e8bd01d7469ae25252dbd859..ea689bdf469a81b7e8bdc4d9429a87598406d05c 100644 --- a/api/src/auth/auth.service.spec.ts +++ b/api/src/auth/auth.service.spec.ts @@ -1,18 +1,23 @@ import { JwtService } from '@nestjs/jwt'; import { Test, TestingModule } from '@nestjs/testing'; import { MockFunctionMetadata, ModuleMocker } from 'jest-mock'; -import LocalService from '../engine/connectors/local/main.connector'; -import { - ENGINE_MODULE_OPTIONS, - ENGINE_SERVICE, -} from '../engine/engine.constants'; +import { ENGINE_MODULE_OPTIONS } from '../engine/engine.constants'; import { AuthService } from './auth.service'; import { User } from '../users/models/user.model'; +import EngineService from '../engine/engine.service'; const moduleMocker = new ModuleMocker(global); +type MockEngineService = Partial<Record<keyof EngineService, jest.Mock>>; + +const createEngineService = (): MockEngineService => ({ + login: jest.fn(), + has: jest.fn(), +}); + describe('AuthService', () => { - let service: AuthService; + let authService: AuthService; + let engineService: MockEngineService; const user: User = { id: 'dummy', username: 'dummy64', @@ -23,8 +28,8 @@ describe('AuthService', () => { const module: TestingModule = await Test.createTestingModule({ providers: [ { - provide: ENGINE_SERVICE, - useClass: LocalService, + provide: EngineService, + useValue: createEngineService(), }, { provide: ENGINE_MODULE_OPTIONS, @@ -52,17 +57,25 @@ describe('AuthService', () => { }) .compile(); - service = module.get<AuthService>(AuthService); + authService = module.get<AuthService>(AuthService); + engineService = module.get<EngineService>( + EngineService, + ) as unknown as MockEngineService; }); it('login', async () => { - const data = await service.login(user); + const data = await authService.login(user); expect(data.accessToken).toBe(jwtToken); }); it('validateUser', async () => { - const data = await service.validateUser('guest', 'password123'); + engineService.has.mockReturnValue(true); + engineService.login.mockReturnValue({ + id: '1', + username: 'dummy64', + }); + const data = await authService.validateUser('guest', 'password123'); expect(!!data).toBeTruthy(); }); diff --git a/api/src/auth/auth.service.ts b/api/src/auth/auth.service.ts index 18eaadfa1d35fbae3b6ca306b057e17ae02a225d..8300b9a1649ce7683d0559c2814dbde94fec16bc 100644 --- a/api/src/auth/auth.service.ts +++ b/api/src/auth/auth.service.ts @@ -1,20 +1,19 @@ -import { Inject, Injectable, NotImplementedException } from '@nestjs/common'; +import { Injectable, NotImplementedException } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; -import { ENGINE_SERVICE } from '../engine/engine.constants'; -import { IEngineService } from '../engine/engine.interfaces'; +import EngineService from '../engine/engine.service'; import { User } from '../users/models/user.model'; import { AuthenticationOutput } from './outputs/authentication.output'; @Injectable() export class AuthService { constructor( - @Inject(ENGINE_SERVICE) private readonly engineService: IEngineService, + private readonly engineService: EngineService, private jwtService: JwtService, ) {} async validateUser(username: string, password: string): Promise<User> { - if (!this.engineService.login) throw new NotImplementedException(); - return await this.engineService.login?.(username, password); + if (!this.engineService.has('login')) throw new NotImplementedException(); + return this.engineService.login(username, password); } /** diff --git a/api/src/auth/decorators/user.decorator.ts b/api/src/auth/decorators/user.decorator.ts deleted file mode 100644 index 0aa940d9371596e9902e9acf5b8c0dcee1307d2f..0000000000000000000000000000000000000000 --- a/api/src/auth/decorators/user.decorator.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { createParamDecorator, ExecutionContext } from '@nestjs/common'; -import { GqlExecutionContext } from '@nestjs/graphql'; -import { User } from '../../users/models/user.model'; - -/** - * Retrieve the current user within the graphQL request - * @returns instance of User or undefined - */ -export const CurrentUser = createParamDecorator( - (data: unknown, context: ExecutionContext): User | undefined => { - const ctx = GqlExecutionContext.create(context); - return ctx.getContext().req.user; - }, -); diff --git a/api/src/auth/guards/jwt-auth.guard.ts b/api/src/auth/guards/global-auth.guard.ts similarity index 73% rename from api/src/auth/guards/jwt-auth.guard.ts rename to api/src/auth/guards/global-auth.guard.ts index 2ee1a63de580474c54270024e570cc9570788b1d..ecfd65e01ac3ae1a09b452ca74ff86b62cc27b7d 100644 --- a/api/src/auth/guards/jwt-auth.guard.ts +++ b/api/src/auth/guards/global-auth.guard.ts @@ -4,11 +4,13 @@ import { Reflector } from '@nestjs/core'; import { GqlExecutionContext } from '@nestjs/graphql'; import { AuthGuard } from '@nestjs/passport'; import { Observable } from 'rxjs'; -import { parseToBoolean } from '../../common/utilities'; -import { authConstants } from '../auth-constants'; @Injectable() -export class JwtAuthGuard extends AuthGuard(['jwt-cookies', 'jwt-bearer']) { +export class GlobalAuthGuard extends AuthGuard([ + 'jwt-cookies', + 'jwt-bearer', + 'engine', +]) { constructor( private readonly configService: ConfigService, private readonly reflector: Reflector, @@ -31,11 +33,7 @@ export class JwtAuthGuard extends AuthGuard(['jwt-cookies', 'jwt-bearer']) { context.getHandler(), ); - const skipAuth = parseToBoolean( - this.configService.get(authConstants.skipAuth, 'false'), - ); - - if (skipAuth || isPublic) { + if (isPublic) { return true; } diff --git a/api/src/auth/strategies/engine.strategy.ts b/api/src/auth/strategies/engine.strategy.ts new file mode 100644 index 0000000000000000000000000000000000000000..7bf68c2df41d842645f11ce8bb36891213ec6f84 --- /dev/null +++ b/api/src/auth/strategies/engine.strategy.ts @@ -0,0 +1,19 @@ +import { Injectable } from '@nestjs/common'; +import { PassportStrategy } from '@nestjs/passport'; +import { Request } from 'express'; +import { Strategy } from 'passport-custom'; +import EngineService from '../../engine/engine.service'; + +@Injectable() +export class EngineStrategy extends PassportStrategy(Strategy, 'engine') { + constructor(private readonly engineService: EngineService) { + super(); + } + + async validate(req: Request) { + if (!this.engineService.has('getActiveUser')) return false; + const user = this.engineService.getActiveUser(req); + + return user ?? false; + } +} diff --git a/api/src/common/decorators/user.decorator.ts b/api/src/common/decorators/user.decorator.ts index 0aa940d9371596e9902e9acf5b8c0dcee1307d2f..8db357e2c16742b69c5fe31e221a909fd39df3cd 100644 --- a/api/src/common/decorators/user.decorator.ts +++ b/api/src/common/decorators/user.decorator.ts @@ -7,7 +7,7 @@ import { User } from '../../users/models/user.model'; * @returns instance of User or undefined */ export const CurrentUser = createParamDecorator( - (data: unknown, context: ExecutionContext): User | undefined => { + (_data: unknown, context: ExecutionContext): User | undefined => { const ctx = GqlExecutionContext.create(context); return ctx.getContext().req.user; }, diff --git a/api/src/common/utilities.ts b/api/src/common/utilities.ts deleted file mode 100644 index 92d045784438e0181d5e80c85e85c20e82234fbf..0000000000000000000000000000000000000000 --- a/api/src/common/utilities.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { - HttpException, - InternalServerErrorException, - NotFoundException, - RequestTimeoutException, - UnauthorizedException, -} from '@nestjs/common'; -import axios from 'axios'; - -export const errorAxiosHandler = (e: any) => { - if (!axios.isAxiosError(e)) throw new InternalServerErrorException(e); - - if (e.response) { - if (e.response.status === 401) throw new UnauthorizedException(); - if (e.response.status === 404) throw new NotFoundException(); - if (e.response.status === 408) throw new RequestTimeoutException(); - if (e.response.status === 500) throw new InternalServerErrorException(); - if (e.response.status && e.response.status) - throw new HttpException(e.response.data, e.response.status); - } - - throw new InternalServerErrorException('Unknown error'); -}; - -/** - * Parse a string to a boolean - * @param {string} value - The value to parse. - * @param [defaultValue=false] - The default value to return if the value is not a valid boolean. - * @returns A boolean value. - */ -export const parseToBoolean = ( - value: string, - defaultValue = false, -): boolean => { - try { - if (value.toLowerCase() == 'true') return true; - return value.toLowerCase() == 'false' ? false : defaultValue; - } catch { - return defaultValue; - } -}; diff --git a/api/src/common/utilities.spec.ts b/api/src/common/utils/shared.utils.spec.ts similarity index 96% rename from api/src/common/utilities.spec.ts rename to api/src/common/utils/shared.utils.spec.ts index 88f5fb323c1fe3c5de33008cabf3060fa141b8fe..b521a2e27cd3dde32c0dea92ba4f6f97eb5f374f 100644 --- a/api/src/common/utilities.spec.ts +++ b/api/src/common/utils/shared.utils.spec.ts @@ -6,8 +6,7 @@ import { UnauthorizedException, } from '@nestjs/common'; import axios from 'axios'; -import { response } from 'express'; -import { errorAxiosHandler, parseToBoolean } from './utilities'; +import { errorAxiosHandler, parseToBoolean } from './shared.utils'; describe('Utility parseToBoolean testing', () => { it('Parse true string to boolean', () => { diff --git a/api/src/common/utils/shared.utils.ts b/api/src/common/utils/shared.utils.ts new file mode 100644 index 0000000000000000000000000000000000000000..8d9b507a16f8e293fd2bacf76542981e1962c6eb --- /dev/null +++ b/api/src/common/utils/shared.utils.ts @@ -0,0 +1,118 @@ +/* eslint-disable @typescript-eslint/ban-types */ +/* eslint-disable @typescript-eslint/no-use-before-define */ + +import { + HttpException, + InternalServerErrorException, + LogLevel, + NotFoundException, + RequestTimeoutException, + UnauthorizedException, +} from '@nestjs/common'; +import axios from 'axios'; + +export const LOG_LEVELS = [ + ['warn', 'error'], + ['warn', 'error', 'log'], + ['warn', 'error', 'log', 'verbose'], + ['warn', 'error', 'log', 'verbose', 'debug'], +]; + +export const getLogLevels = (level: number): LogLevel[] => { + let internLevel = level - 1; + if (internLevel > LOG_LEVELS.length || internLevel < 0) internLevel = 0; + return LOG_LEVELS[internLevel] as LogLevel[]; +}; + +export const errorAxiosHandler = (e: any) => { + if (!axios.isAxiosError(e)) throw new InternalServerErrorException(e); + + if (e.response) { + if (e.response.status === 401) throw new UnauthorizedException(); + if (e.response.status === 404) throw new NotFoundException(); + if (e.response.status === 408) throw new RequestTimeoutException(); + if (e.response.status === 500) throw new InternalServerErrorException(); + if (e.response.status) + throw new HttpException(e.response.data, e.response.status); + } + + throw new InternalServerErrorException('Unknown error'); +}; + +/** + * It rounds a number to a given number of decimal places + * @param {number} val - the number to be rounded + * @param [decimal=2] - The number of decimal places to round to. + * @param [keepSmallNumber=true] - If true, it will keep the number in exponential form if it's smaller + * than 1/coef. + * @returns Formatted string number + */ +export const floatRound = ( + val: number, + decimal = 2, + keepSmallNumber = true, +) => { + const n = Math.trunc(decimal); + + if (n < 0) throw new Error('decimal cannot be negative number'); + + const coef = Math.pow(10, n); + + if (keepSmallNumber && val !== 0 && val < 1 / coef) { + return val.toExponential(n); + } + + return (Math.round(val * coef) / coef).toString(); +}; + +/** + * Parse a string to a boolean + * @param {string} value - The value to parse. + * @param [defaultValue=false] - The default value to return if the value is not a valid boolean. + * @returns A boolean value. + */ +export const parseToBoolean = ( + value: string, + defaultValue = false, +): boolean => { + try { + if (value.toLowerCase() == 'true') return true; + return value.toLowerCase() == 'false' ? false : defaultValue; + } catch { + return defaultValue; + } +}; + +export const isUndefined = (obj: any): obj is undefined => + typeof obj === 'undefined'; + +export const isObject = (fn: any): fn is object => + !isNil(fn) && typeof fn === 'object'; + +export const isPlainObject = (fn: any): fn is object => { + if (!isObject(fn)) { + return false; + } + const proto = Object.getPrototypeOf(fn); + if (proto === null) { + return true; + } + const ctor = + Object.prototype.hasOwnProperty.call(proto, 'constructor') && + proto.constructor; + return ( + typeof ctor === 'function' && + ctor instanceof ctor && + Function.prototype.toString.call(ctor) === + Function.prototype.toString.call(Object) + ); +}; + +export const isFunction = (val: any): boolean => typeof val === 'function'; +export const isString = (val: any): val is string => typeof val === 'string'; +export const isNumber = (val: any): val is number => typeof val === 'number'; +export const isConstructor = (val: any): boolean => val === 'constructor'; +export const isNil = (val: any): val is null | undefined => + isUndefined(val) || val === null; +export const isEmpty = (array: any): boolean => !(array && array.length > 0); +export const isSymbol = (val: any): val is symbol => typeof val === 'symbol'; diff --git a/api/src/config/cache.config.ts b/api/src/config/cache.config.ts new file mode 100644 index 0000000000000000000000000000000000000000..763d7d5b3bcc73dbdfeabdebdba94f4837b1f8ed --- /dev/null +++ b/api/src/config/cache.config.ts @@ -0,0 +1,12 @@ +import { registerAs } from '@nestjs/config'; +import { parseToBoolean } from '../common/utils/shared.utils'; + +export default registerAs('cache', () => { + const max = process.env.CACHE_MAX_ITEMS; + const ttl = process.env.CACHE_TTL; + return { + enabled: parseToBoolean(process.env.CACHE_ENABLED, false), + ttl: ttl ? parseInt(ttl) : undefined, + max: max ? parseInt(max) : undefined, + }; +}); diff --git a/api/src/config/matomo.config.ts b/api/src/config/matomo.config.ts index 5e7fa269a4913509846da9d367fc607954c7ee52..dacfd178f90073e950116aedf8e296dfaa557808 100644 --- a/api/src/config/matomo.config.ts +++ b/api/src/config/matomo.config.ts @@ -1,5 +1,5 @@ import { registerAs } from '@nestjs/config'; -import { parseToBoolean } from 'src/common/utilities'; +import { parseToBoolean } from 'src/common/utils/shared.utils'; export default registerAs('matomo', () => { return { diff --git a/api/src/engine/connectors/csv/main.connector.ts b/api/src/engine/connectors/csv/csv.connector.ts similarity index 62% rename from api/src/engine/connectors/csv/main.connector.ts rename to api/src/engine/connectors/csv/csv.connector.ts index d725496356e3133f54a364df1fa2158e13b14616..2d3a80cce5935712fdb5663ce50f8239d45bddb7 100644 --- a/api/src/engine/connectors/csv/main.connector.ts +++ b/api/src/engine/connectors/csv/csv.connector.ts @@ -1,72 +1,33 @@ +import { HttpService } from '@nestjs/axios'; +import { NotImplementedException } from '@nestjs/common'; import { firstValueFrom } from 'rxjs'; -import { IEngineOptions, IEngineService } from 'src/engine/engine.interfaces'; -import { Domain } from 'src/engine/models/domain.model'; -import { ExperimentCreateInput } from 'src/engine/models/experiment/input/experiment-create.input'; import { - Experiment, - PartialExperiment, -} from 'src/engine/models/experiment/experiment.model'; -import { ListExperiments } from 'src/engine/models/experiment/list-experiments.model'; -import { ExperimentEditInput } from 'src/engine/models/experiment/input/experiment-edit.input'; + Dictionary, + ExperimentResult, +} from 'src/common/interfaces/utilities.interface'; +import Connector from 'src/engine/interfaces/connector.interface'; +import EngineOptions from 'src/engine/interfaces/engine-options.interface'; +import { Domain } from 'src/engine/models/domain.model'; import { Algorithm } from 'src/engine/models/experiment/algorithm.model'; -import { HttpService } from '@nestjs/axios'; import { Group } from 'src/engine/models/group.model'; -import { Dictionary } from 'src/common/interfaces/utilities.interface'; import { User } from 'src/users/models/user.model'; -export default class CSVService implements IEngineService { +export default class CSVConnector implements Connector { constructor( - private readonly options: IEngineOptions, + private readonly options: EngineOptions, private readonly httpService: HttpService, ) {} async logout() { - throw new Error('Method not implemented.'); + throw new NotImplementedException(); } async getAlgorithms(): Promise<Algorithm[]> { - throw new Error('Method not implemented.'); + throw new NotImplementedException(); } - async createExperiment( - data: ExperimentCreateInput, - isTransient: boolean, - ): Promise<Experiment> { - return { - id: '', - domain: '', - datasets: [], - algorithm: { - id: '', - description: '', - }, - name: 'test', - variables: [], - }; - } - - async listExperiments(page: number, name: string): Promise<ListExperiments> { - return { - experiments: [], - currentPage: 0, - totalExperiments: 0, - totalPages: 0, - }; - } - - async getExperiment(id: string): Promise<Experiment> { - throw new Error('Method not implemented.'); - } - - async removeExperiment(id: string): Promise<PartialExperiment> { - throw new Error('Method not implemented.'); - } - - async editExperient( - id: string, - expriment: ExperimentEditInput, - ): Promise<Experiment> { - throw new Error('Method not implemented.'); + async runExperiment(): Promise<ExperimentResult[]> { + throw new NotImplementedException(); } async getDomains(): Promise<Domain[]> { @@ -155,17 +116,12 @@ export default class CSVService implements IEngineService { } async getActiveUser(): Promise<User> { - const dummyUser = { + return { username: 'anonymous', id: 'anonymousId', fullname: 'anonymous', email: 'anonymous@anonymous.com', agreeNDA: true, }; - return dummyUser; - } - - getAlgorithmsREST(): string { - return '[]'; } } diff --git a/api/src/engine/connectors/datashield/datashield.connector.ts b/api/src/engine/connectors/datashield/datashield.connector.ts new file mode 100644 index 0000000000000000000000000000000000000000..244c56621dd6d018f73655df9fec142416934e21 --- /dev/null +++ b/api/src/engine/connectors/datashield/datashield.connector.ts @@ -0,0 +1,365 @@ +import { HttpService } from '@nestjs/axios'; +import { Inject, InternalServerErrorException, Logger } from '@nestjs/common'; +import { Request } from 'express'; +import { catchError, firstValueFrom } from 'rxjs'; +import { + ExperimentResult, + MIME_TYPES, +} from 'src/common/interfaces/utilities.interface'; +import { errorAxiosHandler } from 'src/common/utils/shared.utils'; +import { ENGINE_MODULE_OPTIONS } from 'src/engine/engine.constants'; +import EngineService from 'src/engine/engine.service'; +import ConnectorConfiguration from 'src/engine/interfaces/connector-configuration.interface'; +import Connector from 'src/engine/interfaces/connector.interface'; +import EngineOptions from 'src/engine/interfaces/engine-options.interface'; +import { Domain } from 'src/engine/models/domain.model'; +import { Algorithm } from 'src/engine/models/experiment/algorithm.model'; +import { AllowedLink } from 'src/engine/models/experiment/algorithm/nominal-parameter.model'; +import { Experiment } from 'src/engine/models/experiment/experiment.model'; +import { RawResult } from 'src/engine/models/result/raw-result.model'; +import { + TableResult, + TableStyle, +} from 'src/engine/models/result/table-result.model'; +import { Variable } from 'src/engine/models/variable.model'; +import { ExperimentCreateInput } from 'src/experiments/models/input/experiment-create.input'; +import { User } from 'src/users/models/user.model'; +import { + dataToGroups, + dsGroup, + transformToDomain, + transformToHisto, + transformToTable, + transformToTableNominal, + transfoToHistoNominal as transformToHistoNominal, +} from './transformations'; + +export default class DataShieldConnector implements Connector { + private static readonly logger = new Logger(DataShieldConnector.name); + headers = {}; + constructor( + @Inject(ENGINE_MODULE_OPTIONS) private readonly options: EngineOptions, + private readonly httpService: HttpService, + private readonly engineService: EngineService, + ) {} + + getConfiguration(): ConnectorConfiguration { + return { + hasGalaxy: false, + hasGrouping: false, + }; + } + + async login(username: string, password: string): Promise<User> { + const loginPath = this.options.baseurl + 'login'; + + const user: User = { + id: username, + username, + extraFields: { + sid: '', + }, + }; + + const loginData = await firstValueFrom( + this.httpService + .get(loginPath, { + auth: { username, password }, + }) + .pipe(catchError((e) => errorAxiosHandler(e))), + ); + + const cookies = loginData.headers['set-cookie'] ?? []; + if (loginData.headers && loginData.headers['set-cookie']) { + cookies.forEach((cookie) => { + const [key, value] = cookie.split(/={1}/); + if (key === 'sid') { + user.extraFields.sid = value; + } + }); + } + + return user; + } + + async getAlgorithms(): Promise<Algorithm[]> { + return [ + { + id: 'linear-regression', + label: 'Linear Regression', + description: + 'Linear regression analysis is a method of statistical analysis that fits a linear function in order to predict the value of a covariate as a function of one or more variables. Linear regression is a simple model that is easy to understand and interpret.', + variable: { + isRequired: true, + allowedTypes: ['number'], + hasMultiple: false, + }, + coVariable: { + isRequired: true, + allowedTypes: ['number'], + hasMultiple: true, + }, + }, + { + id: 'logistic-regression', + label: 'Logistic Regression', + description: + 'Logistic regression is a statistical method for predicting the probability of a binary event.', + variable: { + isRequired: true, + allowedTypes: ['nominal'], + hasMultiple: false, + hint: 'A binary event to predict', + }, + coVariable: { + isRequired: true, + allowedTypes: ['number'], + hasMultiple: true, + }, + parameters: [ + { + name: 'pos-level', + label: 'Positive level', + linkedTo: AllowedLink.VARIABLE, + isRequired: true, + }, + { + name: 'neg-level', + label: 'Negative level', + linkedTo: AllowedLink.VARIABLE, + isRequired: true, + }, + ], + }, + ]; + } + + async getHistogram( + variable: Variable, + datasets: string[], + cookie?: string, + ): Promise<RawResult> { + const url = new URL(this.options.baseurl + `histogram`); + + url.searchParams.append('var', variable.id); + url.searchParams.append('type', 'combine'); + url.searchParams.append('cohorts', datasets.join(',')); + + const path = url.href; + + const response = await firstValueFrom( + this.httpService.get(path, { + headers: { + cookie, + }, + }), + ); + + if (response.data['global'] === undefined) { + DataShieldConnector.logger.warn('Cannot parse histogram result'); + DataShieldConnector.logger.verbose(path); + return { + rawdata: { + data: + 'Engine error when processing the request. Reason: ' + + response.data, + type: MIME_TYPES.ERROR, + }, + }; + } + + const title = variable.label ?? variable.id; + const data = { ...response.data, title }; + + if (variable.type === 'nominal' && variable.enumerations) { + data['lookup'] = variable.enumerations.reduce((prev, curr) => { + prev[curr.value] = curr.label; + return prev; + }, {}); + } + + const chart = + variable.type === 'nominal' + ? transformToHistoNominal.evaluate(data) + : transformToHisto.evaluate(data); + + return { + rawdata: { + data: chart, + type: 'application/vnd.highcharts+json', + }, + }; + } + + async getDescriptiveStats( + variable: Variable, + datasets: string[], + cookie?: string, + ): Promise<TableResult> { + const url = new URL(this.options.baseurl + 'quantiles'); + + url.searchParams.append('var', variable.id); + url.searchParams.append('type', 'split'); + url.searchParams.append('cohorts', datasets.join(',')); + + const path = url.href; + + const response = await firstValueFrom( + this.httpService.get(path, { + headers: { + cookie, + }, + }), + ); + + const title = variable.label ?? variable.id; + const data = { ...response.data, title }; + + const table = ( + variable.enumerations + ? transformToTableNominal.evaluate(data) + : transformToTable.evaluate(data) + ) as TableResult; + + if ( + table && + table.headers && + variable.type === 'nominal' && + variable.enumerations + ) { + table.headers = table.headers.map((header) => { + const category = variable.enumerations.find( + (v) => v.value === header.name, + ); + + if (!category || !category.label) return header; + + return { + ...header, + name: category.label, + }; + }); + } + + return { + ...table, + tableStyle: TableStyle.DEFAULT, + }; + } + + async runExperiment( + data: ExperimentCreateInput, + request: Request, + ): Promise<ExperimentResult[]> { + const user = request.user as User; + const cookie = [`sid=${user.extraFields['sid']}`, `user=${user.id}`].join( + ';', + ); + const expResult: Experiment = { + id: `${data.algorithm.id}-${Date.now()}`, + variables: data.variables, + coVariables: data.coVariables, + author: { + username: user.username, + fullname: user.fullname ?? user.username, + }, + name: data.name, + domain: data.domain, + datasets: data.datasets, + algorithm: { + name: data.algorithm.id, + }, + }; + + const allVariablesId = [...data.variables, ...data.coVariables]; + + const allVariables = await this.engineService.getVariables( + expResult.domain, + allVariablesId, + request, + ); + + switch (data.algorithm.id) { + case 'MULTIPLE_HISTOGRAMS': { + expResult.results = await Promise.all<RawResult>( + allVariables.map((variable) => + this.getHistogram(variable, expResult.datasets, cookie), + ), + ); + break; + } + case 'DESCRIPTIVE_STATS': { + // Cannot be done in parallel because Datashield API has an issue with parallel request (response mismatching) + const results = []; + for (const variable of allVariables) { + const result = await this.getDescriptiveStats( + variable, + expResult.datasets, + cookie, + ); + + results.push(result); + } + + expResult.results = results; + break; + } + } + + return expResult.results; + } + + async logout(request: Request): Promise<void> { + const user = request.user as User; + const cookie = [`sid=${user.extraFields['sid']}`, `user=${user.id}`].join( + ';', + ); + + const path = new URL('/logout', this.options.baseurl).href; + + this.httpService.get(path, { + headers: { + cookie, + }, + }); + } + + async getDomains(request: Request): Promise<Domain[]> { + const user = request.user as User; + const sid = user && user.extraFields && user.extraFields['sid']; + + if (!sid) + throw new InternalServerErrorException( + 'Datashield sid is missing from the user', + ); + + const cookies = [`sid=${user.extraFields['sid']}`, `user=${user.id}`]; + const path = this.options.baseurl + 'getvars'; + + const response = await firstValueFrom( + this.httpService.get(path, { + headers: { + cookie: cookies.join(';'), + }, + }), + ); + + const dsDomain = transformToDomain.evaluate(response.data); + const groups = response.data['groups'] as dsGroup[]; + + dataToGroups(dsDomain, groups); + return [dsDomain]; + } + + async getActiveUser(req: Request): Promise<User> { + const user = req.user as User; + + if (!user) throw new InternalServerErrorException('User not found'); + + return { + username: user.id, + id: user.id, + fullname: user.id, + }; + } +} diff --git a/api/src/engine/connectors/datashield/main.connector.ts b/api/src/engine/connectors/datashield/main.connector.ts deleted file mode 100644 index ad0e132cda9a53cb5075f101aef35ab04263802d..0000000000000000000000000000000000000000 --- a/api/src/engine/connectors/datashield/main.connector.ts +++ /dev/null @@ -1,265 +0,0 @@ -import { HttpService } from '@nestjs/axios'; -import { - Inject, - InternalServerErrorException, - Logger, - NotImplementedException, -} from '@nestjs/common'; -import { Request } from 'express'; -import { catchError, firstValueFrom } from 'rxjs'; -import { MIME_TYPES } from 'src/common/interfaces/utilities.interface'; -import { errorAxiosHandler } from 'src/common/utilities'; -import { ENGINE_MODULE_OPTIONS } from 'src/engine/engine.constants'; -import { - IConfiguration, - IEngineOptions, - IEngineService, -} from 'src/engine/engine.interfaces'; -import { Domain } from 'src/engine/models/domain.model'; -import { Algorithm } from 'src/engine/models/experiment/algorithm.model'; -import { - Experiment, - PartialExperiment, -} from 'src/engine/models/experiment/experiment.model'; -import { ExperimentCreateInput } from 'src/engine/models/experiment/input/experiment-create.input'; -import { ExperimentEditInput } from 'src/engine/models/experiment/input/experiment-edit.input'; -import { ListExperiments } from 'src/engine/models/experiment/list-experiments.model'; -import { RawResult } from 'src/engine/models/result/raw-result.model'; -import { - TableResult, - TableStyle, -} from 'src/engine/models/result/table-result.model'; -import { User } from 'src/users/models/user.model'; -import { - transformToDomains, - transformToHisto, - transformToTable, -} from './transformations'; - -export default class DataShieldService implements IEngineService { - private static readonly logger = new Logger(DataShieldService.name); - headers = {}; - constructor( - @Inject(ENGINE_MODULE_OPTIONS) private readonly options: IEngineOptions, - private readonly httpService: HttpService, - ) {} - - getConfiguration(): IConfiguration { - return {}; - } - - async login(username: string, password: string): Promise<User> { - const loginPath = this.options.baseurl + 'login'; - - const user: User = { - id: username, - username, - extraFields: { - sid: '', - }, - }; - - const loginData = await firstValueFrom( - this.httpService - .get(loginPath, { - auth: { username, password }, - }) - .pipe(catchError((e) => errorAxiosHandler(e))), - ); - - const cookies = (loginData.headers['set-cookie'] as string[]) ?? []; - if (loginData.headers && loginData.headers['set-cookie']) { - cookies.forEach((cookie) => { - const [key, value] = cookie.split(/={1}/); - if (key === 'sid') { - user.extraFields.sid = value; - } - }); - } - - return user; - } - - async getAlgorithms(): Promise<Algorithm[]> { - throw new NotImplementedException(); - } - - async getHistogram(variable: string, cookie?: string): Promise<RawResult> { - const path = - this.options.baseurl + `histogram?var=${variable}&type=combine`; - - const response = await firstValueFrom( - this.httpService.get(path, { - headers: { - cookie, - }, - }), - ); - - if (response.data['global'] === undefined) { - DataShieldService.logger.warn('Inconsistency on histogram result'); - DataShieldService.logger.verbose(path); - return { - rawdata: { - data: - 'Engine error when processing the request. Reason: ' + - response.data, - type: MIME_TYPES.ERROR, - }, - }; - } - - const title = variable.replace(/\./g, ' ').trim(); - const data = { ...response.data, title }; - - const chart = transformToHisto.evaluate(data); - - return { - rawdata: { - data: chart, - type: 'application/vnd.highcharts+json', - }, - }; - } - - async getDescriptiveStats( - variable: string, - cookie?: string, - ): Promise<TableResult> { - const path = this.options.baseurl + `quantiles?var=${variable}&type=split`; - - const response = await firstValueFrom( - this.httpService.get(path, { - headers: { - cookie, - }, - }), - ); - - const title = variable.replace(/\./g, ' ').trim(); - const data = { ...response.data, title }; - const table = transformToTable.evaluate(data); - return { - ...table, - tableStyle: TableStyle.NORMAL, - }; - } - - async createExperiment( - data: ExperimentCreateInput, - isTransient: boolean, - request: Request, - ): Promise<Experiment> { - const user = request.user as User; - const cookie = [`sid=${user.extraFields['sid']}`, `user=${user.id}`].join( - ';', - ); - const expResult: Experiment = { - id: `${data.algorithm.id}-${Date.now()}`, - variables: data.variables, - name: data.name, - domain: data.domain, - datasets: data.datasets, - algorithm: { - id: data.algorithm.id, - }, - }; - - switch (data.algorithm.id) { - case 'MULTIPLE_HISTOGRAMS': { - expResult.results = await Promise.all<RawResult>( - data.variables.map( - async (variable) => await this.getHistogram(variable, cookie), - ), - ); - break; - } - case 'DESCRIPTIVE_STATS': { - expResult.results = await Promise.all<TableResult>( - [...data.variables, ...data.coVariables].map( - async (variable) => - await this.getDescriptiveStats(variable, cookie), - ), - ); - break; - } - } - - return expResult; - } - - async listExperiments(page: number, name: string): Promise<ListExperiments> { - return { - totalExperiments: 0, - experiments: [], - totalPages: 0, - currentPage: 0, - }; - } - - async getExperiment(id: string): Promise<Experiment> { - throw new NotImplementedException(); - } - - async removeExperiment(id: string): Promise<PartialExperiment> { - throw new NotImplementedException(); - } - - async logout(request: Request): Promise<void> { - const user = request.user as User; - const cookie = [`sid=${user.extraFields['sid']}`, `user=${user.id}`].join( - ';', - ); - - const path = new URL('/logout', this.options.baseurl).href; - - this.httpService.get(path, { - headers: { - cookie, - }, - }); - } - - async editExperient( - id: string, - expriment: ExperimentEditInput, - ): Promise<Experiment> { - throw new NotImplementedException(); - } - - async getDomains(ids: string[], request: Request): Promise<Domain[]> { - const user = request.user as User; - const sid = user && user.extraFields && user.extraFields['sid']; - - if (!sid) - throw new InternalServerErrorException( - 'Datashield sid is missing from the user', - ); - - const cookies = [`sid=${user.extraFields['sid']}`, `user=${user.id}`]; - const path = this.options.baseurl + 'getvars'; - - const response = await firstValueFrom( - this.httpService.get(path, { - headers: { - cookie: cookies.join(';'), - }, - }), - ); - - return [transformToDomains.evaluate(response.data)]; - } - - async getActiveUser(req: Request): Promise<User> { - const user = req.user as User; - return { - username: user.id, - id: user.id, - fullname: user.id, - }; - } - - getAlgorithmsREST(): string { - return '[]'; - } -} diff --git a/api/src/engine/connectors/datashield/transformations.ts b/api/src/engine/connectors/datashield/transformations.ts index 065040b6611b020130709bdb7f0734b9d505da12..b1991aa7fa1e3331c8295068787f226c7a801209 100644 --- a/api/src/engine/connectors/datashield/transformations.ts +++ b/api/src/engine/connectors/datashield/transformations.ts @@ -2,33 +2,66 @@ // see : https://docs.jsonata.org/ import * as jsonata from 'jsonata'; +import { Domain } from 'src/engine/models/domain.model'; +import { Group } from 'src/engine/models/group.model'; -export const transformToDomains = jsonata(` +export const transformToDomain = jsonata(` { "id": "sophia", + "label": "Sophia", "datasets": datasets.{ - "id": $.id[0], - "label": $.label[0] + "id": $.id[0], + "label": $.label[0] }, "rootGroup": { - "id": rootGroup.id[0], - "label": rootGroup.label[0], - "groups": rootGroup.groups + "id": "root", + "label": "Sophia", + "groups": $append(rootGroup.groups, $keys($.groups.variables)) }, - "groups": groups.{ - "id": $.id[0], - "label": $.label[0], - "variables": $.variables, - "groups": $.groups - }, - "variables": $distinct(groups.variables).{ - "id": $, - "label": $trim($replace($ & '', '.', ' ')), - "type": "Number" - } + "groups": datasets.{ + "id": $.id[0], + "label": $.label[0], + "groups": [], + "datasets": $.id[0][] + }[], + "variables": variables.( + $merge([$, {'label': $.label ? $label : $trim($replace($.id, '.', ' '))}]) + ) } `); +export const transfoToHistoNominal = jsonata(` +( + { + "chart": { + "type": 'column' + }, + "legend": { + "enabled": false + }, + "series": [{ + "name": "Count", + "data": global.* + }], + "title": { + "text": title ? title : '' + }, + "tooltip": { + "enabled": true + }, + "xAxis": { + "categories": $keys(global).( + $param := $lookup($$.lookup, $); + $param ? $param : $ + ) + }, + "yAxis": { + "min": 0, + "minRange": 0.1, + "allowDecimals": true + } +})`); + export const transformToHisto = jsonata(` ( $nbBreaks := $count(global.breaks); @@ -73,12 +106,76 @@ export const transformToTable = jsonata(` "name": $, "type": "string" }, + "data": $.$each(function($v, $k) { + $not($k in $params) ? $append($k,$v) : undefined + })[] +}) +`); + +export const transformToTableNominal = jsonata(` +( + $params := ["title"]; + { + "name": "Descriptive Statistics", + "headers": $append(title, $keys($.*)).{ + "name": $, + "type": "string" + }, "data": $.$each(function($v, $k) { $not($k in $params) ? $append($k,$v.*) : undefined - }) + })[] }) `); export const transformToUser = jsonata(` $ ~> |$|{'id': subjectId}, ['subjectId']| `); + +export type dsGroup = { + id: string; + label: string; + variables: Record<string, string[]> | string[]; + groups?: string[]; +}; + +export const dataToGroups = (dsDomain: Domain, groups: dsGroup[]) => { + groups.forEach((group) => { + // Check if variables contains sub db split + if (Array.isArray(group.variables)) { + // Global group (exist in every cohort) + group.variables = dsDomain.datasets.reduce((prev, db) => { + prev[db.id] = group.variables; + return prev; + }, {}); + } + + let isRootGroup = false; + + // remove group if it's in the root + if (dsDomain.rootGroup.groups.includes(group.id)) { + isRootGroup = true; + dsDomain.rootGroup.groups = dsDomain.rootGroup.groups.filter( + (gId) => gId !== group.id, + ); + } + + return Object.entries(group.variables).map(([db, variables]) => { + const id = `${group.id}-${db}`; + + // push group into root db group + if (isRootGroup) + dsDomain.groups.find((g2) => g2.id === db).groups.push(id); + + const newGroup: Group = { + id, + variables, + groups: group['groups'] + ? group['groups'].map((g2) => `${g2}-${db}`) + : undefined, + label: `${group.label} (${db})`, + }; + + dsDomain.groups.push(newGroup); + }); + }); +}; diff --git a/api/src/engine/connectors/exareme/converters.ts b/api/src/engine/connectors/exareme/converters.ts index 56f35ddcd2876a5cd6435cd3454c6fb414034923..86b58070bbca5bfcada6303b2ec4806b6df9ced8 100644 --- a/api/src/engine/connectors/exareme/converters.ts +++ b/api/src/engine/connectors/exareme/converters.ts @@ -1,10 +1,10 @@ import { MIME_TYPES } from 'src/common/interfaces/utilities.interface'; import { Category } from 'src/engine/models/category.model'; import { Dataset } from 'src/engine/models/dataset.model'; -import { Algorithm } from 'src/engine/models/experiment/algorithm.model'; -import { Experiment } from 'src/engine/models/experiment/experiment.model'; -import { AlgorithmParamInput } from 'src/engine/models/experiment/input/algorithm-parameter.input'; -import { ExperimentCreateInput } from 'src/engine/models/experiment/input/experiment-create.input'; +import { + Experiment, + ExperimentStatus, +} from 'src/engine/models/experiment/experiment.model'; import { Group } from 'src/engine/models/group.model'; import { ResultUnion } from 'src/engine/models/result/common/result-union.model'; import { @@ -15,6 +15,8 @@ import { HeatMapResult } from 'src/engine/models/result/heat-map-result.model'; import { LineChartResult } from 'src/engine/models/result/line-chart-result.model'; import { RawResult } from 'src/engine/models/result/raw-result.model'; import { Variable } from 'src/engine/models/variable.model'; +import { AlgorithmParamInput } from 'src/experiments/models/input/algorithm-parameter.input'; +import { ExperimentCreateInput } from 'src/experiments/models/input/experiment-create.input'; import { Entity } from './interfaces/entity.interface'; import { ExperimentData } from './interfaces/experiment/experiment.interface'; import { ResultChartExperiment } from './interfaces/experiment/result-chart-experiment.interface'; @@ -26,7 +28,6 @@ import { dataToHeatmap, descriptiveModelToTables, descriptiveSingleToTables, - transformToAlgorithms, transformToExperiment, } from './transformations'; @@ -38,14 +39,14 @@ export const dataToGroup = (data: Hierarchy): Group => { ? data.groups.map(dataToGroup).map((group) => group.id) : [], variables: data.variables - ? data.variables.map((data: VariableEntity) => data.code) + ? data.variables.map((v: VariableEntity) => v.code) : [], }; }; export const dataToCategory = (data: Entity): Category => { return { - id: data.code, + value: data.code, label: data.label, }; }; @@ -215,16 +216,36 @@ export const dataToExperiment = ( exp.results = data.result ? data.result - .map((result) => dataToResult(result, exp.algorithm.id)) + .map((result) => dataToResult(result, exp.algorithm.name)) + .filter((r) => r.length > 0) .flat() : []; + const allVariables = exp.filterVariables || []; + + // add filter variables + const extractVariablesFromFilter = (filter: any): any => + filter.rules.forEach((r: any) => { + if (r.rules) { + extractVariablesFromFilter(r); + } + if (r.id) { + allVariables.push(r.id); + } + }); + + if (exp && exp.filter) { + extractVariablesFromFilter(JSON.parse(exp.filter)); + } + + exp.filterVariables = Array.from(new Set(allVariables)); + return exp; } catch (e) { return { id: data.uuid, name: data.name, - status: 'error', + status: ExperimentStatus.ERROR, variables: [], domain: data['domain'] ?? '', results: [ @@ -237,16 +258,12 @@ export const dataToExperiment = ( ], datasets: [], algorithm: { - id: 'unknown', + name: 'unknown', }, }; } }; -export const dataToAlgorithms = (data: string): Algorithm[] => { - return transformToAlgorithms.evaluate(data); -}; - export const dataToRaw = ( algo: string, result: ResultExperiment, @@ -283,10 +300,13 @@ export const dataJSONtoResult = ( algo: string, ): Array<typeof ResultUnion> => { switch (algo.toLowerCase()) { + case 'cart': + case 'id3': + return dataToRaw(algo, result); case 'descriptive_stats': return descriptiveDataToTableResult(result); default: - return dataToRaw(algo, result); + return []; } }; diff --git a/api/src/engine/connectors/exareme/main.connector.ts b/api/src/engine/connectors/exareme/exareme.connector.ts similarity index 77% rename from api/src/engine/connectors/exareme/main.connector.ts rename to api/src/engine/connectors/exareme/exareme.connector.ts index 02f28ae18223e74286b62b83a5e1aca03ce577d7..c628a369f867ecb4b1da333269c284071b15166f 100644 --- a/api/src/engine/connectors/exareme/main.connector.ts +++ b/api/src/engine/connectors/exareme/exareme.connector.ts @@ -11,26 +11,24 @@ import { AxiosRequestConfig } from 'axios'; import { Request } from 'express'; import { firstValueFrom, map, Observable } from 'rxjs'; import { ENGINE_MODULE_OPTIONS } from 'src/engine/engine.constants'; -import { - IConfiguration, - IEngineOptions, - IEngineService, -} from 'src/engine/engine.interfaces'; +import ConnectorConfiguration from 'src/engine/interfaces/connector-configuration.interface'; +import Connector from 'src/engine/interfaces/connector.interface'; +import EngineOptions from 'src/engine/interfaces/engine-options.interface'; import { Domain } from 'src/engine/models/domain.model'; import { Algorithm } from 'src/engine/models/experiment/algorithm.model'; import { Experiment, PartialExperiment, } from 'src/engine/models/experiment/experiment.model'; -import { ExperimentCreateInput } from 'src/engine/models/experiment/input/experiment-create.input'; -import { ExperimentEditInput } from 'src/engine/models/experiment/input/experiment-edit.input'; import { ListExperiments } from 'src/engine/models/experiment/list-experiments.model'; +import { FormulaOperation } from 'src/engine/models/formula/formula-operation.model'; import { Group } from 'src/engine/models/group.model'; import { Variable } from 'src/engine/models/variable.model'; +import { ExperimentCreateInput } from 'src/experiments/models/input/experiment-create.input'; +import { ExperimentEditInput } from 'src/experiments/models/input/experiment-edit.input'; import { User } from 'src/users/models/user.model'; import { transformToUser } from '../datashield/transformations'; import { - dataToAlgorithms, dataToDataset, dataToExperiment, dataToGroup, @@ -41,20 +39,36 @@ import { ExperimentData } from './interfaces/experiment/experiment.interface'; import { ExperimentsData } from './interfaces/experiment/experiments.interface'; import { Hierarchy } from './interfaces/hierarchy.interface'; import { Pathology } from './interfaces/pathology.interface'; +import { dataToUser } from './transformations'; +import transformToAlgorithms from './transformations/algorithms'; type Headers = Record<string, string>; @Injectable() -export default class ExaremeService implements IEngineService { +export default class ExaremeConnector implements Connector { constructor( - @Inject(ENGINE_MODULE_OPTIONS) private readonly options: IEngineOptions, + @Inject(ENGINE_MODULE_OPTIONS) private readonly options: EngineOptions, private readonly httpService: HttpService, ) {} - getConfiguration(): IConfiguration { + async getFormulaConfiguration(): Promise<FormulaOperation[]> { + return [ + { + variableType: 'real', + operationTypes: ['log', 'exp', 'center', 'standardize'], + }, + { + variableType: 'nominal', + operationTypes: ['dummy', 'poly', 'contrast', 'additive'], + }, + ]; + } + + getConfiguration(): ConnectorConfiguration { return { contactLink: 'https://ebrains.eu/support/', hasGalaxy: true, + hasGrouping: true, }; } @@ -105,9 +119,8 @@ export default class ExaremeService implements IEngineService { const resultAPI = await firstValueFrom(this.get<string>(request, path)); - return dataToAlgorithms(resultAPI.data); + return transformToAlgorithms.evaluate(resultAPI.data); } - async getExperiment(id: string, request: Request): Promise<Experiment> { const path = this.options.baseurl + `experiments/${id}`; @@ -118,7 +131,7 @@ export default class ExaremeService implements IEngineService { return dataToExperiment(resultAPI.data); } - async editExperient( + async editExperiment( id: string, expriment: ExperimentEditInput, request: Request, @@ -148,29 +161,27 @@ export default class ExaremeService implements IEngineService { } } - async getDomains(ids: string[], request: Request): Promise<Domain[]> { + async getDomains(request: Request): Promise<Domain[]> { const path = this.options.baseurl + 'pathologies'; try { const data = await firstValueFrom(this.get<Pathology[]>(request, path)); return ( - data?.data - .filter((data) => !ids || ids.length == 0 || ids.includes(data.code)) - .map((data): Domain => { - const groups = this.flattenGroups(data.metadataHierarchy); - - return { - id: data.code, - label: data.label, - groups: groups, - rootGroup: dataToGroup(data.metadataHierarchy), - datasets: data.datasets ? data.datasets.map(dataToDataset) : [], - variables: data.metadataHierarchy - ? this.flattenVariables(data.metadataHierarchy, groups) - : [], - }; - }) ?? [] + data?.data.map((d): Domain => { + const groups = this.flattenGroups(d.metadataHierarchy); + + return { + id: d.code, + label: d.label, + groups: groups, + rootGroup: dataToGroup(d.metadataHierarchy), + datasets: d.datasets ? d.datasets.map(dataToDataset) : [], + variables: d.metadataHierarchy + ? this.flattenVariables(d.metadataHierarchy, groups) + : [], + }; + }) ?? [] ); } catch (error) { throw new HttpException( @@ -187,33 +198,23 @@ export default class ExaremeService implements IEngineService { try { return transformToUser.evaluate(response.data); } catch (e) { - new InternalServerErrorException('Cannot parse user data from Engine', e); + throw new InternalServerErrorException( + 'Cannot parse user data from Engine', + e, + ); } } async updateUser(request: Request): Promise<User> { const path = this.options.baseurl + 'activeUser/agreeNDA'; - const response = await firstValueFrom( + + const result = await firstValueFrom( this.post<string>(request, path, { agreeNDA: true, }), ); - try { - return transformToUser.evaluate(response.data); - } catch (e) { - throw new InternalServerErrorException( - 'Error when trying to parse user data from the engine', - ); - } - } - - getAlgorithmsREST(request: Request): Observable<string> { - const path = this.options.baseurl + 'algorithms'; - - return this.get<string>(request, path, { params: request.query }).pipe( - map((response) => response.data), - ); + return dataToUser.evaluate(result.data); } getPassthrough( @@ -239,7 +240,7 @@ export default class ExaremeService implements IEngineService { }; private flattenVariables = (data: Hierarchy, groups: Group[]): Variable[] => { - const group = groups.find((group) => group.id == data.code); + const group = groups.find((g) => g.id == data.code); let variables = data.variables ? data.variables.map(dataToVariable) : []; variables.forEach((variable) => (variable.groups = group ? [group] : [])); diff --git a/api/src/engine/connectors/exareme/handlers/algorithms/PCA.handler.spec.ts b/api/src/engine/connectors/exareme/handlers/algorithms/PCA.handler.spec.ts index a7af56efcc46540e8565e6130e5d7c70e420ee8e..d2a299b68ae5de8399e3c4116548d2303a04e971 100644 --- a/api/src/engine/connectors/exareme/handlers/algorithms/PCA.handler.spec.ts +++ b/api/src/engine/connectors/exareme/handlers/algorithms/PCA.handler.spec.ts @@ -7,7 +7,7 @@ const createExperiment = (): Experiment => ({ id: 'dummy-id', name: 'Testing purpose', algorithm: { - id: 'PCA', + name: 'PCA', }, datasets: ['desd-synthdata'], domain: 'dementia', @@ -78,7 +78,7 @@ describe('PCA result handler', () => { it('Test PCA handler with regular data (no edge cases)', () => { const exp = createExperiment(); - handlers(exp, data); + handlers(exp, data, null); expect(exp.results.length).toBeGreaterThanOrEqual(2); exp.results.forEach((it) => { diff --git a/api/src/engine/connectors/exareme/handlers/algorithms/PCA.handler.ts b/api/src/engine/connectors/exareme/handlers/algorithms/PCA.handler.ts index 64ff970285259fd69147fe2ff8fc0b3ca9eab668..61f82be6593f6165326387e228971236679e1d84 100644 --- a/api/src/engine/connectors/exareme/handlers/algorithms/PCA.handler.ts +++ b/api/src/engine/connectors/exareme/handlers/algorithms/PCA.handler.ts @@ -9,7 +9,8 @@ export default class PCAHandler extends BaseHandler { } handle(exp: Experiment, data: unknown): void { - if (!this.canHandle(exp.algorithm.id)) return this.next?.handle(exp, data); + if (!this.canHandle(exp.algorithm.name)) + return this.next?.handle(exp, data); const barChar: BarChartResult = { name: 'Eigen values', diff --git a/api/src/engine/connectors/exareme/handlers/algorithms/anova-one-way.handler.spec.ts b/api/src/engine/connectors/exareme/handlers/algorithms/anova-one-way.handler.spec.ts index c4cd6dcf6a9f3cd8c1d837b8a46b4d310842b874..0de41ca99cd43c14bfb389c58ee28934dcb49f78 100644 --- a/api/src/engine/connectors/exareme/handlers/algorithms/anova-one-way.handler.spec.ts +++ b/api/src/engine/connectors/exareme/handlers/algorithms/anova-one-way.handler.spec.ts @@ -6,7 +6,7 @@ const createExperiment = (): Experiment => ({ id: 'dummy-id', name: 'Testing purpose', algorithm: { - id: 'Anova_OnEway', + name: 'Anova_OnEway', }, datasets: ['desd-synthdata'], domain: 'dementia', @@ -140,7 +140,7 @@ describe('Anova oneway result handler', () => { const table2 = anovaHandler.getTuckeyTable(data); const meanPlot = anovaHandler.getMeanPlot(data); - handlers(exp, data); + handlers(exp, data, null); expect(exp.results.length).toBeGreaterThanOrEqual(3); expect(exp.results).toContainEqual(table1); diff --git a/api/src/engine/connectors/exareme/handlers/algorithms/anova-one-way.handler.ts b/api/src/engine/connectors/exareme/handlers/algorithms/anova-one-way.handler.ts index c8d9a6750785d57df2f2637572d306cd313935ee..d52d088f9234d00d5aa06329b9d7d49e71a002a8 100644 --- a/api/src/engine/connectors/exareme/handlers/algorithms/anova-one-way.handler.ts +++ b/api/src/engine/connectors/exareme/handlers/algorithms/anova-one-way.handler.ts @@ -1,4 +1,5 @@ import * as jsonata from 'jsonata'; // old import style needed due to 'export = jsonata' +import { Domain } from 'src/engine/models/domain.model'; import { MeanChartResult } from 'src/engine/models/result/means-chart-result.model'; import { Experiment } from '../../../../models/experiment/experiment.model'; import { @@ -97,8 +98,9 @@ export default class AnovaOneWayHandler extends BaseHandler { return AnovaOneWayHandler.meanPlotTransform.evaluate(data); } - handle(exp: Experiment, data: unknown): void { - if (!this.canHandle(exp.algorithm.id)) return super.handle(exp, data); + handle(exp: Experiment, data: unknown, domain: Domain): void { + if (!this.canHandle(exp.algorithm.name)) + return super.handle(exp, data, domain); const summaryTable = this.getSummaryTable(data, exp.coVariables[0]); if (summaryTable) exp.results.push(summaryTable); @@ -109,6 +111,6 @@ export default class AnovaOneWayHandler extends BaseHandler { const meanPlot = this.getMeanPlot(data); if (meanPlot && meanPlot.pointCIs) exp.results.push(meanPlot); - super.handle(exp, data); // continue request + return super.handle(exp, data, domain); // continue request } } diff --git a/api/src/engine/connectors/exareme/handlers/algorithms/area.handler.ts b/api/src/engine/connectors/exareme/handlers/algorithms/area.handler.ts deleted file mode 100644 index 6e096e31c85d37193089acc4039f2a949c2dfd2e..0000000000000000000000000000000000000000 --- a/api/src/engine/connectors/exareme/handlers/algorithms/area.handler.ts +++ /dev/null @@ -1,55 +0,0 @@ -import * as jsonata from 'jsonata'; // old import style needed due to 'export = jsonata' -import { Experiment } from '../../../../../engine/models/experiment/experiment.model'; -import { ResultChartExperiment } from '../../interfaces/experiment/result-chart-experiment.interface'; -import BaseHandler from '../base.handler'; - -export default class AreaHandler extends BaseHandler { - private static readonly transform = jsonata(` - ({ - "name": data.title.text, - "xAxis": { - "label": data.xAxis.title.text - }, - "yAxis": { - "label": data.yAxis.title.text - }, - "lines": [ - { - "label": "ROC curve", - "x": data.series.data.$[0], - "y": data.series.data.$[1], - "type": 0 - } - ] - }) - `); - - canHandle(input: ResultChartExperiment): boolean { - try { - return ( - input.type === 'application/vnd.highcharts+json' && - input.data?.chart?.type === 'area' - ); - } catch (e) { - AreaHandler.logger.log('Error when parsing input from experiment'); - AreaHandler.logger.debug(e); - return false; - } - } - - handle(exp: Experiment, data: unknown): void { - let req = data; - const inputs = data as ResultChartExperiment[]; - - if (inputs && Array.isArray(inputs)) { - inputs - .filter(this.canHandle) - .map((input) => AreaHandler.transform.evaluate(input)) - .forEach((input) => exp.results.push(input)); - - req = JSON.stringify(inputs.filter((input) => !this.canHandle(input))); - } - - this.next?.handle(exp, req); - } -} diff --git a/api/src/engine/connectors/exareme/handlers/algorithms/descriptive.handler.ts b/api/src/engine/connectors/exareme/handlers/algorithms/descriptive.handler.ts index d9fbc65f432ac29931d69428078d55e72eecd2a7..56396522d7a5504f218f586962d44cf81ac461b3 100644 --- a/api/src/engine/connectors/exareme/handlers/algorithms/descriptive.handler.ts +++ b/api/src/engine/connectors/exareme/handlers/algorithms/descriptive.handler.ts @@ -113,7 +113,7 @@ $fn := function($o, $prefix) { handle(exp: Experiment, data: unknown): void { let req = data; - if (exp.algorithm.id.toLowerCase() === 'descriptive_stats') { + if (exp.algorithm.name.toLowerCase() === 'descriptive_stats') { const inputs = data as ResultExperiment[]; if (inputs && Array.isArray(inputs)) { diff --git a/api/src/engine/connectors/exareme/handlers/algorithms/heat-map.handler.ts b/api/src/engine/connectors/exareme/handlers/algorithms/heat-map.handler.ts deleted file mode 100644 index 47d937a04a5e0223ba6b636cd5cfe7094076dc8a..0000000000000000000000000000000000000000 --- a/api/src/engine/connectors/exareme/handlers/algorithms/heat-map.handler.ts +++ /dev/null @@ -1,46 +0,0 @@ -import * as jsonata from 'jsonata'; // old import style needed due to 'export = jsonata' -import { Experiment } from '../../../../models/experiment/experiment.model'; -import { ResultChartExperiment } from '../../interfaces/experiment/result-chart-experiment.interface'; -import BaseHandler from '../base.handler'; - -export default class HeatMapHandler extends BaseHandler { - private static readonly transform = jsonata(` - ( - { - "name": data.title.text, - "xAxis": { - "categories": data.xAxis.categories, - "label": data.xAxis.label - }, - "yAxis": { - "categories": data.yAxis.categories, - "label": data.yAxis.label - }, - "matrix": $toMat(data.series.data) - } - ) - `); - - canHandle(input: ResultChartExperiment): boolean { - return ( - input.type.toLowerCase() === 'application/vnd.highcharts+json' && - input.data.chart.type.toLowerCase() === 'heatmap' - ); - } - - handle(exp: Experiment, data: unknown): void { - let req = data; - const inputs = data as ResultChartExperiment[]; - - if (inputs && Array.isArray(inputs)) { - inputs - .filter(this.canHandle) - .map((input) => HeatMapHandler.transform.evaluate(input)) - .forEach((input) => exp.results.push(input)); - - req = JSON.stringify(inputs.filter((input) => !this.canHandle(input))); - } - - this.next?.handle(exp, req); - } -} diff --git a/api/src/engine/connectors/exareme/handlers/algorithms/linear-regression-cv.handler.spec.ts b/api/src/engine/connectors/exareme/handlers/algorithms/linear-regression-cv.handler.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..f1a404cd7941f69101f2e59e641baedceefc6760 --- /dev/null +++ b/api/src/engine/connectors/exareme/handlers/algorithms/linear-regression-cv.handler.spec.ts @@ -0,0 +1,96 @@ +import { Domain } from 'src/engine/models/domain.model'; +import { TableResult } from 'src/engine/models/result/table-result.model'; +import { Experiment } from '../../../../models/experiment/experiment.model'; +import LinearRegressionCVHandler from './linear-regression-cv.handler'; + +const data = { + dependent_var: 'leftocpoccipitalpole', + indep_vars: [ + 'Intercept', + 'righthippocampus', + 'rightsogsuperioroccipitalgyrus', + 'leftppplanumpolare', + ], + n_obs: [497, 498, 498, 499], + mean_sq_error: [0.3296455054532643, 0.02930654997949175], + r_squared: [0.5886631959286948, 0.04365853383949705], + mean_abs_error: [0.2585157369288272, 0.019919123005319055], +}; + +const domain: Domain = { + id: 'dummy-id', + groups: [], + rootGroup: { + id: 'dummy-id', + }, + datasets: [{ id: 'desd-synthdata', label: 'Dead Synthdata' }], + variables: [ + { id: 'leftocpoccipitalpole', label: 'Left OCP occipital Pole' }, + { id: 'righthippocampus', label: 'Right Hippo Campus' }, + { id: 'rightsogsuperioroccipitalgyrus', label: 'Right superior occipital' }, + { id: 'leftppplanumpolare', label: 'Left Planum polare' }, + ], +}; + +const createExperiment = (): Experiment => ({ + id: 'dummy-id', + name: 'Testing purpose', + algorithm: { + name: 'LINEAR_REGRESSION_CROSS_VALIDATION', + }, + datasets: ['desd-synthdata'], + domain: 'dementia', + variables: ['leftocpoccipitalpole'], + coVariables: [ + 'righthippocampus', + 'rightsogsuperioroccipitalgyrus', + 'leftppplanumpolare', + ], + results: [], +}); + +describe('Linear regression CV result handler', () => { + let linearHandler: LinearRegressionCVHandler; + let experiment: Experiment; + + beforeEach(() => { + linearHandler = new LinearRegressionCVHandler(); + experiment = createExperiment(); + }); + + describe('Handle', () => { + it('with standard linear algo data', () => { + const expectedDataPoints = [ + ['Intercept', 497], + ['Right Hippo Campus', 498], + ['Right superior occipital', 498], + ['Left Planum polare', 499], + ]; + const expectedScoresData = [ + ['Root mean squared error', '0.3296', '0.02931'], + ['R-squared', '0.5887', '0.04366'], + ['Mean absolute error', '0.2585', '0.01992'], + ]; + + linearHandler.handle(experiment, data, domain); + + const json = JSON.stringify(experiment.results); + + const dataPoints = experiment.results[0] as TableResult; + const scoresData = experiment.results[1] as TableResult; + + expect(dataPoints.data).toStrictEqual(expectedDataPoints); + expect(scoresData.data).toStrictEqual(expectedScoresData); + + expect(json.includes(domain.variables[0].label)).toBeTruthy(); + expect(experiment.results.length === 2); + }); + + it('Should be empty with another algo', () => { + experiment.algorithm.name = 'dummy_algo'; + linearHandler.handle(experiment, data, domain); + + expect(experiment.results.length === 0); + }); + }); +}); diff --git a/api/src/engine/connectors/exareme/handlers/algorithms/linear-regression-cv.handler.ts b/api/src/engine/connectors/exareme/handlers/algorithms/linear-regression-cv.handler.ts new file mode 100644 index 0000000000000000000000000000000000000000..89f46631872eb16e193e5fdd14c1d0371d4a5d96 --- /dev/null +++ b/api/src/engine/connectors/exareme/handlers/algorithms/linear-regression-cv.handler.ts @@ -0,0 +1,81 @@ +import { Domain } from 'src/engine/models/domain.model'; +import { Variable } from 'src/engine/models/variable.model'; +import { isNumber } from '../../../../../common/utils/shared.utils'; +import { Experiment } from '../../../../models/experiment/experiment.model'; +import { + TableResult, + TableStyle, +} from '../../../../models/result/table-result.model'; +import BaseHandler from '../base.handler'; + +const NUMBER_PRECISION = 4; +const ALGO_NAME = 'linear_regression_cross_validation'; +const lookupDict = { + dependent_var: 'Dependent variable', + indep_vars: 'Independent variables', + n_obs: 'Number of observations', + mean_sq_error: 'Root mean squared error', + r_squared: 'R-squared', + mean_abs_error: 'Mean absolute error', +}; + +export default class LinearRegressionCVHandler extends BaseHandler { + private getModel(data: any): TableResult | undefined { + return { + name: 'Data points', + tableStyle: TableStyle.NORMAL, + headers: ['', `${lookupDict['n_obs']} (${data['dependent_var']})`].map( + (name) => ({ name, type: 'string' }), + ), + data: data['indep_vars'].map((name: string, i: number) => [ + name, + data['n_obs'][i], + ]), + }; + } + + private getScores(data: any): TableResult | undefined { + return { + name: 'Scores', + tableStyle: TableStyle.NORMAL, + headers: ['', 'Mean', 'Standard deviation'].map((name) => ({ + name: name, + type: 'string', + })), + data: ['mean_sq_error', 'r_squared', 'mean_abs_error'].map((variable) => [ + lookupDict[variable], + ...data[variable].map((val: unknown) => + isNumber(val) ? val.toPrecision(NUMBER_PRECISION) : val, + ), + ]), + }; + } + + getLabelFromVariableId(id: string, vars: Variable[]): string { + const varible = vars.find((v) => v.id === id); + return varible.label ?? id; + } + + handle(experiment: Experiment, data: any, domain: Domain): void { + if (experiment.algorithm.name.toLowerCase() !== ALGO_NAME) + return super.handle(experiment, data, domain); + + const varIds = [...experiment.variables, ...(experiment.coVariables ?? [])]; + const variables = domain.variables.filter((v) => varIds.includes(v.id)); + + let jsonData = JSON.stringify(data); + + variables.forEach((v) => { + const regEx = new RegExp(v.id, 'gi'); + jsonData = jsonData.replaceAll(regEx, v.label); + }); + + const improvedData = JSON.parse(jsonData); + + const model = this.getModel(improvedData); + if (model) experiment.results.push(model); + + const coefs = this.getScores(improvedData); + if (coefs) experiment.results.push(coefs); + } +} diff --git a/api/src/engine/connectors/exareme/handlers/algorithms/linear-regression.handler.spec.ts b/api/src/engine/connectors/exareme/handlers/algorithms/linear-regression.handler.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..c2d6561e6268d278e3da3eed70a8264c67033ac0 --- /dev/null +++ b/api/src/engine/connectors/exareme/handlers/algorithms/linear-regression.handler.spec.ts @@ -0,0 +1,78 @@ +import { Domain } from 'src/engine/models/domain.model'; +import { Experiment } from '../../../../models/experiment/experiment.model'; +import LinearRegressionHandler from './linear-regression.handler'; + +const data = { + dependent_var: 'lefthippocampus', + n_obs: 15431, + df_resid: 1540.0, + df_model: 2.0, + rse: 0.1270107560405171, + r_squared: 0.8772983534917347, + r_squared_adjusted: 0.8771390007040616, + f_stat: 5505.38441342865, + f_pvalue: 0.0, + indep_vars: ['Intercept', 'righthippocampus', 'leftamygdala'], + coefficients: [0.2185676251985193, 0.611894589820809, 1.0305204881766319], + std_err: [0.029052606790014847, 0.016978263425746872, 0.05180007458246667], + t_stats: [7.523167431352125, 36.03988078621131, 19.894189274496593], + pvalues: [ + 9.04278019740564e-14, 8.833386164556705e-207, 1.4580450464941301e-78, + ], + lower_ci: [0.16158077395909892, 0.5785916308422961, 0.9289143512210847], + upper_ci: [0.2755544764379397, 0.6451975487993219, 1.132126625132179], +}; + +const domain: Domain = { + id: 'dummy-id', + groups: [], + rootGroup: { + id: 'dummy-id', + }, + datasets: [{ id: 'desd-synthdata', label: 'Dead Synthdata' }], + variables: [ + { id: 'lefthippocampus', label: 'Left Hippo Campus' }, + { id: 'righthippocampus', label: 'Right Hippo Campus' }, + { id: 'leftamygdala', label: 'Left Amygdala' }, + ], +}; + +const createExperiment = (): Experiment => ({ + id: 'dummy-id', + name: 'Testing purpose', + algorithm: { + name: 'LINEAR_REGRESSION', + }, + datasets: ['desd-synthdata'], + domain: 'dementia', + variables: ['lefthippocampus'], + coVariables: ['righthippocampus', 'leftamygdala'], + results: [], +}); + +describe('Linear regression result handler', () => { + let linearHandler: LinearRegressionHandler; + let experiment: Experiment; + + beforeEach(() => { + linearHandler = new LinearRegressionHandler(); + experiment = createExperiment(); + }); + + describe('Handle', () => { + it('with standard linear algo data', () => { + linearHandler.handle(experiment, data, domain); + + const json = JSON.stringify(experiment.results); + + expect(json.includes(domain.variables[0].label)).toBeTruthy(); + expect(experiment.results.length === 2); + }); + it('Should be empty with another algo', () => { + experiment.algorithm.name = 'dummy_algo'; + linearHandler.handle(experiment, data, domain); + + expect(experiment.results.length === 0); + }); + }); +}); diff --git a/api/src/engine/connectors/exareme/handlers/algorithms/linear-regression.handler.ts b/api/src/engine/connectors/exareme/handlers/algorithms/linear-regression.handler.ts new file mode 100644 index 0000000000000000000000000000000000000000..b3b2801d9fa0d9fa88409786c7cd5dbfc2cef820 --- /dev/null +++ b/api/src/engine/connectors/exareme/handlers/algorithms/linear-regression.handler.ts @@ -0,0 +1,118 @@ +import { Domain } from 'src/engine/models/domain.model'; +import { Variable } from 'src/engine/models/variable.model'; +import { isNumber } from '../../../../../common/utils/shared.utils'; +import { Experiment } from '../../../../models/experiment/experiment.model'; +import { + TableResult, + TableStyle, +} from '../../../../models/result/table-result.model'; +import BaseHandler from '../base.handler'; + +const NUMBER_PRECISION = 4; +const ALGO_NAME = 'linear_regression'; +const lookupDict = { + dependent_var: 'Dependent variable', + n_obs: 'Number of observations', + df_resid: 'Residual degrees of freedom', + df_model: 'Model degrees of freedom', + rse: 'Residual standard error', + r_squared: 'R-squared', + r_squared_adjusted: 'Adjusted R-squared', + f_stat: 'F statistic', + f_pvalue: 'P{>F}', + indep_vars: 'Independent variables', + coefficients: 'Coefficients', + std_err: 'Std.Err.', + t_stats: 't-statistics', + pvalues: 'P{>|t|}', + lower_ci: 'Lower 95% c.i.', + upper_ci: 'Upper 95% c.i.', +}; + +export default class LinearRegressionHandler extends BaseHandler { + private getModel(data: any): TableResult | undefined { + const excepts = ['n_obs']; + const tableModel: TableResult = { + name: 'Model', + tableStyle: TableStyle.NORMAL, + headers: ['', 'name', 'value'].map((name) => ({ name, type: 'string' })), + data: [ + 'dependent_var', + 'n_obs', + 'df_resid', + 'df_model', + 'rse', + 'r_squared', + 'r_squared_adjusted', + 'f_stat', + 'f_pvalue', + ].map((name) => [ + lookupDict[name], + isNumber(data[name]) && !excepts.includes(name) + ? data[name].toPrecision(NUMBER_PRECISION) + : data[name], + ]), + }; + + return tableModel; + } + + private getCoefficients(data: any): TableResult | undefined { + const keys = [ + 'indep_vars', + 'coefficients', + 'std_err', + 't_stats', + 'pvalues', + 'lower_ci', + 'upper_ci', + ]; + const tabKeys = keys.slice(1); + + const tableCoef: TableResult = { + name: 'Coefficients', + tableStyle: TableStyle.NORMAL, + headers: ['', ...keys].map((name) => ({ + name: lookupDict[name], + type: 'string', + })), + data: data.indep_vars.map((variable, i) => { + const row = tabKeys + .map((key) => data[key][i]) + .map((val) => + isNumber(val) ? val.toPrecision(NUMBER_PRECISION) : val, + ); + row.unshift(variable); + return row; + }), + }; + + return tableCoef; + } + + getLabelFromVariableId(id: string, vars: Variable[]): string { + const varible = vars.find((v) => v.id === id); + return varible.label ?? id; + } + + handle(experiment: Experiment, data: any, domain: Domain): void { + if (experiment.algorithm.name.toLowerCase() !== ALGO_NAME) + return super.handle(experiment, data, domain); + + const varIds = [...experiment.variables, ...(experiment.coVariables ?? [])]; + const variables = domain.variables.filter((v) => varIds.includes(v.id)); + + let jsonData = JSON.stringify(data); + variables.forEach((v) => { + const regEx = new RegExp(v.id, 'gi'); + jsonData = jsonData.replaceAll(regEx, v.label); + }); + const improvedData = JSON.parse(jsonData); + + const model = this.getModel(improvedData); + if (model) experiment.results.push(model); + + const coefs = this.getCoefficients(improvedData); + if (coefs) experiment.results.push(coefs); + } +} diff --git a/api/src/engine/connectors/exareme/handlers/algorithms/pearson.handler.spec.ts b/api/src/engine/connectors/exareme/handlers/algorithms/pearson.handler.spec.ts index f3ca9a5a31aae0db39f78813c63013071b840ce0..17abd4315ce007d48502d9924416720d50b2ba91 100644 --- a/api/src/engine/connectors/exareme/handlers/algorithms/pearson.handler.spec.ts +++ b/api/src/engine/connectors/exareme/handlers/algorithms/pearson.handler.spec.ts @@ -6,7 +6,7 @@ const createExperiment = (): Experiment => ({ id: 'dummy-id', name: 'Testing purpose', algorithm: { - id: 'pearson', + name: 'pearson', }, datasets: ['desd-synthdata'], domain: 'dementia', @@ -423,7 +423,7 @@ describe('Pearson result handler', () => { 'high_confidence_intervals', ]; - handlers(exp, data); + handlers(exp, data, null); const results = exp.results as HeatMapResult[]; const heatmaps = names.map((name) => diff --git a/api/src/engine/connectors/exareme/handlers/algorithms/pearson.handler.ts b/api/src/engine/connectors/exareme/handlers/algorithms/pearson.handler.ts index 0832d960e4ceb8bcf29551e6aaf98ebf9ffff2a5..40248088dcbb5aeff0a6b9b822d093743ed0607c 100644 --- a/api/src/engine/connectors/exareme/handlers/algorithms/pearson.handler.ts +++ b/api/src/engine/connectors/exareme/handlers/algorithms/pearson.handler.ts @@ -41,7 +41,7 @@ export default class PearsonHandler extends BaseHandler { * @returns */ handle(exp: Experiment, data: unknown): void { - if (this.canHandle(exp.algorithm.id)) { + if (this.canHandle(exp.algorithm.name)) { try { const results = PearsonHandler.transform.evaluate( data, diff --git a/api/src/engine/connectors/exareme/handlers/algorithms/raw.handler.ts b/api/src/engine/connectors/exareme/handlers/algorithms/raw.handler.ts index 2e0dd77d9f86138dca531e1a0680286ba03fb40c..08cdac953300a1a06dee98568e37268a1999298c 100644 --- a/api/src/engine/connectors/exareme/handlers/algorithms/raw.handler.ts +++ b/api/src/engine/connectors/exareme/handlers/algorithms/raw.handler.ts @@ -21,7 +21,7 @@ export default class RawHandler extends BaseHandler { if (inputs && Array.isArray(inputs)) inputs .filter((input) => !!input.data && !!input.type) - .map((input) => this.dataToRaw(exp.algorithm.id, input)) + .map((input) => this.dataToRaw(exp.algorithm.name, input)) .forEach((input) => exp.results.push(input)); this.next?.handle(exp, data); diff --git a/api/src/engine/connectors/exareme/handlers/base.handler.ts b/api/src/engine/connectors/exareme/handlers/base.handler.ts index 6f84123e0514cfe937c48366bccbacd7763d99cb..1aa3d2f6391922b10e1d9b29bf9c9a74fef0d575 100644 --- a/api/src/engine/connectors/exareme/handlers/base.handler.ts +++ b/api/src/engine/connectors/exareme/handlers/base.handler.ts @@ -1,4 +1,5 @@ import { Logger } from '@nestjs/common'; +import { Domain } from 'src/engine/models/domain.model'; import { Experiment } from '../../../models/experiment/experiment.model'; import ResultHandler from './result-handler.interface'; @@ -12,7 +13,7 @@ export default abstract class BaseHandler implements ResultHandler { return h; } - handle(partialExperiment: Experiment, data: unknown): void { - this.next?.handle(partialExperiment, data); + handle(experiment: Experiment, data: unknown, domain: Domain): void { + this.next?.handle(experiment, data, domain); } } diff --git a/api/src/engine/connectors/exareme/handlers/index.ts b/api/src/engine/connectors/exareme/handlers/index.ts index 2d7d607d5dac7ebc553536c30f1a3b4c2acd6514..ad4dfb9e0789eb8a8acd4145446aad8825ca9e27 100644 --- a/api/src/engine/connectors/exareme/handlers/index.ts +++ b/api/src/engine/connectors/exareme/handlers/index.ts @@ -1,23 +1,25 @@ +import { Domain } from 'src/engine/models/domain.model'; import { Experiment } from '../../../../engine/models/experiment/experiment.model'; import AnovaOneWayHandler from './algorithms/anova-one-way.handler'; -import AreaHandler from './algorithms/area.handler'; import DescriptiveHandler from './algorithms/descriptive.handler'; -import HeatMapHandler from './algorithms/heat-map.handler'; +import LinearRegressionCVHandler from './algorithms/linear-regression-cv.handler'; +import LinearRegressionHandler from './algorithms/linear-regression.handler'; import PCAHandler from './algorithms/PCA.handler'; import PearsonHandler from './algorithms/pearson.handler'; import RawHandler from './algorithms/raw.handler'; +import ResultHandler from './result-handler.interface'; -const start = new PearsonHandler(); +const start = new PearsonHandler() as ResultHandler; start - .setNext(new AreaHandler()) .setNext(new DescriptiveHandler()) - .setNext(new HeatMapHandler()) .setNext(new AnovaOneWayHandler()) .setNext(new PCAHandler()) + .setNext(new LinearRegressionHandler()) + .setNext(new LinearRegressionCVHandler()) .setNext(new RawHandler()); // should be last handler as it works as a fallback (if other handlers could not process the results) -export default (exp: Experiment, data: unknown): Experiment => { - start.handle(exp, data); +export default (exp: Experiment, data: unknown, domain: Domain): Experiment => { + start.handle(exp, data, domain); return exp; }; diff --git a/api/src/engine/connectors/exareme/handlers/result-handler.interface.ts b/api/src/engine/connectors/exareme/handlers/result-handler.interface.ts index 661c2a437015b8a75950394a2f27380602cbb25b..3c82f7d32ca56246281bcb09c996565d77a518eb 100644 --- a/api/src/engine/connectors/exareme/handlers/result-handler.interface.ts +++ b/api/src/engine/connectors/exareme/handlers/result-handler.interface.ts @@ -1,7 +1,8 @@ +import { Domain } from 'src/engine/models/domain.model'; import { Experiment } from '../../../models/experiment/experiment.model'; // produce algo handler export default interface ResultHandler { setNext(h: ResultHandler): ResultHandler; - handle(partialExperiment: Experiment, data: unknown): void; + handle(partialExperiment: Experiment, data: unknown, domain?: Domain): void; } diff --git a/api/src/engine/connectors/exareme/interfaces/test-utilities.ts b/api/src/engine/connectors/exareme/interfaces/test-utilities.ts index 2ad1c79e8384dfb1a12fe8dbe75511720dfe4e1f..26596046209d10901b4467ca5c778baa17cca69b 100644 --- a/api/src/engine/connectors/exareme/interfaces/test-utilities.ts +++ b/api/src/engine/connectors/exareme/interfaces/test-utilities.ts @@ -1,6 +1,6 @@ -import { IEngineService } from 'src/engine/engine.interfaces'; -import { Experiment } from 'src/engine/models/experiment/experiment.model'; -import { ExperimentCreateInput } from 'src/engine/models/experiment/input/experiment-create.input'; +import EngineService from '../../../engine.service'; +import { Experiment } from '../../../models/experiment/experiment.model'; +import { ExperimentCreateInput } from '../../../../experiments/models/input/experiment-create.input'; const TIMEOUT_DURATION_SECONDS = 60 * 10; @@ -28,14 +28,14 @@ const TEST_PATHOLOGIES = { const createExperiment = async ( input: ExperimentCreateInput, - service: IEngineService, + service: EngineService, ): Promise<Experiment | undefined> => { - return await service.createExperiment(input, false); + return service.createExperiment(input, false); }; const waitForResult = ( id: string, - service: IEngineService, + service: EngineService, ): Promise<Experiment> => new Promise((resolve, reject) => { let elapsed = 0; diff --git a/api/src/engine/connectors/exareme/tests/e2e/3c.e2e-spec.ts b/api/src/engine/connectors/exareme/tests/e2e/3c.e2e-spec.ts index 8770e901a69a2cfbb5575f079e2d7c65bcd32b0f..e64ca3e15f6eb8d66a83ef6baad8628bbf2a642e 100644 --- a/api/src/engine/connectors/exareme/tests/e2e/3c.e2e-spec.ts +++ b/api/src/engine/connectors/exareme/tests/e2e/3c.e2e-spec.ts @@ -1,8 +1,6 @@ import { Test, TestingModule } from '@nestjs/testing'; import { AppModule } from '../../../../../main/app.module'; -import { ENGINE_SERVICE } from '../../../../engine.constants'; -import { IEngineService } from '../../../../engine.interfaces'; -import { ExperimentCreateInput } from '../../../../models/experiment/input/experiment-create.input'; +import { ExperimentCreateInput } from '../../../../../experiments/models/input/experiment-create.input'; import { createExperiment, generateNumber, @@ -10,18 +8,19 @@ import { TIMEOUT_DURATION_SECONDS, waitForResult, } from '../../interfaces/test-utilities'; +import EngineService from '../../../../../engine/engine.service'; jest.setTimeout(1000 * TIMEOUT_DURATION_SECONDS); describe('ExaremeService', () => { - let exaremeService: IEngineService; + let exaremeService: EngineService; beforeEach(async () => { const moduleRef: TestingModule = await Test.createTestingModule({ imports: [AppModule], }).compile(); - exaremeService = await moduleRef.resolve<IEngineService>(ENGINE_SERVICE); + exaremeService = await moduleRef.resolve<EngineService>(EngineService); }); const modelSlug = `3c-${generateNumber()}`; diff --git a/api/src/engine/connectors/exareme/tests/e2e/calibration-belt.e2e-spec.ts b/api/src/engine/connectors/exareme/tests/e2e/calibration-belt.e2e-spec.ts index 447b8f457dafe655ee12d7d518bcf8b0fc7d37c7..254e03e012adf6fa8fced8486864034a04830e98 100644 --- a/api/src/engine/connectors/exareme/tests/e2e/calibration-belt.e2e-spec.ts +++ b/api/src/engine/connectors/exareme/tests/e2e/calibration-belt.e2e-spec.ts @@ -1,8 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; +import EngineService from '../../../../../engine/engine.service'; +import { ExperimentCreateInput } from '../../../../../experiments/models/input/experiment-create.input'; import { AppModule } from '../../../../../main/app.module'; -import { ENGINE_SERVICE } from '../../../../engine.constants'; -import { IEngineService } from '../../../../engine.interfaces'; -import { ExperimentCreateInput } from '../../../../models/experiment/input/experiment-create.input'; import { createExperiment, generateNumber, @@ -14,14 +13,14 @@ import { jest.setTimeout(1000 * TIMEOUT_DURATION_SECONDS); describe('ExaremeService', () => { - let exaremeService: IEngineService; + let exaremeService: EngineService; beforeEach(async () => { const moduleRef: TestingModule = await Test.createTestingModule({ imports: [AppModule], }).compile(); - exaremeService = await moduleRef.resolve<IEngineService>(ENGINE_SERVICE); + exaremeService = await moduleRef.resolve<EngineService>(EngineService); }); const modelSlug = `calibration-belt-${generateNumber()}`; diff --git a/api/src/engine/connectors/exareme/tests/e2e/cart.e2e-spec.ts b/api/src/engine/connectors/exareme/tests/e2e/cart.e2e-spec.ts index f8b0085449c7a0229044d66cd73b09a727149ce1..639f1d3085b3563e86b32a779559fa221bad41cd 100644 --- a/api/src/engine/connectors/exareme/tests/e2e/cart.e2e-spec.ts +++ b/api/src/engine/connectors/exareme/tests/e2e/cart.e2e-spec.ts @@ -1,9 +1,8 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { RawResult } from 'src/engine/models/result/raw-result.model'; +import EngineService from '../../../../../engine/engine.service'; +import { ExperimentCreateInput } from '../../../../../experiments/models/input/experiment-create.input'; import { AppModule } from '../../../../../main/app.module'; -import { ENGINE_SERVICE } from '../../../../engine.constants'; -import { IEngineService } from '../../../../engine.interfaces'; -import { ExperimentCreateInput } from '../../../../models/experiment/input/experiment-create.input'; +import { RawResult } from '../../../../models/result/raw-result.model'; import { createExperiment, generateNumber, @@ -15,14 +14,14 @@ import { jest.setTimeout(1000 * TIMEOUT_DURATION_SECONDS); describe('ExaremeService', () => { - let exaremeService: IEngineService; + let exaremeService: EngineService; beforeEach(async () => { const moduleRef: TestingModule = await Test.createTestingModule({ imports: [AppModule], }).compile(); - exaremeService = await moduleRef.resolve<IEngineService>(ENGINE_SERVICE); + exaremeService = await moduleRef.resolve<EngineService>(EngineService); }); const modelSlug = `cart-${generateNumber()}`; const algorithmId = 'CART'; diff --git a/api/src/engine/connectors/exareme/tests/e2e/descriptiveStatistics.e2e-spec.ts b/api/src/engine/connectors/exareme/tests/e2e/descriptiveStatistics.e2e-spec.ts index 5808b159dae93067640e66ade63f82a15ab3debb..3d549536f2e7a6b56f017e8122e4f110cb594db2 100644 --- a/api/src/engine/connectors/exareme/tests/e2e/descriptiveStatistics.e2e-spec.ts +++ b/api/src/engine/connectors/exareme/tests/e2e/descriptiveStatistics.e2e-spec.ts @@ -1,10 +1,9 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { GroupsResult } from 'src/engine/models/result/groups-result.model'; -import { TableResult } from 'src/engine/models/result/table-result.model'; +import EngineService from '../../../../../engine/engine.service'; +import { ExperimentCreateInput } from '../../../../../experiments/models/input/experiment-create.input'; import { AppModule } from '../../../../../main/app.module'; -import { ENGINE_SERVICE } from '../../../../engine.constants'; -import { IEngineService } from '../../../../engine.interfaces'; -import { ExperimentCreateInput } from '../../../../models/experiment/input/experiment-create.input'; +import { GroupsResult } from '../../../../models/result/groups-result.model'; +import { TableResult } from '../../../../models/result/table-result.model'; import { createExperiment, generateNumber, @@ -16,14 +15,14 @@ import { jest.setTimeout(1000 * TIMEOUT_DURATION_SECONDS); describe('ExaremeService', () => { - let exaremeService: IEngineService; + let exaremeService: EngineService; beforeEach(async () => { const moduleRef: TestingModule = await Test.createTestingModule({ imports: [AppModule], }).compile(); - exaremeService = await moduleRef.resolve<IEngineService>(ENGINE_SERVICE); + exaremeService = await moduleRef.resolve<EngineService>(EngineService); }); const modelSlug = `statistics-${generateNumber()}`; const algorithmId = 'DESCRIPTIVE_STATS'; diff --git a/api/src/engine/connectors/exareme/tests/e2e/id3.e2e-spec.ts b/api/src/engine/connectors/exareme/tests/e2e/id3.e2e-spec.ts index 4ef963a6ca5ab93cbe07bf0ff422e40a94744413..922734f5fe6d9ab089e181a1c34d5f7e96853a79 100644 --- a/api/src/engine/connectors/exareme/tests/e2e/id3.e2e-spec.ts +++ b/api/src/engine/connectors/exareme/tests/e2e/id3.e2e-spec.ts @@ -1,9 +1,8 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { RawResult } from 'src/engine/models/result/raw-result.model'; +import EngineService from '../../../../../engine/engine.service'; +import { ExperimentCreateInput } from '../../../../../experiments/models/input/experiment-create.input'; import { AppModule } from '../../../../../main/app.module'; -import { ENGINE_SERVICE } from '../../../../engine.constants'; -import { IEngineService } from '../../../../engine.interfaces'; -import { ExperimentCreateInput } from '../../../../models/experiment/input/experiment-create.input'; +import { RawResult } from '../../../../models/result/raw-result.model'; import { createExperiment, generateNumber, @@ -15,14 +14,14 @@ import { jest.setTimeout(1000 * TIMEOUT_DURATION_SECONDS); describe('ExaremeService', () => { - let exaremeService: IEngineService; + let exaremeService: EngineService; beforeEach(async () => { const moduleRef: TestingModule = await Test.createTestingModule({ imports: [AppModule], }).compile(); - exaremeService = await moduleRef.resolve<IEngineService>(ENGINE_SERVICE); + exaremeService = await moduleRef.resolve<EngineService>(EngineService); }); const modelSlug = `id3-${generateNumber()}`; const algorithmId = 'ID3'; diff --git a/api/src/engine/connectors/exareme/tests/e2e/k-means.e2e-spec.ts b/api/src/engine/connectors/exareme/tests/e2e/k-means.e2e-spec.ts index 57054721ff0675b01d5865ca312bcaa5335e143f..7253261295736adce6de2de271ec35f2d2ce936a 100644 --- a/api/src/engine/connectors/exareme/tests/e2e/k-means.e2e-spec.ts +++ b/api/src/engine/connectors/exareme/tests/e2e/k-means.e2e-spec.ts @@ -1,9 +1,8 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { RawResult } from 'src/engine/models/result/raw-result.model'; +import EngineService from '../../../../../engine/engine.service'; +import { ExperimentCreateInput } from '../../../../../experiments/models/input/experiment-create.input'; import { AppModule } from '../../../../../main/app.module'; -import { ENGINE_SERVICE } from '../../../../engine.constants'; -import { IEngineService } from '../../../../engine.interfaces'; -import { ExperimentCreateInput } from '../../../../models/experiment/input/experiment-create.input'; +import { RawResult } from '../../../../models/result/raw-result.model'; import { createExperiment, generateNumber, @@ -15,14 +14,14 @@ import { jest.setTimeout(1000 * TIMEOUT_DURATION_SECONDS); describe('ExaremeService', () => { - let exaremeService: IEngineService; + let exaremeService: EngineService; beforeEach(async () => { const moduleRef: TestingModule = await Test.createTestingModule({ imports: [AppModule], }).compile(); - exaremeService = await moduleRef.resolve<IEngineService>(ENGINE_SERVICE); + exaremeService = await moduleRef.resolve<EngineService>(EngineService); }); const modelSlug = `kmeans-${generateNumber()}`; const algorithmId = 'KMEANS'; diff --git a/api/src/engine/connectors/exareme/tests/e2e/kaplan-meier.e2e-spec.ts b/api/src/engine/connectors/exareme/tests/e2e/kaplan-meier.e2e-spec.ts index b0766164343410b79c98588100c3a43d037b4df1..e0e6e41f33b2308ff1395d9b5553376a27f1472a 100644 --- a/api/src/engine/connectors/exareme/tests/e2e/kaplan-meier.e2e-spec.ts +++ b/api/src/engine/connectors/exareme/tests/e2e/kaplan-meier.e2e-spec.ts @@ -1,8 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; +import EngineService from '../../../../../engine/engine.service'; +import { ExperimentCreateInput } from '../../../../../experiments/models/input/experiment-create.input'; import { AppModule } from '../../../../../main/app.module'; -import { ENGINE_SERVICE } from '../../../../engine.constants'; -import { IEngineService } from '../../../../engine.interfaces'; -import { ExperimentCreateInput } from '../../../../models/experiment/input/experiment-create.input'; import { createExperiment, generateNumber, @@ -14,14 +13,14 @@ import { jest.setTimeout(1000 * TIMEOUT_DURATION_SECONDS); describe('ExaremeService', () => { - let exaremeService: IEngineService; + let exaremeService: EngineService; beforeEach(async () => { const moduleRef: TestingModule = await Test.createTestingModule({ imports: [AppModule], }).compile(); - exaremeService = await moduleRef.resolve<IEngineService>(ENGINE_SERVICE); + exaremeService = await moduleRef.resolve<EngineService>(EngineService); }); const modelSlug = `kaplan-meier-${generateNumber()}`; const algorithmId = 'KAPLAN_MEIER'; diff --git a/api/src/engine/connectors/exareme/tests/e2e/linear-regression.e2e-spec.ts b/api/src/engine/connectors/exareme/tests/e2e/linear-regression.e2e-spec.ts index 4ed7f222182726313aaaafde67af362e4f748cc1..928ee3b790586126acdf01b6dfc0461b67a6d975 100644 --- a/api/src/engine/connectors/exareme/tests/e2e/linear-regression.e2e-spec.ts +++ b/api/src/engine/connectors/exareme/tests/e2e/linear-regression.e2e-spec.ts @@ -1,9 +1,8 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { RawResult } from 'src/engine/models/result/raw-result.model'; +import EngineService from '../../../../../engine/engine.service'; +import { ExperimentCreateInput } from '../../../../../experiments/models/input/experiment-create.input'; import { AppModule } from '../../../../../main/app.module'; -import { ENGINE_SERVICE } from '../../../../engine.constants'; -import { IEngineService } from '../../../../engine.interfaces'; -import { ExperimentCreateInput } from '../../../../models/experiment/input/experiment-create.input'; +import { RawResult } from '../../../../models/result/raw-result.model'; import { createExperiment, generateNumber, @@ -15,14 +14,14 @@ import { jest.setTimeout(1000 * TIMEOUT_DURATION_SECONDS); describe('ExaremeService', () => { - let exaremeService: IEngineService; + let exaremeService: EngineService; beforeEach(async () => { const moduleRef: TestingModule = await Test.createTestingModule({ imports: [AppModule], }).compile(); - exaremeService = await moduleRef.resolve<IEngineService>(ENGINE_SERVICE); + exaremeService = await moduleRef.resolve<EngineService>(EngineService); }); const modelSlug = `linear-${generateNumber()}`; const algorithmId = 'LINEAR_REGRESSION'; diff --git a/api/src/engine/connectors/exareme/tests/e2e/logistic-regression.e2e-spec.ts b/api/src/engine/connectors/exareme/tests/e2e/logistic-regression.e2e-spec.ts index abc045cd32efb98cd61908c77bd5a122719d7a41..9ca333a190ae0b4419adc04712bc32f4150de18f 100644 --- a/api/src/engine/connectors/exareme/tests/e2e/logistic-regression.e2e-spec.ts +++ b/api/src/engine/connectors/exareme/tests/e2e/logistic-regression.e2e-spec.ts @@ -1,9 +1,8 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { RawResult } from 'src/engine/models/result/raw-result.model'; +import EngineService from '../../../../../engine/engine.service'; +import { ExperimentCreateInput } from '../../../../../experiments/models/input/experiment-create.input'; import { AppModule } from '../../../../../main/app.module'; -import { ENGINE_SERVICE } from '../../../../engine.constants'; -import { IEngineService } from '../../../../engine.interfaces'; -import { ExperimentCreateInput } from '../../../../models/experiment/input/experiment-create.input'; +import { RawResult } from '../../../../models/result/raw-result.model'; import { createExperiment, generateNumber, @@ -15,14 +14,14 @@ import { jest.setTimeout(1000 * TIMEOUT_DURATION_SECONDS); describe('ExaremeService', () => { - let exaremeService: IEngineService; + let exaremeService: EngineService; beforeEach(async () => { const moduleRef: TestingModule = await Test.createTestingModule({ imports: [AppModule], }).compile(); - exaremeService = await moduleRef.resolve<IEngineService>(ENGINE_SERVICE); + exaremeService = await moduleRef.resolve<EngineService>(EngineService); }); const modelSlug = `logistic-${generateNumber()}`; const algorithmId = 'LOGISTIC_REGRESSION'; diff --git a/api/src/engine/connectors/exareme/tests/e2e/multiple-histograms.e2e-spec.ts b/api/src/engine/connectors/exareme/tests/e2e/multiple-histograms.e2e-spec.ts index 7604dd93bd27b021f0437fd5c87304135b80957d..f1ad948db341733b4892aa95ddbcbfe709381030 100644 --- a/api/src/engine/connectors/exareme/tests/e2e/multiple-histograms.e2e-spec.ts +++ b/api/src/engine/connectors/exareme/tests/e2e/multiple-histograms.e2e-spec.ts @@ -1,8 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; +import EngineService from '../../../../../engine/engine.service'; +import { ExperimentCreateInput } from '../../../../../experiments/models/input/experiment-create.input'; import { AppModule } from '../../../../../main/app.module'; -import { ENGINE_SERVICE } from '../../../../engine.constants'; -import { IEngineService } from '../../../../engine.interfaces'; -import { ExperimentCreateInput } from '../../../../models/experiment/input/experiment-create.input'; import { createExperiment, generateNumber, @@ -14,14 +13,14 @@ import { jest.setTimeout(1000 * TIMEOUT_DURATION_SECONDS); describe('ExaremeService', () => { - let exaremeService: IEngineService; + let exaremeService: EngineService; beforeEach(async () => { const moduleRef: TestingModule = await Test.createTestingModule({ imports: [AppModule], }).compile(); - exaremeService = await moduleRef.resolve<IEngineService>(ENGINE_SERVICE); + exaremeService = await moduleRef.resolve<EngineService>(EngineService); }); const modelSlug = `histograms-${generateNumber()}`; const algorithmId = 'MULTIPLE_HISTOGRAMS'; diff --git a/api/src/engine/connectors/exareme/tests/e2e/naive-bayes.e2e-spec.ts b/api/src/engine/connectors/exareme/tests/e2e/naive-bayes.e2e-spec.ts index 024e41227e8642e6896882d09213b02b7510ec25..893a7af2f7cc1ae2e5e36fe932df0fb1ba81a55c 100644 --- a/api/src/engine/connectors/exareme/tests/e2e/naive-bayes.e2e-spec.ts +++ b/api/src/engine/connectors/exareme/tests/e2e/naive-bayes.e2e-spec.ts @@ -1,8 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; +import EngineService from '../../../../../engine/engine.service'; +import { ExperimentCreateInput } from '../../../../../experiments/models/input/experiment-create.input'; import { AppModule } from '../../../../../main/app.module'; -import { ENGINE_SERVICE } from '../../../../engine.constants'; -import { IEngineService } from '../../../../engine.interfaces'; -import { ExperimentCreateInput } from '../../../../models/experiment/input/experiment-create.input'; import { RawResult } from '../../../../models/result/raw-result.model'; import { createExperiment, @@ -15,14 +14,14 @@ import { jest.setTimeout(1000 * TIMEOUT_DURATION_SECONDS); describe('ExaremeService', () => { - let exaremeService: IEngineService; + let exaremeService: EngineService; beforeEach(async () => { const moduleRef: TestingModule = await Test.createTestingModule({ imports: [AppModule], }).compile(); - exaremeService = await moduleRef.resolve<IEngineService>(ENGINE_SERVICE); + exaremeService = await moduleRef.resolve<EngineService>(EngineService); }); const modelSlug = `naivebayes-${generateNumber()}`; const algorithmId = 'NAIVE_BAYES'; diff --git a/api/src/engine/connectors/exareme/tests/e2e/one-way-anova.e2e-spec.ts b/api/src/engine/connectors/exareme/tests/e2e/one-way-anova.e2e-spec.ts index e8361797cd5805a1f84e4e8601d110e8ef344a9e..54b9b31a10d8f94facd43c1bf8d8a8bffeea1661 100644 --- a/api/src/engine/connectors/exareme/tests/e2e/one-way-anova.e2e-spec.ts +++ b/api/src/engine/connectors/exareme/tests/e2e/one-way-anova.e2e-spec.ts @@ -1,9 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; +import EngineService from '../../../../../engine/engine.service'; +import { ExperimentCreateInput } from '../../../../../experiments/models/input/experiment-create.input'; import { AppModule } from '../../../../../main/app.module'; -import { ENGINE_SERVICE } from '../../../../engine.constants'; -import { IEngineService } from '../../../../engine.interfaces'; -import { ExperimentCreateInput } from '../../../../models/experiment/input/experiment-create.input'; -import { RawResult } from '../../../../models/result/raw-result.model'; import { createExperiment, generateNumber, @@ -15,14 +13,14 @@ import { jest.setTimeout(1000 * TIMEOUT_DURATION_SECONDS); describe('ExaremeService', () => { - let exaremeService: IEngineService; + let exaremeService: EngineService; beforeEach(async () => { const moduleRef: TestingModule = await Test.createTestingModule({ imports: [AppModule], }).compile(); - exaremeService = await moduleRef.resolve<IEngineService>(ENGINE_SERVICE); + exaremeService = await moduleRef.resolve<EngineService>(EngineService); }); const modelSlug = `anova-1way-${generateNumber()}`; const algorithmId = 'ANOVA_ONEWAY'; diff --git a/api/src/engine/connectors/exareme/tests/e2e/pca.e2e-spec.ts b/api/src/engine/connectors/exareme/tests/e2e/pca.e2e-spec.ts index 79f65b81638c405455cdcf2288ce4fd46a6b3a59..9c23cbbb9bf454c55c15c293c41d0adb1ccc36f6 100644 --- a/api/src/engine/connectors/exareme/tests/e2e/pca.e2e-spec.ts +++ b/api/src/engine/connectors/exareme/tests/e2e/pca.e2e-spec.ts @@ -1,8 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; +import EngineService from '../../../../../engine/engine.service'; +import { ExperimentCreateInput } from '../../../../../experiments/models/input/experiment-create.input'; import { AppModule } from '../../../../../main/app.module'; -import { ENGINE_SERVICE } from '../../../../engine.constants'; -import { IEngineService } from '../../../../engine.interfaces'; -import { ExperimentCreateInput } from '../../../../models/experiment/input/experiment-create.input'; import { RawResult } from '../../../../models/result/raw-result.model'; import { createExperiment, @@ -15,14 +14,14 @@ import { jest.setTimeout(1000 * TIMEOUT_DURATION_SECONDS); describe('ExaremeService', () => { - let exaremeService: IEngineService; + let exaremeService: EngineService; beforeEach(async () => { const moduleRef: TestingModule = await Test.createTestingModule({ imports: [AppModule], }).compile(); - exaremeService = await moduleRef.resolve<IEngineService>(ENGINE_SERVICE); + exaremeService = await moduleRef.resolve<EngineService>(EngineService); }); const modelSlug = `pca-${generateNumber()}`; const algorithmId = 'PCA'; diff --git a/api/src/engine/connectors/exareme/tests/e2e/pearson-correlation.e2e-spec.ts b/api/src/engine/connectors/exareme/tests/e2e/pearson-correlation.e2e-spec.ts index e1617dd43649650676c7eb41f7c3227158a1bec2..dd36eac8d92b0a60f1e2563c8a1d55e56052eabf 100644 --- a/api/src/engine/connectors/exareme/tests/e2e/pearson-correlation.e2e-spec.ts +++ b/api/src/engine/connectors/exareme/tests/e2e/pearson-correlation.e2e-spec.ts @@ -1,8 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; +import EngineService from '../../../../../engine/engine.service'; +import { ExperimentCreateInput } from '../../../../../experiments/models/input/experiment-create.input'; import { AppModule } from '../../../../../main/app.module'; -import { ENGINE_SERVICE } from '../../../../engine.constants'; -import { IEngineService } from '../../../../engine.interfaces'; -import { ExperimentCreateInput } from '../../../../models/experiment/input/experiment-create.input'; import { RawResult } from '../../../../models/result/raw-result.model'; import { createExperiment, @@ -15,14 +14,14 @@ import { jest.setTimeout(1000 * TIMEOUT_DURATION_SECONDS); describe('ExaremeService', () => { - let exaremeService: IEngineService; + let exaremeService: EngineService; beforeEach(async () => { const moduleRef: TestingModule = await Test.createTestingModule({ imports: [AppModule], }).compile(); - exaremeService = await moduleRef.resolve<IEngineService>(ENGINE_SERVICE); + exaremeService = await moduleRef.resolve<EngineService>(EngineService); }); const modelSlug = `pearson-${generateNumber()}`; const algorithmId = 'PEARSON_CORRELATION'; diff --git a/api/src/engine/connectors/exareme/tests/e2e/t-test-independant.e2e-spec.ts b/api/src/engine/connectors/exareme/tests/e2e/t-test-independant.e2e-spec.ts index 2dee1858887a161e31f114f0343660847b676215..a15c9ace53b6292dfab0a02fb59edc20656ff27c 100644 --- a/api/src/engine/connectors/exareme/tests/e2e/t-test-independant.e2e-spec.ts +++ b/api/src/engine/connectors/exareme/tests/e2e/t-test-independant.e2e-spec.ts @@ -1,8 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; +import EngineService from '../../../../../engine/engine.service'; +import { ExperimentCreateInput } from '../../../../../experiments/models/input/experiment-create.input'; import { AppModule } from '../../../../../main/app.module'; -import { ENGINE_SERVICE } from '../../../../engine.constants'; -import { IEngineService } from '../../../../engine.interfaces'; -import { ExperimentCreateInput } from '../../../../models/experiment/input/experiment-create.input'; import { RawResult } from '../../../../models/result/raw-result.model'; import { createExperiment, @@ -15,14 +14,14 @@ import { jest.setTimeout(1000 * TIMEOUT_DURATION_SECONDS); describe('ExaremeService', () => { - let exaremeService: IEngineService; + let exaremeService: EngineService; beforeEach(async () => { const moduleRef: TestingModule = await Test.createTestingModule({ imports: [AppModule], }).compile(); - exaremeService = await moduleRef.resolve<IEngineService>(ENGINE_SERVICE); + exaremeService = await moduleRef.resolve<EngineService>(EngineService); }); const modelSlug = `ttest-idp-${generateNumber()}`; const algorithmId = 'TTEST_INDEPENDENT'; diff --git a/api/src/engine/connectors/exareme/tests/e2e/t-test-one-sample.e2e-spec.ts b/api/src/engine/connectors/exareme/tests/e2e/t-test-one-sample.e2e-spec.ts index a08e6fb3c9a4ce8332e09fc0a4b02b7cf89f2df7..c49169d6d3bb08d337fc6b6a1516f18cf6800423 100644 --- a/api/src/engine/connectors/exareme/tests/e2e/t-test-one-sample.e2e-spec.ts +++ b/api/src/engine/connectors/exareme/tests/e2e/t-test-one-sample.e2e-spec.ts @@ -1,8 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; +import EngineService from '../../../../../engine/engine.service'; +import { ExperimentCreateInput } from '../../../../../experiments/models/input/experiment-create.input'; import { AppModule } from '../../../../../main/app.module'; -import { ENGINE_SERVICE } from '../../../../engine.constants'; -import { IEngineService } from '../../../../engine.interfaces'; -import { ExperimentCreateInput } from '../../../../models/experiment/input/experiment-create.input'; import { RawResult } from '../../../../models/result/raw-result.model'; import { createExperiment, @@ -15,14 +14,14 @@ import { jest.setTimeout(1000 * TIMEOUT_DURATION_SECONDS); describe('ExaremeService', () => { - let exaremeService: IEngineService; + let exaremeService: EngineService; beforeEach(async () => { const moduleRef: TestingModule = await Test.createTestingModule({ imports: [AppModule], }).compile(); - exaremeService = await moduleRef.resolve<IEngineService>(ENGINE_SERVICE); + exaremeService = await moduleRef.resolve<EngineService>(EngineService); }); const modelSlug = `ttest-1s-${generateNumber()}`; const algorithmId = 'TTEST_ONESAMPLE'; diff --git a/api/src/engine/connectors/exareme/tests/e2e/t-test-paired.e2e-spec.ts b/api/src/engine/connectors/exareme/tests/e2e/t-test-paired.e2e-spec.ts index dd2b971e8dcb5668faa917f1b1886efb1104359b..95fa4c3fa4d74d9f0904e9f4171fc81e5e2edc32 100644 --- a/api/src/engine/connectors/exareme/tests/e2e/t-test-paired.e2e-spec.ts +++ b/api/src/engine/connectors/exareme/tests/e2e/t-test-paired.e2e-spec.ts @@ -1,8 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; +import EngineService from '../../../../../engine/engine.service'; +import { ExperimentCreateInput } from '../../../../../experiments/models/input/experiment-create.input'; import { AppModule } from '../../../../../main/app.module'; -import { ENGINE_SERVICE } from '../../../../engine.constants'; -import { IEngineService } from '../../../../engine.interfaces'; -import { ExperimentCreateInput } from '../../../../models/experiment/input/experiment-create.input'; import { RawResult } from '../../../../models/result/raw-result.model'; import { createExperiment, @@ -15,14 +14,14 @@ import { jest.setTimeout(1000 * TIMEOUT_DURATION_SECONDS); describe('ExaremeService', () => { - let exaremeService: IEngineService; + let exaremeService: EngineService; beforeEach(async () => { const moduleRef: TestingModule = await Test.createTestingModule({ imports: [AppModule], }).compile(); - exaremeService = await moduleRef.resolve<IEngineService>(ENGINE_SERVICE); + exaremeService = await moduleRef.resolve<EngineService>(EngineService); }); const modelSlug = `ttest-paired-${generateNumber()}`; const algorithmId = 'TTEST_PAIRED'; diff --git a/api/src/engine/connectors/exareme/tests/e2e/two-way-anova.e2e-spec.ts b/api/src/engine/connectors/exareme/tests/e2e/two-way-anova.e2e-spec.ts index 7b616e3266ed5ad805c74297c512c22f4c20c734..80c65495461375baf6bca3714778402480aa2b66 100644 --- a/api/src/engine/connectors/exareme/tests/e2e/two-way-anova.e2e-spec.ts +++ b/api/src/engine/connectors/exareme/tests/e2e/two-way-anova.e2e-spec.ts @@ -1,8 +1,6 @@ import { Test, TestingModule } from '@nestjs/testing'; import { AppModule } from '../../../../../main/app.module'; -import { ENGINE_SERVICE } from '../../../../engine.constants'; -import { IEngineService } from '../../../../engine.interfaces'; -import { ExperimentCreateInput } from '../../../../models/experiment/input/experiment-create.input'; +import { ExperimentCreateInput } from '../../../../../experiments/models/input/experiment-create.input'; import { RawResult } from '../../../../models/result/raw-result.model'; import { createExperiment, @@ -11,18 +9,19 @@ import { TIMEOUT_DURATION_SECONDS, waitForResult, } from '../../interfaces/test-utilities'; +import EngineService from '../../../../../engine/engine.service'; jest.setTimeout(1000 * TIMEOUT_DURATION_SECONDS); describe('ExaremeService', () => { - let exaremeService: IEngineService; + let exaremeService: EngineService; beforeEach(async () => { const moduleRef: TestingModule = await Test.createTestingModule({ imports: [AppModule], }).compile(); - exaremeService = await moduleRef.resolve<IEngineService>(ENGINE_SERVICE); + exaremeService = await moduleRef.resolve<EngineService>(EngineService); }); const modelSlug = `anova-2way-${generateNumber()}`; const algorithmId = 'ANOVA'; diff --git a/api/src/engine/connectors/exareme/transformations.ts b/api/src/engine/connectors/exareme/transformations.ts index 07e59d977343342a6686f1f71ce936ee350cd7cd..60f6a78a7b9e9598eb5495e42493787f6c28ae96 100644 --- a/api/src/engine/connectors/exareme/transformations.ts +++ b/api/src/engine/connectors/exareme/transformations.ts @@ -3,36 +3,11 @@ import * as jsonata from 'jsonata'; // old import style needed due to 'export = jsonata' -export const transformToAlgorithms = jsonata(` -( - $params := ["y", "pathology", "dataset", "filter", "x"]; - - $toArray := function($x) { $type($x) = 'array' ? $x : [$x]}; - - *.{ - 'id': name, - 'label': label, - 'description': desc, - 'parameters': $toArray(parameters[$not(name in $params)].{ - 'id': name, - 'description': desc, - 'label': label, - 'type': valueType, - 'defaultValue': defaultValue, - 'isMultiple': $boolean(valueMultiple), - 'isRequired': $boolean(valueNotBlank), - 'min': valueMin, - 'max': valueMax - }) -} -) -`); - export const transformToExperiment = jsonata(` ( $params := ["y", "pathology", "dataset", "filter", "x", "formula"]; $toArray := function($x) { $type($x) = 'array' ? $x : [$x]}; - $convDate := function($v) { $type($v) = 'string' ? $toMillis($v) : $v }; + $convDate := function($v) { $type($v) = 'string' ? $v : $fromMillis($v) }; $rp := function($v) {$replace($v, /(\\+|\\*|-)/, ',')}; $strSafe := function($v) { $type($v) = 'string' ? $v : "" }; $formula := $eval(algorithm.parameters[name = "formula"].value); @@ -47,12 +22,12 @@ export const transformToExperiment = jsonata(` "finishedAt": $convDate(finished), "shared": shared, "updateAt": $convDate(updated), - "domain": algorithm.parameters[name = "pathology"].value, - "datasets": $split(algorithm.parameters[name = "dataset"].value, ','), - "variables": $split($rp(algorithm.parameters[name = "y"].value), ','), - "coVariables": $toArray($split($rp(algorithm.parameters[name = "x"].value), ',')), - "filterVariables": (algorithm.parameters[name = "filter"].value ~> $strSafe() ~> $match(/\\"id\\":\\"(\w*)\\"/)).groups, - "filter": algorithm.parameters[name = "filter"].value, + "domain": algorithm.parameters[name = "pathology"][0].value, + "datasets": $split(algorithm.parameters[name = "dataset"][0].value, ','), + "variables": $split($rp(algorithm.parameters[name = "y"][0].value), ','), + "coVariables": $toArray($split($rp(algorithm.parameters[name = "x"][0].value), ',')), + "filterVariables": (algorithm.parameters[name = "filter"][0].value ~> $strSafe() ~> $match(/\\"id\\":\\"(\w*)\\"/)).groups, + "filter": algorithm.parameters[name = "filter"][0].value, "formula": { "transformations": $formula.single.{ "id": var_name, @@ -61,10 +36,10 @@ export const transformToExperiment = jsonata(` "interactions" : $formula.interactions.[var1, var2][] }, "algorithm": { - "id": algorithm.name, + "name": algorithm.name, "parameters" : $toArray( algorithm.parameters[$not(name in $params)].({ - "id": name, + "name": name, "label": label, "value": value }) @@ -169,7 +144,8 @@ export const dataROCToLineResult = jsonata(` "y": data.series.data.$[1], "type": 0 } - ] + ], + "hasBisector": true }) `); @@ -190,17 +166,21 @@ export const dataToHeatmap = jsonata(` ) `); +export const dataToUser = jsonata(` +$ ~> |$|{'id': subjectId}, ['subjectId']| +`); + dataToHeatmap.registerFunction( 'toMat', (a) => { const matrix = []; - a.forEach( - (elem: { y: number | number; x: number | number; value: number }) => { - matrix[elem.y] = matrix[elem.y] ?? []; - matrix[elem.y][elem.x] = elem.value; - }, - ); + a.forEach((elem: { y: number; x: number; value: number }) => { + matrix[elem.y] = matrix[elem.y] ?? []; + matrix[elem.y][elem.x] = elem.value; + }); + + matrix.reverse(); return matrix; }, diff --git a/api/src/engine/connectors/exareme/transformations/algorithms/algorithms.spec.ts b/api/src/engine/connectors/exareme/transformations/algorithms/algorithms.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..1ed945272518d566d82debaff425b3b0dc148f20 --- /dev/null +++ b/api/src/engine/connectors/exareme/transformations/algorithms/algorithms.spec.ts @@ -0,0 +1,289 @@ +import { Algorithm } from 'src/engine/models/experiment/algorithm.model'; +import { NominalParameter } from 'src/engine/models/experiment/algorithm/nominal-parameter.model'; +import transformToAlgorithms from '.'; + +describe('Algorithms', () => { + describe('when data is correct (Dummy Kaplan)', () => { + const data = [ + { + name: 'KAPLAN_MEIER', + desc: 'Kaplan-Meier Estimator for the Survival Function', + label: 'Kaplan-Meier Estimator', + type: 'python_local_global', + parameters: [ + { + name: 'y', + desc: 'A single categorical variable whose values describe a binary event.', + label: 'y', + type: 'column', + columnValuesSQLType: 'integer, text', + columnValuesIsCategorical: 'true', + value: 'alzheimerbroadcategory', + defaultValue: null, + valueType: 'string', + valueNotBlank: 'false', + valueMultiple: 'false', + valueMin: null, + valueMax: null, + valueEnumerations: null, + }, + { + name: 'x', + desc: 'A single categorical variable based on which patients are grouped.', + label: 'x', + type: 'column', + columnValuesSQLType: 'integer, text', + columnValuesIsCategorical: 'false', + value: 'apoe4', + defaultValue: null, + valueType: 'string', + valueNotBlank: 'false', + valueMultiple: 'false', + valueMin: null, + valueMax: null, + valueEnumerations: null, + }, + { + name: 'pathology', + desc: 'The name of the pathology that the dataset belongs to.', + label: 'pathology', + type: 'pathology', + columnValuesSQLType: null, + columnValuesIsCategorical: null, + value: 'dementia_longitudinal', + defaultValue: null, + valueType: 'string', + valueNotBlank: 'true', + valueMultiple: 'false', + valueMin: null, + valueMax: null, + valueEnumerations: null, + }, + { + name: 'dataset', + desc: '', + label: 'dataset', + type: 'dataset', + columnValuesSQLType: null, + columnValuesIsCategorical: null, + value: 'alzheimer_fake_cohort', + defaultValue: null, + valueType: 'string', + valueNotBlank: 'true', + valueMultiple: 'true', + valueMin: null, + valueMax: null, + valueEnumerations: null, + }, + { + name: 'filter', + desc: '', + label: 'filter', + type: 'filter', + columnValuesSQLType: null, + columnValuesIsCategorical: null, + value: '', + defaultValue: null, + valueType: 'string', + valueNotBlank: 'false', + valueMultiple: 'true', + valueMin: null, + valueMax: null, + valueEnumerations: null, + }, + { + name: 'outcome_pos', + desc: '', + label: 'Positive outcome', + type: 'other', + columnValuesSQLType: null, + columnValuesIsCategorical: null, + value: 'AD', + defaultValue: null, + valueType: 'string', + valueNotBlank: 'true', + valueMultiple: 'false', + valueMin: null, + valueMax: null, + valueEnumerations: null, + }, + { + name: 'outcome_neg', + desc: '', + label: 'Negative outcome', + type: 'other', + columnValuesSQLType: null, + columnValuesIsCategorical: null, + value: 'MCI', + defaultValue: null, + valueType: 'string', + valueNotBlank: 'true', + valueMultiple: 'false', + valueMin: null, + valueMax: null, + valueEnumerations: null, + }, + { + name: 'test', + desc: '', + label: 'Total duration of experiment in days', + type: 'other', + columnValuesSQLType: null, + columnValuesIsCategorical: null, + value: '1100', + defaultValue: null, + valueType: 'real', + valueNotBlank: 'true', + valueMultiple: 'false', + valueMin: null, + valueMax: null, + valueEnumerations: ['test', 'test2'], + }, + { + name: 'total_duration', + desc: '', + label: 'Total duration of experiment in days', + type: 'other', + columnValuesSQLType: null, + columnValuesIsCategorical: null, + value: '1100', + defaultValue: null, + valueType: 'real', + valueNotBlank: 'true', + valueMultiple: 'false', + valueMin: null, + valueMax: null, + valueEnumerations: null, + }, + ], + }, + ]; + + const algorithms: Algorithm[] = transformToAlgorithms.evaluate(data); + const kaplan = algorithms[0]; + + it('should produce one algorithm', () => { + expect(algorithms.length).toEqual(1); + }); + + it('should produce two algorithms', () => { + const algorithms: Algorithm[] = transformToAlgorithms.evaluate([ + ...data, + ...data, + ]); + expect(algorithms.length).toEqual(2); + }); + + it('variable should be nominal', () => { + expect(kaplan.variable).not.toBeUndefined(); + expect(kaplan.variable?.allowedTypes).toStrictEqual(['nominal']); + }); + + it('covariable should be integer or text', () => { + expect(kaplan.coVariable).not.toBeUndefined(); + expect(kaplan.coVariable?.allowedTypes).toStrictEqual([ + 'integer', + 'text', + ]); + }); + + it('should have 4 parameters', () => { + expect(kaplan.parameters.length).toBe(4); + }); + + it('nominal parameters should have values allowed', () => { + const nominalParam = kaplan.parameters.find( + (p) => p.name === 'test', + ) as NominalParameter; + + expect(JSON.stringify(nominalParam.allowedValues)).toEqual( + JSON.stringify([ + { + value: 'test', + label: 'test', + }, + { + value: 'test2', + label: 'test2', + }, + ]), + ); + }); + + it('should have at least one linkedTo parameter', () => { + expect(kaplan.parameters.some((param) => param['linkedTo'])); + }); + }); + + describe('when data does not contains any parameters', () => { + const data = [ + { + name: 'KAPLAN_MEIER', + desc: 'Kaplan-Meier Estimator for the Survival Function', + label: 'Kaplan-Meier Estimator', + type: 'python_local_global', + parameters: [ + { + name: 'y', + desc: 'A single categorical variable whose values describe a binary event.', + label: 'y', + type: 'column', + columnValuesSQLType: 'integer, text', + columnValuesIsCategorical: 'true', + value: 'alzheimerbroadcategory', + defaultValue: null, + valueType: 'string', + valueNotBlank: 'false', + valueMultiple: 'false', + valueMin: null, + valueMax: null, + valueEnumerations: null, + }, + { + name: 'x', + desc: 'A single categorical variable based on which patients are grouped.', + label: 'x', + type: 'column', + columnValuesSQLType: 'integer, text', + columnValuesIsCategorical: 'false', + value: 'apoe4', + defaultValue: null, + valueType: 'string', + valueNotBlank: 'false', + valueMultiple: 'false', + valueMin: null, + valueMax: null, + valueEnumerations: null, + }, + ], + }, + ]; + + const algorithms: Algorithm[] = transformToAlgorithms.evaluate(data); + const kaplan = algorithms[0]; + + it('Algo parameters should be undefined', () => { + expect(kaplan.parameters).toBeUndefined; + }); + }); + + describe('when data is empty or undefined', () => { + it('should be undefined when data is undefined', () => { + const data = undefined; + const algorithms: Algorithm[] = transformToAlgorithms.evaluate(data); + + expect(algorithms).toBeUndefined(); + }); + + it('should be undefined when data is empty', () => { + const data = ` + { + dummy: + }`; + + const algorithms: Algorithm[] = transformToAlgorithms.evaluate(data); + + expect(algorithms).toBeUndefined(); + }); + }); +}); diff --git a/api/src/engine/connectors/exareme/transformations/algorithms/index.ts b/api/src/engine/connectors/exareme/transformations/algorithms/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..f177cbfdee60baf64000ad170a520fb097560b0f --- /dev/null +++ b/api/src/engine/connectors/exareme/transformations/algorithms/index.ts @@ -0,0 +1,60 @@ +import * as jsonata from 'jsonata'; + +const transformToAlgorithms = jsonata(` +( + $dict:={ + 'integer': 'NumberParameter', + 'real': 'NumberParameter' + }; + $checkVal:= function($val) { $val ? $val : undefined}; + $excludedParams:= ['centers', 'formula']; + $includes:= ['ANOVA_ONEWAY','ANOVA','LINEAR_REGRESSION', + 'LOGISTIC_REGRESSION','TTEST_INDEPENDENT','TTEST_PAIRED', + 'PEARSON_CORRELATION','ID3','KMEANS','NAIVE_BAYES', + 'TTEST_ONESAMPLE','PCA','CALIBRATION_BELT','CART', + 'KAPLAN_MEIER','THREE_C']; + $linkedVars:= ['positive_level', 'negative_level', 'outcome_neg', 'outcome_pos']; + $linkedCoVars:= ['referencevalues', 'xlevels']; + $truthy:= function($val) {( + $v:= $lowercase($val); + $v='true' ? true : ($v='false'? false : undefined) + )}; + $extract:= function($v) { + $v? + { + "hint": $v.desc, + "isRequired": $truthy($checkVal($v.valueNotBlank)), + "hasMultiple": $truthy($checkVal($v.valueMultiple)), + "allowedTypes": $v.columnValuesIsCategorical = '' and $v.columnValuesSQLType = '' ? + undefined : $append($v.columnValuesIsCategorical = '' ? + ['nominal'] : [], $truthy($v.columnValuesIsCategorical) ? 'nominal' : $map(($checkVal($v.columnValuesSQLType) ~> $split(',')), $trim)) + } : undefined + }; + + $[name in $includes].{ + "id": name, + "label": $checkVal(label), + "type": type, + "description": $checkVal(desc), + "variable": parameters[(type='column' or type='formula') and name='y'] ~> $extract, + "coVariable": parameters[(type='column' or type='formula') and name='x'] ~> $extract, + "hasFormula": $boolean(parameters[(type='formula_description')]), + "parameters": parameters[type='other' and $not(name in $excludedParams)].{ + "__typename": $lookup($dict, valueType), + "name": name, + "label": label, + "hint": $checkVal(desc), + "defaultValue": (name in $linkedCoVars) ? "[]" : (defaultValue ? defaultValue : value), + "isRequired": $truthy(valueNotBlank), + "hasMultiple": $truthy(valueMultiple), + "isReal": valueType = 'real' ? true : undefined, + "min": $checkVal(valueMin), + "max": $checkVal(valueMax), + "allowedValues": $checkVal(valueEnumerations).{'value':$, 'label': $}[], + "linkedTo": (name in $linkedVars) ? "VARIABLE" : ((name in $linkedCoVars) ? "COVARIABLE" : undefined) + }[] + }[] +) +`); + +export default transformToAlgorithms; diff --git a/api/src/engine/connectors/local/local.connector.ts b/api/src/engine/connectors/local/local.connector.ts new file mode 100644 index 0000000000000000000000000000000000000000..e19fb48cf5f7653deac3ac1924bf99183b21df07 --- /dev/null +++ b/api/src/engine/connectors/local/local.connector.ts @@ -0,0 +1,51 @@ +import Connector from 'src/engine/interfaces/connector.interface'; +import { Domain } from 'src/engine/models/domain.model'; +import { Algorithm } from 'src/engine/models/experiment/algorithm.model'; +import { ResultUnion } from 'src/engine/models/result/common/result-union.model'; +import { User } from 'src/users/models/user.model'; + +export default class LocalConnector implements Connector { + async login(): Promise<User> { + return { + id: '1', + username: 'LocalServiceUser', + }; + } + + async getAlgorithms(): Promise<Algorithm[]> { + throw new Error('Method not implemented.'); + } + + async runExperiment(): Promise<Array<typeof ResultUnion>> { + throw new Error('Method not implemented.'); + } + + async getDomains(): Promise<Domain[]> { + return [ + { + id: 'Dummy', + label: 'Dummy', + datasets: [{ id: 'DummyDataset', label: 'DummyDataset' }], + groups: [ + { + id: 'DummyGroup', + variables: ['DummyVar'], + groups: [], + }, + ], + rootGroup: { id: 'DummyGroup' }, + variables: [{ id: 'DummyVar', type: 'string' }], + }, + ]; + } + + async getActiveUser(): Promise<User> { + return { + username: 'anonymous', + id: 'anonymousId', + fullname: 'anonymous', + email: 'anonymous@anonymous.com', + agreeNDA: true, + }; + } +} diff --git a/api/src/engine/connectors/local/main.connector.ts b/api/src/engine/connectors/local/main.connector.ts deleted file mode 100644 index 600fcc9824046a54a017b76ca27033a0b15912de..0000000000000000000000000000000000000000 --- a/api/src/engine/connectors/local/main.connector.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { IEngineService } from 'src/engine/engine.interfaces'; -import { Domain } from 'src/engine/models/domain.model'; -import { Algorithm } from 'src/engine/models/experiment/algorithm.model'; -import { - Experiment, - PartialExperiment, -} from 'src/engine/models/experiment/experiment.model'; -import { ExperimentCreateInput } from 'src/engine/models/experiment/input/experiment-create.input'; -import { ExperimentEditInput } from 'src/engine/models/experiment/input/experiment-edit.input'; -import { ListExperiments } from 'src/engine/models/experiment/list-experiments.model'; -import { User } from 'src/users/models/user.model'; - -export default class LocalService implements IEngineService { - async login(): Promise<User> { - return { - id: '1', - username: 'LocalServiceUser', - }; - } - - async getAlgorithms(): Promise<Algorithm[]> { - throw new Error('Method not implemented.'); - } - - async createExperiment( - data: ExperimentCreateInput, - isTransient: boolean, - ): Promise<Experiment> { - throw new Error('Method not implemented.'); - } - - async listExperiments(page: number, name: string): Promise<ListExperiments> { - throw new Error('Method not implemented.'); - } - - async getExperiment(id: string): Promise<Experiment> { - throw new Error('Method not implemented.'); - } - - async removeExperiment(id: string): Promise<PartialExperiment> { - throw new Error('Method not implemented.'); - } - - async editExperient( - id: string, - expriment: ExperimentEditInput, - ): Promise<Experiment> { - throw new Error('Method not implemented.'); - } - - getDomains(): Domain[] { - return [ - { - id: 'Dummy', - label: 'Dummy', - datasets: [{ id: 'DummyDataset', label: 'DummyDataset' }], - groups: [ - { - id: 'DummyGroup', - variables: ['DummyVar'], - groups: [], - }, - ], - rootGroup: { id: 'DummyGroup' }, - variables: [{ id: 'DummyVar', type: 'string' }], - }, - ]; - } - - async getActiveUser(): Promise<User> { - const dummyUser = { - username: 'anonymous', - id: 'anonymousId', - fullname: 'anonymous', - email: 'anonymous@anonymous.com', - agreeNDA: true, - }; - return dummyUser; - } - - getAlgorithmsREST(): string { - return '[]'; - } -} diff --git a/api/src/engine/engine.constants.ts b/api/src/engine/engine.constants.ts index 3c9ae327491d37456964f9ce2cba8a7f04ecccf5..351c3aa3a3258603cf674a090d61da9bd3ab0ba1 100644 --- a/api/src/engine/engine.constants.ts +++ b/api/src/engine/engine.constants.ts @@ -1,3 +1,3 @@ export const ENGINE_MODULE_OPTIONS = 'EngineModuleOption'; -export const ENGINE_SERVICE = 'EngineService'; export const ENGINE_SKIP_TOS = 'TOS_SKIP'; +export const ENGINE_ONTOLOGY_URL = 'ONTOLOGY_URL'; diff --git a/api/src/engine/engine.controller.ts b/api/src/engine/engine.controller.ts index f00a8e74803cc955fae75214d99bf561713e63cb..d47b63b20b2851715d8b58e63c90851470547fca 100644 --- a/api/src/engine/engine.controller.ts +++ b/api/src/engine/engine.controller.ts @@ -1,24 +1,17 @@ -import { Controller, Get, Inject, Req, UseInterceptors } from '@nestjs/common'; +import { Controller, Get, Req, UseInterceptors } from '@nestjs/common'; import { Request } from 'express'; import { Observable } from 'rxjs'; -import { ENGINE_SERVICE } from './engine.constants'; -import { IEngineService } from './engine.interfaces'; +import EngineService from './engine.service'; import { ErrorsInterceptor } from './interceptors/errors.interceptor'; @UseInterceptors(ErrorsInterceptor) @Controller() export class EngineController { - constructor( - @Inject(ENGINE_SERVICE) private readonly engineService: IEngineService, - ) {} - - @Get('/algorithms') - getAlgorithms(@Req() request: Request): Observable<string> | string { - return this.engineService.getAlgorithmsREST(request); - } + constructor(private readonly engineService: EngineService) {} @Get('galaxy') galaxy(@Req() request: Request): Observable<string> | string { - return this.engineService.getPassthrough?.('galaxy', request); + if (this.engineService.has('getPassthrough')) + return this.engineService.getPassthrough('galaxy', request); } } diff --git a/api/src/engine/engine.interfaces.ts b/api/src/engine/engine.interfaces.ts deleted file mode 100644 index 1a93078d70be9ded4272022f0dff5b85d2017e33..0000000000000000000000000000000000000000 --- a/api/src/engine/engine.interfaces.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { Request } from 'express'; -import { Observable } from 'rxjs'; -import { UpdateUserInput } from 'src/users/inputs/update-user.input'; -import { User } from '../users/models/user.model'; -import { Configuration } from './models/configuration.model'; -import { Domain } from './models/domain.model'; -import { Algorithm } from './models/experiment/algorithm.model'; -import { - Experiment, - PartialExperiment, -} from './models/experiment/experiment.model'; -import { ExperimentCreateInput } from './models/experiment/input/experiment-create.input'; -import { ExperimentEditInput } from './models/experiment/input/experiment-edit.input'; -import { ListExperiments } from './models/experiment/list-experiments.model'; - -export interface IEngineOptions { - type: string; - baseurl: string; -} - -export type IConfiguration = Pick<Configuration, 'contactLink' | 'hasGalaxy'>; - -export interface IEngineService { - //GraphQL - - /** - * Allow specific configuration for the engine - * - * `connectorId` is always overwrite by the engine module - */ - getConfiguration?(): IConfiguration; - - getDomains(ids: string[], req?: Request): Domain[] | Promise<Domain[]>; - - createExperiment( - data: ExperimentCreateInput, - isTransient: boolean, - req?: Request, - ): Promise<Experiment>; - - listExperiments( - page: number, - name: string, - req?: Request, - ): Promise<ListExperiments>; - - getExperiment(id: string, req?: Request): Promise<Experiment>; - - removeExperiment(id: string, req?: Request): Promise<PartialExperiment>; - - editExperient( - id: string, - expriment: ExperimentEditInput, - req?: Request, - ): Promise<Experiment>; - - getAlgorithms(req?: Request): Promise<Algorithm[]>; - - // Standard REST API call - getAlgorithmsREST(req?: Request): Observable<string> | string; - - getActiveUser?(req?: Request): Promise<User>; - - updateUser?( - req?: Request, - userId?: string, - data?: UpdateUserInput, - ): Promise<User>; - - logout?(req?: Request): Promise<void>; - - /** - * Method that login a user with username and password - * @param username - * @param password - * @returns User object or empty if user not found - */ - login?(username: string, password: string): Promise<User | undefined>; - - getPassthrough?(suffix: string, req?: Request): Observable<string> | string; -} diff --git a/api/src/engine/engine.module.ts b/api/src/engine/engine.module.ts index efdf729c1ea27eb85028aee5d0f5c4b597ed6486..6e359c9d8ba268c199bb606cdce7d7398d03758b 100644 --- a/api/src/engine/engine.module.ts +++ b/api/src/engine/engine.module.ts @@ -1,57 +1,44 @@ -import { HttpModule, HttpService } from '@nestjs/axios'; -import { DynamicModule, Global, Logger, Module } from '@nestjs/common'; -import { ENGINE_MODULE_OPTIONS, ENGINE_SERVICE } from './engine.constants'; +import { HttpModule } from '@nestjs/axios'; +import { CacheModule, DynamicModule, Module } from '@nestjs/common'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { ENGINE_MODULE_OPTIONS } from './engine.constants'; import { EngineController } from './engine.controller'; -import { IEngineOptions, IEngineService } from './engine.interfaces'; import { EngineResolver } from './engine.resolver'; +import EngineService from './engine.service'; +import EngineOptions from './interfaces/engine-options.interface'; -@Global() @Module({}) export class EngineModule { - private static readonly logger = new Logger(EngineModule.name); - - static forRoot(options?: Partial<IEngineOptions>): DynamicModule { + static forRoot(options?: Partial<EngineOptions>): DynamicModule { const optionsProvider = { provide: ENGINE_MODULE_OPTIONS, useValue: { - type: process.env.ENGINE_TYPE, - baseurl: process.env.ENGINE_BASE_URL, - ...(options ?? {}), - }, - }; - - const engineProvider = { - provide: ENGINE_SERVICE, - useFactory: async (httpService: HttpService) => { - return await this.createEngineConnection( - optionsProvider.useValue, - httpService, - ); + ...options, + type: options?.type.toLowerCase(), }, - inject: [HttpService], }; return { + global: true, module: EngineModule, - imports: [HttpModule], - providers: [optionsProvider, engineProvider, EngineResolver], + imports: [ + HttpModule, + CacheModule.registerAsync({ + imports: [ConfigModule], + useFactory: async (configService: ConfigService) => { + const config = configService.get('cache'); + return { + isGlobal: true, + ttl: config.ttl, + max: config.max, + }; + }, + inject: [ConfigService], + }), + ], + providers: [optionsProvider, EngineService, EngineResolver], controllers: [EngineController], - exports: [optionsProvider, engineProvider], + exports: [optionsProvider, EngineService], }; } - - private static async createEngineConnection( - opt: IEngineOptions, - httpService: HttpService, - ): Promise<IEngineService> { - try { - const service = await import(`./connectors/${opt.type}/main.connector`); - const engine = new service.default(opt, httpService); - - return engine; - } catch (e) { - this.logger.error(`There is a problem with the connector '${opt.type}'`); - this.logger.verbose(e); - } - } } diff --git a/api/src/engine/engine.resolver.ts b/api/src/engine/engine.resolver.ts index 6260111702f3268881409f80f5db91cc197c1cba..69718a7c0b32763ccef141f96ea53774fcf015ae 100644 --- a/api/src/engine/engine.resolver.ts +++ b/api/src/engine/engine.resolver.ts @@ -1,46 +1,42 @@ import { Inject, UseGuards, UseInterceptors } from '@nestjs/common'; -import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; +import { ConfigService } from '@nestjs/config'; +import { Args, Query, Resolver } from '@nestjs/graphql'; import { Request } from 'express'; -import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; -import { GQLRequest } from '../common/decorators/gql-request.decoractor'; +import { Public } from 'src/auth/decorators/public.decorator'; +import { GlobalAuthGuard } from 'src/auth/guards/global-auth.guard'; +import { parseToBoolean } from 'src/common/utils/shared.utils'; import { Md5 } from 'ts-md5'; +import { authConstants } from '../auth/auth-constants'; +import { GQLRequest } from '../common/decorators/gql-request.decoractor'; import { ENGINE_MODULE_OPTIONS, - ENGINE_SERVICE, + ENGINE_ONTOLOGY_URL, ENGINE_SKIP_TOS, } from './engine.constants'; -import { IEngineOptions, IEngineService } from './engine.interfaces'; +import EngineService from './engine.service'; +import { ErrorsInterceptor } from './interceptors/errors.interceptor'; +import EngineOptions from './interfaces/engine-options.interface'; import { Configuration } from './models/configuration.model'; import { Domain } from './models/domain.model'; import { Algorithm } from './models/experiment/algorithm.model'; -import { - Experiment, - PartialExperiment, -} from './models/experiment/experiment.model'; -import { ExperimentCreateInput } from './models/experiment/input/experiment-create.input'; -import { ExperimentEditInput } from './models/experiment/input/experiment-edit.input'; -import { ListExperiments } from './models/experiment/list-experiments.model'; -import { ConfigService } from '@nestjs/config'; -import { parseToBoolean } from '../common/utilities'; -import { authConstants } from '../auth/auth-constants'; -import { Public } from 'src/auth/decorators/public.decorator'; -import { ErrorsInterceptor } from './interceptors/errors.interceptor'; +import { FilterConfiguration } from './models/filter/filter-configuration'; +import { FormulaOperation } from './models/formula/formula-operation.model'; @UseInterceptors(ErrorsInterceptor) -@UseGuards(JwtAuthGuard) +@UseGuards(GlobalAuthGuard) @Resolver() export class EngineResolver { constructor( - @Inject(ENGINE_SERVICE) private readonly engineService: IEngineService, + private readonly engineService: EngineService, @Inject(ENGINE_MODULE_OPTIONS) - private readonly engineOptions: IEngineOptions, + private readonly engineOptions: EngineOptions, private readonly configSerivce: ConfigService, ) {} @Query(() => Configuration) @Public() configuration(): Configuration { - const config = this.engineService.getConfiguration?.(); + const config = this.engineService.getConfiguration(); const matomo = this.configSerivce.get('matomo'); const data = { @@ -55,6 +51,7 @@ export class EngineResolver { true, ), matomo, + ontologyUrl: this.configSerivce.get(ENGINE_ONTOLOGY_URL), }; const version = Md5.hashStr(JSON.stringify(data)); @@ -66,26 +63,8 @@ export class EngineResolver { } @Query(() => [Domain]) - async domains( - @GQLRequest() req: Request, - @Args('ids', { nullable: true, type: () => [String], defaultValue: [] }) - ids: string[], - ) { - return this.engineService.getDomains(ids, req); - } - - @Query(() => ListExperiments) - async experimentList( - @Args('page', { nullable: true, defaultValue: 0 }) page: number, - @Args('name', { nullable: true, defaultValue: '' }) name: string, - @GQLRequest() req: Request, - ) { - return this.engineService.listExperiments(page, name, req); - } - - @Query(() => Experiment) - async experiment(@Args('id') id: string, @GQLRequest() req: Request) { - return this.engineService.getExperiment(id, req); + async domains(@GQLRequest() req: Request) { + return this.engineService.getDomains(req); } @Query(() => [Algorithm]) @@ -93,34 +72,19 @@ export class EngineResolver { return this.engineService.getAlgorithms(req); } - @Mutation(() => Experiment) - async createExperiment( - @GQLRequest() req: Request, - @Args('data') experimentCreateInput: ExperimentCreateInput, - @Args('isTransient', { nullable: true, defaultValue: false }) - isTransient: boolean, - ) { - return this.engineService.createExperiment( - experimentCreateInput, - isTransient, - req, - ); - } + @Query(() => [FormulaOperation]) + async formula() { + if (this.engineService.has('getFormulaConfiguration')) + return this.engineService.getFormulaConfiguration(); - @Mutation(() => Experiment) - async editExperiment( - @GQLRequest() req: Request, - @Args('id') id: string, - @Args('data') experiment: ExperimentEditInput, - ) { - return this.engineService.editExperient(id, experiment, req); + return []; } - @Mutation(() => PartialExperiment) - async removeExperiment( - @Args('id') id: string, - @GQLRequest() req: Request, - ): Promise<PartialExperiment> { - return this.engineService.removeExperiment(id, req); + @Query(() => FilterConfiguration) + async filter() { + if (this.engineService.has('getFilterConfiguration')) + return this.engineService.getFilterConfiguration(); + + return []; } } diff --git a/api/src/engine/engine.service.ts b/api/src/engine/engine.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..97ff0b4687613a9e8eb7863ab0e4814aa19d1ce6 --- /dev/null +++ b/api/src/engine/engine.service.ts @@ -0,0 +1,249 @@ +import { HttpService } from '@nestjs/axios'; +import { + CACHE_MANAGER, + Inject, + Injectable, + InternalServerErrorException, + NotImplementedException, +} from '@nestjs/common'; +import { ConfigType } from '@nestjs/config'; +import { Cache } from 'cache-manager'; +import { Request } from 'express'; +import { Observable } from 'rxjs'; +import { ExperimentResult } from '../common/interfaces/utilities.interface'; +import cacheConfig from '../config/cache.config'; +import { ExperimentCreateInput } from '../experiments/models/input/experiment-create.input'; +import { ExperimentEditInput } from '../experiments/models/input/experiment-edit.input'; +import { UpdateUserInput } from '../users/inputs/update-user.input'; +import { User } from '../users/models/user.model'; +import { ENGINE_MODULE_OPTIONS } from './engine.constants'; +import ConnectorConfiguration from './interfaces/connector-configuration.interface'; +import Connector from './interfaces/connector.interface'; +import EngineOptions from './interfaces/engine-options.interface'; +import { Domain } from './models/domain.model'; +import { Algorithm } from './models/experiment/algorithm.model'; +import { + Experiment, + PartialExperiment, +} from './models/experiment/experiment.model'; +import { ListExperiments } from './models/experiment/list-experiments.model'; +import { FilterConfiguration } from './models/filter/filter-configuration'; +import { FormulaOperation } from './models/formula/formula-operation.model'; +import { Variable } from './models/variable.model'; + +const DOMAINS_CACHE_KEY = 'domains'; +const ALGORITHMS_CACHE_KEY = 'experiments'; +const CACHE_KEYS = [DOMAINS_CACHE_KEY, ALGORITHMS_CACHE_KEY]; + +/** + * Engine service. + * This class is used as a Proxy to the real Connector. + */ +@Injectable() +export default class EngineService implements Connector { + private connector: Connector; + + constructor( + @Inject(ENGINE_MODULE_OPTIONS) private readonly options: EngineOptions, + private readonly httpService: HttpService, + @Inject(CACHE_MANAGER) private cacheManager: Cache, + @Inject(cacheConfig.KEY) private cacheConf: ConfigType<typeof cacheConfig>, + ) { + import(`./connectors/${options.type}/${options.type}.connector`).then( + (conn) => { + const instance = new conn.default(options, httpService, this); + + if (instance.createExperiment && instance.runExperiment) + throw new InternalServerErrorException( + `Connector ${options.type} should declare either createExperiment or runExperiment not both`, + ); + + if ( + instance.createExperiment && + (!instance.getExperiment || + !instance.listExperiments || + !instance.removeExperiment || + !instance.editExperiment) + ) + throw new InternalServerErrorException( + `Connector ${options.type} has 'createExperiment' implemented it implies that getExperiment, listExperiments, removeExperiment and editExperiment methods must also be implemented.`, + ); + + this.connector = instance; + }, + ); + } + + getConfiguration(): ConnectorConfiguration { + return this.connector.getConfiguration?.() ?? {}; + } + + /** + * "If the cache is enabled, try to get the value from the cache, otherwise call the function and cache + * the result." + * + * The function takes two arguments: + * + * * `key`: The key to use for the cache. + * * `fn`: The function to call if the value is not in the cache + * @param {string} key - The key to use for the cache. + * @param fn - () => Promise<T> + * @returns The result of the function call. + */ + private async getFromCacheOrCall<T>( + key: string, + fn: () => Promise<T>, + ): Promise<T | undefined> { + if (!key || !this.cacheConf.enabled) return fn(); + + const cached = await this.cacheManager.get<T>(key); + if (cached) return cached; + + const result = await fn(); + + this.cacheManager.set(key, result, { ttl: this.cacheConf.ttl }); + + return result; + } + + async getDomains(req: Request): Promise<Domain[]> { + const user = req?.user as User; + const key = user.id ? `${DOMAINS_CACHE_KEY}-${user.id}` : undefined; + + return this.getFromCacheOrCall<Domain[]>(key, () => + this.connector.getDomains(req), + ); + } + + async getAlgorithms(req: Request): Promise<Algorithm[]> { + const user = req?.user as User; + const key = user.id ? `${ALGORITHMS_CACHE_KEY}-${user.id}` : undefined; + + return this.getFromCacheOrCall<Algorithm[]>(key, () => + this.connector.getAlgorithms(req), + ); + } + + /** + * It takes a domain ID and a list of variable IDs, and returns a list of variables that match the IDs + * @param {string} domainId - The domain ID of the domain you want to get variables from. + * @param {string[]} varIds - The list of variable IDs to get. + * @param {Request} request - The request object from the HTTP request. + * @returns An array of variables + */ + async getVariables( + domainId: string, + varIds: string[], + request: Request, + ): Promise<Variable[]> { + if (!domainId || varIds.length === 0) return []; + + const domains = await this.getDomains(request); + + return ( + domains + .find((d) => d.id === domainId) + ?.variables?.filter((v) => varIds.includes(v.id)) ?? [] + ); + } + + async createExperiment( + data: ExperimentCreateInput, + isTransient: boolean, + req?: Request, + ): Promise<Experiment> { + if (!this.connector.createExperiment) throw new NotImplementedException(); + return this.connector.createExperiment(data, isTransient, req); + } + + async runExperiment( + data: ExperimentCreateInput, + req?: Request, + ): Promise<ExperimentResult[]> { + if (!this.connector.runExperiment) throw new NotImplementedException(); + return this.connector.runExperiment(data, req); + } + + async listExperiments?( + page: number, + name: string, + req?: Request, + ): Promise<ListExperiments> { + if (!this.connector.listExperiments) throw new NotImplementedException(); + return this.connector.listExperiments(page, name, req); + } + + async getExperiment?(id: string, req?: Request): Promise<Experiment> { + if (!this.connector.getExperiment) throw new NotImplementedException(); + return this.connector.getExperiment(id, req); + } + + async removeExperiment?( + id: string, + req?: Request, + ): Promise<PartialExperiment> { + if (!this.connector.removeExperiment) throw new NotImplementedException(); + return this.connector.removeExperiment(id, req); + } + + async editExperiment?( + id: string, + data: ExperimentEditInput, + req?: Request, + ): Promise<Experiment> { + if (!this.connector.editExperiment) throw new NotImplementedException(); + return this.connector.editExperiment(id, data, req); + } + + async getActiveUser?(req?: Request): Promise<User> { + if (!this.connector.getActiveUser) throw new NotImplementedException(); + return this.connector.getActiveUser(req); + } + + async updateUser?( + req?: Request, + userId?: string, + data?: UpdateUserInput, + ): Promise<User> { + if (!this.connector.updateUser) throw new NotImplementedException(); + return this.connector.updateUser(req, userId, data); + } + + async getFormulaConfiguration?(req?: Request): Promise<FormulaOperation[]> { + if (!this.connector.getFormulaConfiguration) + throw new NotImplementedException(); + return this.connector.getFormulaConfiguration(req); + } + + async getFilterConfiguration?(req?: Request): Promise<FilterConfiguration[]> { + if (!this.connector.getFilterConfiguration) + throw new NotImplementedException(); + return this.connector.getFilterConfiguration(req); + } + + async logout?(req?: Request): Promise<void> { + const user = req?.user as User; + + if (user && user.id) + CACHE_KEYS.map((key) => `${key}-${user.id}`).forEach((key) => + this.cacheManager.del(key), + ); + + if (!this.connector.logout) throw new NotImplementedException(); + return this.connector.logout(req); + } + + async login?(username: string, password: string): Promise<User> { + if (!this.connector.login) throw new NotImplementedException(); + return this.connector.login(username, password); + } + + getPassthrough?(suffix: string, req?: Request): string | Observable<string> { + if (!this.connector.getPassthrough) throw new NotImplementedException(); + return this.connector.getPassthrough(suffix, req); + } + + has(name: keyof Connector): boolean { + return this.connector && this.connector[name] !== undefined; + } +} diff --git a/api/src/engine/interceptors/errors.interceptor.ts b/api/src/engine/interceptors/errors.interceptor.ts index 17bd2202b5ee03a82ba2c15bb2563da16f57a4ba..fa68a283ca34b0dee868c9c09e143d3c01a0109d 100644 --- a/api/src/engine/interceptors/errors.interceptor.ts +++ b/api/src/engine/interceptors/errors.interceptor.ts @@ -9,14 +9,14 @@ import { import { GqlExecutionContext } from '@nestjs/graphql'; import { catchError, Observable } from 'rxjs'; import { ENGINE_MODULE_OPTIONS } from '../engine.constants'; -import { IEngineOptions } from '../engine.interfaces'; +import EngineOptions from '../interfaces/engine-options.interface'; @Injectable() export class ErrorsInterceptor implements NestInterceptor { private readonly logger: Logger; constructor( - @Inject(ENGINE_MODULE_OPTIONS) private readonly options: IEngineOptions, + @Inject(ENGINE_MODULE_OPTIONS) private readonly options: EngineOptions, ) { // Logger name is the engine name // HttpService will be used mostly by the engine (but it's not always true) diff --git a/api/src/engine/interfaces/connector-configuration.interface.ts b/api/src/engine/interfaces/connector-configuration.interface.ts new file mode 100644 index 0000000000000000000000000000000000000000..d327ded434b169bd7c03ceda845198e0bd033cf6 --- /dev/null +++ b/api/src/engine/interfaces/connector-configuration.interface.ts @@ -0,0 +1,8 @@ +import { Configuration } from '../models/configuration.model'; + +type ConnectorConfiguration = Pick< + Configuration, + 'contactLink' | 'hasGalaxy' | 'hasGrouping' +>; + +export default ConnectorConfiguration; diff --git a/api/src/engine/interfaces/connector.interface.ts b/api/src/engine/interfaces/connector.interface.ts new file mode 100644 index 0000000000000000000000000000000000000000..66224d6b9ec898e9b1a050cfacae17a2aff493f7 --- /dev/null +++ b/api/src/engine/interfaces/connector.interface.ts @@ -0,0 +1,150 @@ +import { Request } from 'express'; +import { Observable } from 'rxjs'; +import { ExperimentResult } from 'src/common/interfaces/utilities.interface'; +import { UpdateUserInput } from 'src/users/inputs/update-user.input'; +import { ExperimentCreateInput } from '../../experiments/models/input/experiment-create.input'; +import { ExperimentEditInput } from '../../experiments/models/input/experiment-edit.input'; +import { User } from '../../users/models/user.model'; +import { Domain } from '../models/domain.model'; +import { Algorithm } from '../models/experiment/algorithm.model'; +import { + Experiment, + PartialExperiment, +} from '../models/experiment/experiment.model'; +import { ListExperiments } from '../models/experiment/list-experiments.model'; +import { FilterConfiguration } from '../models/filter/filter-configuration'; +import { FormulaOperation } from '../models/formula/formula-operation.model'; +import ConnectorConfiguration from './connector-configuration.interface'; + +export default interface Connector { + /** + * Allow specific configuration for the engine + */ + getConfiguration?(): ConnectorConfiguration; + + /** + * Get the list of domains along with a list of variables + * @param req - Request - this is the request object from the HTTP request. + */ + getDomains(req?: Request): Promise<Domain[]>; + + /** + * Create and return a full detailed experiment + * @param {ExperimentCreateInput} data - ExperimentCreateInput - this is the data that you want to + * send to the API. + * @param [isTransient=false] - If true, the experiment will be created as a transient experiment. + * @param {Request} req - Request - this is the request object from the HTTP request. + * @returns An experiment object + */ + createExperiment?( + data: ExperimentCreateInput, + isTransient: boolean, + req?: Request, + ): Promise<Experiment>; + + /** + * Run an experiment and return results (Transient only) + * @param {ExperimentCreateInput} data - ExperimentCreateInput - Data context for the experiment. + * @param {Request} req - Request - this is the request object from the HTTP request. + * @returns ResultUnion + */ + runExperiment?( + data: ExperimentCreateInput, + req?: Request, + ): Promise<ExperimentResult[]>; + + /** + * Get a list of experiment (limited to 10 per page) + * @param page - the page number + * @param name - the name of the experiment you are looking for + * @param req - Request - this is the request object from the HTTP request. + */ + listExperiments?( + page: number, + name: string, + req?: Request, + ): Promise<ListExperiments>; + + /** + * It takes an experiment id and a request object, and returns a promise of an experiment + * @param {string} id - the id of the experiment you want to get + * @param {Request} req - Request - this is the request object from the HTTP request. + * @returns An experiment object + */ + getExperiment?(id: string, req?: Request): Promise<Experiment>; + + /** + * Remove an experiment + * @param id - the id of the experiment you want to remove + * @param req - this is the request object from the user HTTP request + */ + removeExperiment?(id: string, req?: Request): Promise<PartialExperiment>; + + /** + * Update an experiment + * @param id - the id of the experiment you want to remove + * @param data - this is the data object containing the updated fields + * @param req - this is the request object from the user HTTP request + */ + editExperiment?( + id: string, + data: ExperimentEditInput, + req?: Request, + ): Promise<Experiment>; + + /** + * Retrieve the list of available algorithms + * @param req - Request - this is the request object from the HTTP request. + */ + getAlgorithms(req?: Request): Promise<Algorithm[]>; + + /** + * Get the current user logged in + * @param req - Request - this is the request object from the HTTP request. + */ + getActiveUser?(req?: Request): Promise<User>; + + /** + * Update the current logged in user + * @param req - Request - this is the request object from the HTTP request. + * @param userId - the id to update + * @param data - Data object with the updated fields + */ + updateUser?( + req?: Request, + userId?: string, + data?: UpdateUserInput, + ): Promise<User | undefined>; + + /** + * This is a method that is used to get the list of formula operations + * that are available in the engine. + * @param req - Request - Optional request object from the HTTP request + * @returns - Formula configuration + */ + getFormulaConfiguration?(req?: Request): Promise<FormulaOperation[]>; + + /** + * This is a method that is used to get the filter configuration + * that is available in the engine. + * @param req - Request - Optional request object from the HTTP request + * @returns Filter configuration + */ + getFilterConfiguration?(req?: Request): Promise<FilterConfiguration[]>; + + /** + * Perform a logout on the current logged in user + * @param req - Request - this is the request object from the HTTP request. + */ + logout?(req?: Request): Promise<void>; + + /** + * Method that login a user with username and password + * @param username + * @param password + * @returns User object or empty if user not found + */ + login?(username: string, password: string): Promise<User | undefined>; + + getPassthrough?(suffix: string, req?: Request): Observable<string> | string; +} diff --git a/api/src/engine/interfaces/engine-options.interface.ts b/api/src/engine/interfaces/engine-options.interface.ts new file mode 100644 index 0000000000000000000000000000000000000000..c05d4a0c3fe4bab5f9e9777cfba48ab928675edf --- /dev/null +++ b/api/src/engine/interfaces/engine-options.interface.ts @@ -0,0 +1,4 @@ +export default interface EngineOptions { + type: string; + baseurl: string; +} diff --git a/api/src/engine/models/category.model.ts b/api/src/engine/models/category.model.ts index 2000d8f655368b17f99c6c7eee5540b32d94b9a0..4cb4460ffb878b00883419e1b34190b107882fbe 100644 --- a/api/src/engine/models/category.model.ts +++ b/api/src/engine/models/category.model.ts @@ -1,5 +1,10 @@ -import { ObjectType } from '@nestjs/graphql'; -import { BaseModel } from './entity.model'; +import { Field, ObjectType } from '@nestjs/graphql'; @ObjectType() -export class Category extends BaseModel {} +export class Category { + @Field() + value: string; + + @Field({ nullable: true }) + label?: string; +} diff --git a/api/src/engine/models/configuration.model.ts b/api/src/engine/models/configuration.model.ts index 6ffe779cc55962c0dc0c20fd0e3d47c9e4d73348..a7aeea0f935bce28ef7e4e39a63c58fe7f1e68f9 100644 --- a/api/src/engine/models/configuration.model.ts +++ b/api/src/engine/models/configuration.model.ts @@ -8,6 +8,13 @@ export class Configuration { @Field({ nullable: true, defaultValue: false }) hasGalaxy?: boolean; + @Field({ + nullable: true, + defaultValue: false, + description: 'Indicates if histograms can handle grouping', + }) + hasGrouping?: boolean; + @Field({ nullable: true }) contactLink?: string; @@ -25,4 +32,7 @@ export class Configuration { @Field(() => Matomo, { nullable: true }) matomo?: Matomo; + + @Field({ nullable: true }) + ontologyUrl?: string; } diff --git a/api/src/engine/models/experiment/algorithm.model.ts b/api/src/engine/models/experiment/algorithm.model.ts index 818b3c9d5c131a58625b3d724185cb6d87bc7827..b697092e7d7ae4240dea01403e6ac2f12956d571 100644 --- a/api/src/engine/models/experiment/algorithm.model.ts +++ b/api/src/engine/models/experiment/algorithm.model.ts @@ -1,20 +1,34 @@ import { Field, ObjectType } from '@nestjs/graphql'; -import { AlgorithmParameter } from './algorithm-parameter.model'; +import { BaseParameter } from './algorithm/base-parameter.model'; +import { NominalParameter } from './algorithm/nominal-parameter.model'; +import { NumberParameter } from './algorithm/number-parameter.model'; +import { VariableParameter } from './algorithm/variable-parameter.model'; + +type Parameter = BaseParameter | NumberParameter | NominalParameter; @ObjectType() export class Algorithm { @Field() id: string; - @Field(() => [AlgorithmParameter], { nullable: true, defaultValue: [] }) - parameters?: AlgorithmParameter[]; + @Field(() => [BaseParameter], { nullable: true, defaultValue: [] }) + parameters?: Parameter[]; - @Field({ nullable: true }) - label?: string; + @Field(() => VariableParameter) + variable?: VariableParameter; + + @Field(() => VariableParameter, { nullable: true }) + coVariable?: VariableParameter; + + @Field({ nullable: true, defaultValue: false }) + hasFormula?: boolean; @Field({ nullable: true }) type?: string; + @Field({ nullable: true }) + label?: string; + @Field({ nullable: true }) description?: string; } diff --git a/api/src/engine/models/experiment/algorithm/base-parameter.model.ts b/api/src/engine/models/experiment/algorithm/base-parameter.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..2b60e0806b187b1c5e37ee089dc9ce1e5f6f7713 --- /dev/null +++ b/api/src/engine/models/experiment/algorithm/base-parameter.model.ts @@ -0,0 +1,47 @@ +import { Field, InterfaceType } from '@nestjs/graphql'; +import { NominalParameter } from './nominal-parameter.model'; +import { NumberParameter } from './number-parameter.model'; +import { StringParameter } from './string-parameter.model'; + +@InterfaceType({ + resolveType(param) { + if ( + param.min || + param.max || + param.isReal || + param.__typename === 'NumberParameter' + ) + return NumberParameter; + + if ( + param.allowedValues || + param.linkedTo || + param.__typename === 'NominalParameter' + ) + return NominalParameter; + + return StringParameter; + }, +}) +export abstract class BaseParameter { + @Field() + name: string; + + @Field({ nullable: true }) + label?: string; + + @Field({ + nullable: true, + description: 'Small hint (description) for the end user', + }) + hint?: string; + + @Field({ nullable: true, defaultValue: false }) + isRequired?: boolean; + + @Field({ nullable: true, defaultValue: false }) + hasMultiple?: boolean; + + @Field({ nullable: true }) + defaultValue?: string; +} diff --git a/api/src/engine/models/experiment/algorithm/nominal-parameter.model.ts b/api/src/engine/models/experiment/algorithm/nominal-parameter.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..b52f6261578adb3db68feb42a4bd90fd4103e282 --- /dev/null +++ b/api/src/engine/models/experiment/algorithm/nominal-parameter.model.ts @@ -0,0 +1,40 @@ +import { Field, ObjectType, registerEnumType } from '@nestjs/graphql'; +import { BaseParameter } from './base-parameter.model'; + +export enum AllowedLink { + VARIABLE = 'VARIABLE', + COVARIABLE = 'COVARIABLE', +} + +registerEnumType(AllowedLink, { + name: 'AllowedLink', + description: 'The supported links.', +}); + +@ObjectType() +export class OptionValue { + @Field() + value: string; + + @Field() + label: string; +} + +@ObjectType({ implements: () => [BaseParameter] }) +export class NominalParameter implements BaseParameter { + name: string; + label?: string; + hint?: string; + isRequired?: boolean; + hasMultiple?: boolean; + defaultValue?: string; + + @Field(() => AllowedLink, { + nullable: true, + description: 'Id of the parameter', + }) + linkedTo?: AllowedLink; + + @Field(() => [OptionValue], { defaultValue: [], nullable: true }) + allowedValues?: OptionValue[]; +} diff --git a/api/src/engine/models/experiment/algorithm/number-parameter.model.ts b/api/src/engine/models/experiment/algorithm/number-parameter.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..88b3604dcd4f8d2ac88bd2c422c5f67cf3d62fce --- /dev/null +++ b/api/src/engine/models/experiment/algorithm/number-parameter.model.ts @@ -0,0 +1,20 @@ +import { Field, ObjectType } from '@nestjs/graphql'; +import { BaseParameter } from './base-parameter.model'; +@ObjectType({ implements: () => [BaseParameter] }) +export class NumberParameter implements BaseParameter { + name: string; + label?: string; + hint?: string; + isRequired?: boolean; + hasMultiple?: boolean; + defaultValue?: string; + + @Field({ nullable: true }) + min?: number; + + @Field({ nullable: true }) + max?: number; + + @Field({ nullable: true, defaultValue: false }) + isReal?: boolean; +} diff --git a/api/src/engine/models/experiment/algorithm/string-parameter.model.ts b/api/src/engine/models/experiment/algorithm/string-parameter.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..d7c2e40b16f64dc36d24c1c5d7283888a2d8cee8 --- /dev/null +++ b/api/src/engine/models/experiment/algorithm/string-parameter.model.ts @@ -0,0 +1,12 @@ +import { ObjectType } from '@nestjs/graphql'; +import { BaseParameter } from './base-parameter.model'; + +@ObjectType({ implements: () => [BaseParameter] }) +export class StringParameter implements BaseParameter { + name: string; + label?: string; + hint?: string; + isRequired?: boolean; + hasMultiple?: boolean; + defaultValue?: string; +} diff --git a/api/src/engine/models/experiment/algorithm/variable-parameter.model.ts b/api/src/engine/models/experiment/algorithm/variable-parameter.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..2ef85536371278a96d16bcb377df8c7e50a4a242 --- /dev/null +++ b/api/src/engine/models/experiment/algorithm/variable-parameter.model.ts @@ -0,0 +1,19 @@ +import { Field, ObjectType } from '@nestjs/graphql'; + +@ObjectType() +export class VariableParameter { + @Field({ nullable: true }) + hint?: string; + + @Field({ nullable: true, defaultValue: false }) + isRequired?: boolean; + + @Field({ nullable: true, defaultValue: false }) + hasMultiple?: boolean; + + @Field(() => [String], { + nullable: true, + description: 'If undefined, all types are allowed', + }) + allowedTypes?: string[]; +} diff --git a/api/src/engine/models/experiment/experiment.model.ts b/api/src/engine/models/experiment/experiment.model.ts index eb757d64d9444f4c505705d3d6faa2b7e32c4d72..bffd7e8f9743716299aec5a22e4da335bdf10c60 100644 --- a/api/src/engine/models/experiment/experiment.model.ts +++ b/api/src/engine/models/experiment/experiment.model.ts @@ -1,8 +1,25 @@ -import { Field, ObjectType, PartialType } from '@nestjs/graphql'; +import { + Field, + ObjectType, + PartialType, + registerEnumType, +} from '@nestjs/graphql'; +import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; import { ResultUnion } from '../result/common/result-union.model'; -import { Algorithm } from './algorithm.model'; import { Author } from './author.model'; +export enum ExperimentStatus { + INIT = 'init', + PENDING = 'pending', + SUCCESS = 'success', + WARN = 'warn', + ERROR = 'error', +} + +registerEnumType(ExperimentStatus, { + name: 'ExperimentStatus', +}); + @ObjectType() export class Transformation { @Field({ description: "Variable's id on which to apply the transformation" }) @@ -21,61 +38,101 @@ export class Formula { interactions: string[][]; } +@ObjectType() +export class ParamValue { + @Field() + name: string; + + @Field() + value: string; +} +@ObjectType() +export class AlgorithmResult { + @Field() + name: string; + + @Field(() => [ParamValue], { nullable: true, defaultValue: [] }) + parameters?: ParamValue[]; +} + +@Entity() @ObjectType() export class Experiment { + @PrimaryGeneratedColumn('uuid') @Field() id: string; + @Column() @Field() name: string; + @Column('jsonb', { nullable: true }) @Field(() => Author, { nullable: true, defaultValue: '' }) author?: Author; + @Column() @Field({ nullable: true }) - createdAt?: number; + createdAt?: string; + @Column({ nullable: true }) @Field({ nullable: true }) - updateAt?: number; + updateAt?: string; + @Column({ nullable: true }) @Field({ nullable: true }) - finishedAt?: number; + finishedAt?: string; + @Column({ nullable: true, default: false }) @Field({ nullable: true, defaultValue: false }) viewed?: boolean; - @Field({ nullable: true }) - status?: string; + @Column({ + type: 'enum', + enum: ExperimentStatus, + default: ExperimentStatus.INIT, + }) + @Field(() => ExperimentStatus, { nullable: true }) + status?: ExperimentStatus; + @Column({ nullable: true, default: false }) @Field({ defaultValue: false }) shared?: boolean; + @Column('jsonb', { nullable: true }) @Field(() => [ResultUnion], { nullable: true, defaultValue: [] }) results?: Array<typeof ResultUnion>; + @Column('text', { array: true }) @Field(() => [String]) datasets: string[]; + @Column({ nullable: true }) @Field(() => String, { nullable: true }) filter?: string; + @Column() @Field() domain: string; + @Column('text', { array: true }) @Field(() => [String]) variables: string[]; + @Column('text', { nullable: true, array: true }) @Field(() => [String], { nullable: true, defaultValue: [] }) coVariables?: string[]; + @Column('text', { nullable: true, array: true }) @Field(() => [String], { nullable: true, defaultValue: [] }) filterVariables?: string[]; + @Column('jsonb', { nullable: true }) @Field(() => Formula, { nullable: true }) formula?: Formula; + @Column('jsonb', { nullable: true }) @Field() - algorithm: Algorithm; + algorithm: AlgorithmResult; } @ObjectType() diff --git a/api/src/engine/models/experiment/input/algorithm-parameter.input.ts b/api/src/engine/models/experiment/input/algorithm-parameter.input.ts deleted file mode 100644 index 8cd3ec29ddfecb22c1574ea339cd96b4a010f79a..0000000000000000000000000000000000000000 --- a/api/src/engine/models/experiment/input/algorithm-parameter.input.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Field, InputType, registerEnumType } from '@nestjs/graphql'; - -export enum ParamType { - STRING, - NUMBER, -} - -registerEnumType(ParamType, { - name: 'ParamType', -}); - -@InputType() -export class AlgorithmParamInput { - @Field() - id: string; - - @Field(() => ParamType, { - nullable: true, - defaultValue: ParamType.STRING, - }) - type?: ParamType; - - @Field(() => String) - value: string; -} diff --git a/api/src/engine/models/experiment/list-experiments.model.ts b/api/src/engine/models/experiment/list-experiments.model.ts index 8f8592e52f1e3d85946d373a73b4edd00ac318f5..6d00ac535b57122ed782505fbd7d17b36423da65 100644 --- a/api/src/engine/models/experiment/list-experiments.model.ts +++ b/api/src/engine/models/experiment/list-experiments.model.ts @@ -12,6 +12,6 @@ export class ListExperiments { @Field({ nullable: true }) totalExperiments?: number; - @Field(() => [Experiment]) + @Field(() => [Experiment], { nullable: true, defaultValue: [] }) experiments: Experiment[]; } diff --git a/api/src/engine/models/filter/filter-configuration.ts b/api/src/engine/models/filter/filter-configuration.ts new file mode 100644 index 0000000000000000000000000000000000000000..436057040645da932d133516a312fc3705d09ead --- /dev/null +++ b/api/src/engine/models/filter/filter-configuration.ts @@ -0,0 +1,11 @@ +import { Field, ObjectType } from '@nestjs/graphql'; + +@ObjectType() +export class FilterConfiguration { + @Field(() => [String], { + description: 'List of types that can considered as number', + defaultValue: ['real', 'integer'], + nullable: true, + }) + numberTypes?: string[]; +} diff --git a/api/src/engine/models/formula/formula-operation.model.ts b/api/src/engine/models/formula/formula-operation.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..82077516b6d9d826461e3f63795d1bc39af4397e --- /dev/null +++ b/api/src/engine/models/formula/formula-operation.model.ts @@ -0,0 +1,12 @@ +import { Field, ObjectType } from '@nestjs/graphql'; + +@ObjectType() +export class FormulaOperation { + @Field({ description: 'Type name of the variable' }) + variableType: string; + + @Field(() => [String], { + description: 'List of operation available for this type', + }) + operationTypes: string[]; +} diff --git a/api/src/engine/models/group.model.ts b/api/src/engine/models/group.model.ts index 11be6a14bd2e671a4363b742f380e6fa54470f1c..92a6c1d2f485a38f16f9d7b0c469f90b9d1fe33f 100644 --- a/api/src/engine/models/group.model.ts +++ b/api/src/engine/models/group.model.ts @@ -15,4 +15,10 @@ export class Group extends BaseModel { nullable: true, }) variables?: string[]; + + @Field(() => [String], { + description: 'List of datasets avalaible, set null if all datasets allowed', + nullable: true, + }) + datasets?: string[]; } diff --git a/api/src/engine/models/result/line-chart-result.model.ts b/api/src/engine/models/result/line-chart-result.model.ts index 85ea4ff66e9780a1cb01de9ea68621b6486e9de5..c08e590d4305a4769f48f4d6f41892ae0b76b54f 100644 --- a/api/src/engine/models/result/line-chart-result.model.ts +++ b/api/src/engine/models/result/line-chart-result.model.ts @@ -51,4 +51,7 @@ export class LineChartResult extends Result { @Field(() => [LineResult]) lines: LineResult[]; + + @Field({ nullable: true, defaultValue: false }) + hasBisector?: boolean; } diff --git a/api/src/engine/models/variable.model.ts b/api/src/engine/models/variable.model.ts index c48a32b127870e08c4fcf48e34d10716e1b1018e..7d8aae782fe87095399e25d088f4b92814cc2d66 100644 --- a/api/src/engine/models/variable.model.ts +++ b/api/src/engine/models/variable.model.ts @@ -16,4 +16,10 @@ export class Variable extends BaseModel { @Field(() => [Group], { nullable: true, defaultValue: [] }) groups?: Group[]; + + @Field(() => [String], { + description: 'List of datasets avalaible, set null if all datasets allowed', + nullable: true, + }) + datasets?: string[]; } diff --git a/api/src/engine/test/core.e2e-spec.ts b/api/src/engine/test/core.e2e-spec.ts index bb35467dd1911ea2f88c8af9f0268218ddfdb8a5..f76bd806049be71cb7d78e6f53ca915e0b0bac52 100644 --- a/api/src/engine/test/core.e2e-spec.ts +++ b/api/src/engine/test/core.e2e-spec.ts @@ -1,14 +1,13 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { Domain } from 'src/engine/models/domain.model'; +import { Domain } from '../models/domain.model'; import { AppModule } from '../../main/app.module'; import { TIMEOUT_DURATION_SECONDS } from '../connectors/exareme/interfaces/test-utilities'; -import { ENGINE_SERVICE } from '../engine.constants'; -import { IEngineService } from '../engine.interfaces'; +import EngineService from '../engine.service'; jest.setTimeout(1000 * TIMEOUT_DURATION_SECONDS); describe('Engine service', () => { - let engineService: IEngineService; + let engineService: EngineService; let domains: Domain[]; beforeAll(async () => { @@ -16,7 +15,7 @@ describe('Engine service', () => { imports: [AppModule], }).compile(); - engineService = await moduleRef.resolve<IEngineService>(ENGINE_SERVICE); + engineService = await moduleRef.resolve<EngineService>(EngineService); domains = await engineService.getDomains([]); }); diff --git a/api/src/experiments/dto/experiment-update.dto.ts b/api/src/experiments/dto/experiment-update.dto.ts new file mode 100644 index 0000000000000000000000000000000000000000..71e1db046b61b772876a8622bd5ba71f6b26530c --- /dev/null +++ b/api/src/experiments/dto/experiment-update.dto.ts @@ -0,0 +1,3 @@ +import { Experiment } from '../../engine/models/experiment/experiment.model'; + +export type ExperimentUpdateDto = Partial<Omit<Experiment, 'id' | 'author'>>; diff --git a/api/src/experiments/experiments.module.ts b/api/src/experiments/experiments.module.ts new file mode 100644 index 0000000000000000000000000000000000000000..122695790d795ca961caed42f14515aecc9b0641 --- /dev/null +++ b/api/src/experiments/experiments.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { ExperimentsService } from './experiments.service'; +import { ExperimentsResolver } from './experiments.resolver'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Experiment } from 'src/engine/models/experiment/experiment.model'; + +@Module({ + imports: [TypeOrmModule.forFeature([Experiment])], + providers: [ExperimentsService, ExperimentsResolver], +}) +export class ExperimentsModule {} diff --git a/api/src/experiments/experiments.resolver.spec.ts b/api/src/experiments/experiments.resolver.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..68907635d623d9a6b49026d5c8387490670143ba --- /dev/null +++ b/api/src/experiments/experiments.resolver.spec.ts @@ -0,0 +1,250 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import EngineService from '../engine/engine.service'; +import { ExperimentStatus } from '../engine/models/experiment/experiment.model'; +import { User } from '../users/models/user.model'; +import { ExperimentsResolver } from './experiments.resolver'; +import { ExperimentsService } from './experiments.service'; +import { ExperimentCreateInput } from './models/input/experiment-create.input'; +import { ExperimentEditInput } from './models/input/experiment-edit.input'; + +type MockEngineService = Partial<Record<keyof EngineService, jest.Mock>>; +type MockExperimentService = Partial< + Record<keyof ExperimentsService, jest.Mock> +>; + +const createEngineService = (): MockEngineService => ({ + getDomains: jest.fn(), + getAlgorithms: jest.fn(), + createExperiment: jest.fn(), + runExperiment: jest.fn(), + getExperiment: jest.fn(), + editExperiment: jest.fn(), + listExperiments: jest.fn(), + removeExperiment: jest.fn(), + has: jest.fn().mockReturnValue(true), +}); + +const createExperimentsService = (): MockExperimentService => ({ + findAll: jest.fn(), + findOne: jest.fn(), + dataToExperiment: jest.fn(), + create: jest.fn(), + update: jest.fn(), + remove: jest.fn(), +}); + +describe('ExperimentsResolver', () => { + let resolver: ExperimentsResolver; + let engineService: MockEngineService; + let experimentsService: MockExperimentService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + ExperimentsResolver, + { provide: ExperimentsService, useValue: createExperimentsService() }, + { + provide: EngineService, + useValue: createEngineService(), + }, + ], + }).compile(); + + engineService = module.get<EngineService>( + EngineService, + ) as unknown as MockEngineService; + experimentsService = module.get<ExperimentsService>( + ExperimentsService, + ) as unknown as MockExperimentService; + resolver = module.get<ExperimentsResolver>(ExperimentsResolver); + }); + + it('should be defined', () => { + expect(resolver).toBeDefined(); + }); + + describe('experimentList', () => { + describe('when engine method exist', () => { + it('should call engine method', async () => { + const request: any = jest.fn(); + engineService.listExperiments.mockReturnValue({}); + await resolver.experimentList(0, '', request); + + expect(engineService.listExperiments.mock.calls.length).toBeGreaterThan( + 0, + ); + }); + }); + + describe('when engine method does not exist', () => { + it('should call service method', async () => { + const request: any = jest.fn(); + engineService.has.mockReturnValue(false); + experimentsService.findAll.mockReturnValue([[], 9]); + await resolver.experimentList(0, '', request); + + expect(experimentsService.findAll.mock.calls.length).toBeGreaterThan(0); + }); + }); + }); + + describe('experiment', () => { + describe('when engine method exist', () => { + it('should call engine method', async () => { + const request: any = jest.fn(); + const user: User = { + id: 'dummyUser', + username: 'test', + }; + await resolver.experiment('test', request, user); + + expect(experimentsService.findOne.mock.calls.length).toBe(0); + expect(engineService.getExperiment.mock.calls.length).toBeGreaterThan( + 0, + ); + }); + }); + + describe('when engine method does not exist', () => { + it('should call service method', async () => { + const request: any = jest.fn(); + const user: User = { + id: 'dummyUser', + username: 'test', + }; + engineService.has.mockReturnValue(false); + await resolver.experiment('test', request, user); + + expect(experimentsService.findOne.mock.calls.length).toBeGreaterThan(0); + }); + }); + }); + + describe('createExperiment', () => { + describe('when engine method exist', () => { + it('should call engine method', async () => { + const request: any = jest.fn(); + const data: ExperimentCreateInput = + {} as unknown as ExperimentCreateInput; + const user: User = { + id: 'dummyUser', + username: 'test', + }; + await resolver.createExperiment(request, user, data, true); + + expect(experimentsService.create.mock.calls.length).toBe(0); + expect( + engineService.createExperiment.mock.calls.length, + ).toBeGreaterThan(0); + }); + }); + + describe('when engine method does not exist', () => { + it('should call service method', async () => { + const request: any = jest.fn(); + const data: ExperimentCreateInput = + {} as unknown as ExperimentCreateInput; + const user: User = { + id: 'dummyUser', + username: 'test', + }; + engineService.has.mockReturnValue(false); + engineService.runExperiment.mockResolvedValue([]); + experimentsService.create.mockReturnValue({ id: 'test' }); + await resolver.createExperiment(request, user, data, false); + + expect(experimentsService.create.mock.calls.length).toBeGreaterThan(0); + }); + + it('should only call runExperiment if transient', async () => { + const request: any = jest.fn(); + const data: ExperimentCreateInput = + {} as unknown as ExperimentCreateInput; + const user: User = { + id: 'dummyUser', + username: 'test', + }; + engineService.has.mockReturnValue(false); + engineService.runExperiment.mockResolvedValue([]); + experimentsService.create.mockReturnValue({ id: 'test' }); + const result = await resolver.createExperiment( + request, + user, + data, + true, + ); + + expect(engineService.runExperiment.mock.calls.length).toBeGreaterThan( + 0, + ); + expect(result.status).toBe(ExperimentStatus.SUCCESS); + }); + }); + }); + + describe('editExperiment', () => { + describe('when engine method exist', () => { + it('should call engine method', async () => { + const request: any = jest.fn(); + const data: ExperimentEditInput = {} as unknown as ExperimentEditInput; + const user: User = { + id: 'dummyUser', + username: 'test', + }; + await resolver.editExperiment(request, 'test', data, user); + + expect(experimentsService.update.mock.calls.length).toBe(0); + expect(engineService.editExperiment.mock.calls.length).toBeGreaterThan( + 0, + ); + }); + }); + + describe('when engine method does not exist', () => { + it('should call service method', async () => { + const request: any = jest.fn(); + const data: ExperimentEditInput = {} as unknown as ExperimentEditInput; + const user: User = { + id: 'dummyUser', + username: 'test', + }; + engineService.has.mockReturnValue(false); + await resolver.editExperiment(request, 'test', data, user); + + expect(experimentsService.update.mock.calls.length).toBeGreaterThan(0); + }); + }); + }); + + describe('removeExperiment', () => { + describe('when engine method exist', () => { + it('should call engine method', async () => { + const request: any = jest.fn(); + const user: User = { + id: 'dummyUser', + username: 'test', + }; + await resolver.removeExperiment('test', request, user); + + expect(experimentsService.remove.mock.calls.length).toBe(0); + expect( + engineService.removeExperiment.mock.calls.length, + ).toBeGreaterThan(0); + }); + }); + + describe('when engine method does not exist', () => { + it('should call service method', async () => { + const request: any = jest.fn(); + const user: User = { + id: 'dummyUser', + username: 'test', + }; + engineService.has.mockReturnValue(false); + await resolver.removeExperiment('test', request, user); + + expect(experimentsService.remove.mock.calls.length).toBeGreaterThan(0); + }); + }); + }); +}); diff --git a/api/src/experiments/experiments.resolver.ts b/api/src/experiments/experiments.resolver.ts new file mode 100644 index 0000000000000000000000000000000000000000..3739933dfa2c2b736f4851375afc0f272e456b3c --- /dev/null +++ b/api/src/experiments/experiments.resolver.ts @@ -0,0 +1,135 @@ +import { Logger, UseGuards } from '@nestjs/common'; +import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; +import { Request } from 'express'; +import EngineService from '../engine/engine.service'; +import { GlobalAuthGuard } from '../auth/guards/global-auth.guard'; +import { GQLRequest } from '../common/decorators/gql-request.decoractor'; +import { CurrentUser } from '../common/decorators/user.decorator'; +import { + Experiment, + ExperimentStatus, + PartialExperiment, +} from '../engine/models/experiment/experiment.model'; +import { ListExperiments } from '../engine/models/experiment/list-experiments.model'; +import { User } from '../users/models/user.model'; +import { ExperimentsService } from './experiments.service'; +import { ExperimentCreateInput } from './models/input/experiment-create.input'; +import { ExperimentEditInput } from './models/input/experiment-edit.input'; + +const LIMIT_EXP_BY_PAGE = 10; // TODO Consider refactoring to allow offset and limit in API call + +@UseGuards(GlobalAuthGuard) +@Resolver() +export class ExperimentsResolver { + private readonly logger = new Logger(ExperimentsResolver.name); + + constructor( + private readonly engineService: EngineService, + private readonly experimentService: ExperimentsService, + ) {} + + @Query(() => ListExperiments) + async experimentList( + @Args('page', { nullable: true, defaultValue: 0 }) page: number, + @Args('name', { nullable: true, defaultValue: '' }) name: string, + @GQLRequest() req: Request, + ): Promise<ListExperiments> { + if (this.engineService.has('listExperiments')) { + return this.engineService.listExperiments(page, name, req); + } + + const [results, total] = await this.experimentService.findAll( + { + limit: LIMIT_EXP_BY_PAGE, + offset: LIMIT_EXP_BY_PAGE * page, + }, + name, + ); + return { + experiments: results, + currentPage: page, + totalExperiments: total, + totalPages: Math.ceil(total / LIMIT_EXP_BY_PAGE), + }; + } + + @Query(() => Experiment) + async experiment( + @Args('id') id: string, + @GQLRequest() req: Request, + @CurrentUser() user: User, + ) { + if (this.engineService.has('getExperiment')) + return this.engineService.getExperiment(id, req); + + return this.experimentService.findOne(id, user); + } + + @Mutation(() => Experiment) + async createExperiment( + @GQLRequest() req: Request, + @CurrentUser() user: User, + @Args('data') data: ExperimentCreateInput, + @Args('isTransient', { nullable: true, defaultValue: false }) + isTransient: boolean, + ) { + if (this.engineService.has('createExperiment')) { + return this.engineService.createExperiment(data, isTransient, req); + } + + //if the experiment is transient we wait a response before returning a response + if (isTransient) { + const results = await this.engineService.runExperiment(data, req); + const expTransient = this.experimentService.dataToExperiment(data, user); + return { ...expTransient, results, status: ExperimentStatus.SUCCESS }; + } + + //if not we will create an experiment in local db + const experiment = await this.experimentService.create( + data, + user, + ExperimentStatus.PENDING, + ); + + //create an async query that will update the result when it's done + this.engineService.runExperiment(data, req).then((results) => { + this.experimentService.update( + experiment.id, + { + results, + finishedAt: new Date().toISOString(), + status: ExperimentStatus.SUCCESS, + }, + user, + ); + }); + + //we return the experiment before finishing the runExperiment + return experiment; + } + + @Mutation(() => Experiment) + async editExperiment( + @GQLRequest() req: Request, + @Args('id') id: string, + @Args('data') experiment: ExperimentEditInput, + @CurrentUser() user: User, + ) { + if (this.engineService.has('editExperiment')) + return this.engineService.editExperiment(id, experiment, req); + + return this.experimentService.update(id, experiment, user); + } + + @Mutation(() => PartialExperiment) + async removeExperiment( + @Args('id') id: string, + @GQLRequest() req: Request, + @CurrentUser() user: User, + ): Promise<PartialExperiment> { + if (this.engineService.has('removeExperiment')) + return this.engineService.removeExperiment(id, req); + + return this.experimentService.remove(id, user); + } +} diff --git a/api/src/experiments/experiments.service.spec.ts b/api/src/experiments/experiments.service.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..4ff6cbf202ce6248f3cee88498929bcf4d4bfe6a --- /dev/null +++ b/api/src/experiments/experiments.service.spec.ts @@ -0,0 +1,184 @@ +import { ForbiddenException, NotFoundException } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { User } from 'src/users/models/user.model'; +import { Repository } from 'typeorm'; +import { Experiment } from '../engine/models/experiment/experiment.model'; +import { ExperimentsService } from './experiments.service'; +import { ExperimentCreateInput } from './models/input/experiment-create.input'; + +type MockRepository<T = any> = Partial<Record<keyof Repository<T>, jest.Mock>>; +const createMockRepository = <T = any>(): MockRepository<T> => ({ + findOne: jest.fn(), + findAndCount: jest.fn(), + create: jest.fn(), + save: jest.fn(), + remove: jest.fn(), + update: jest.fn(), +}); + +describe('ExperimentsService', () => { + let service: ExperimentsService; + let experimentRepository: MockRepository; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + ExperimentsService, + { + provide: getRepositoryToken(Experiment), + useValue: createMockRepository(), + }, + ], + }).compile(); + + service = module.get<ExperimentsService>(ExperimentsService); + experimentRepository = module.get<MockRepository>( + getRepositoryToken(Experiment), + ); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('findOne', () => { + describe('when experiment with ID exists', () => { + it('should return the experiment object', async () => { + const experimentId = '1'; + const expectedExperiment = {}; + + experimentRepository.findOne.mockReturnValue(expectedExperiment); + const experiment = await service.findOne(experimentId); + expect(experiment).toEqual(expectedExperiment); + }); + it('should throw an error if user does not match ', async () => { + const experimentId = '1'; + const expectedExperiment = { + author: { + username: 'differentUsername', + }, + }; + const user: User = { + username: 'test', + id: 'dummyid', + }; + + experimentRepository.findOne.mockReturnValue(expectedExperiment); + + try { + await service.findOne(experimentId, user); + } catch (err) { + expect(err).toBeInstanceOf(ForbiddenException); + } + }); + }); + describe('otherwise', () => { + it('should throw the "NotFoundException"', async () => { + const experimentId = '1'; + experimentRepository.findOne.mockReturnValue(undefined); + + try { + await service.findOne(experimentId); + } catch (err) { + expect(err).toBeInstanceOf(NotFoundException); + expect(err.message).toEqual(`Experiment #${experimentId} not found`); + } + }); + }); + }); + + describe('findAll', () => { + describe('Should return a list of experiments', () => { + it('should return the experiment object', async () => { + const excpectedList = [{}, {}, {}]; + const expectedOutput = [excpectedList, excpectedList.length]; + + experimentRepository.findAndCount.mockReturnValue(expectedOutput); + const [experimentList, count] = await service.findAll(); + expect(count).toEqual(excpectedList.length); + expect(experimentList).toStrictEqual(excpectedList); + }); + }); + }); + + describe('create', () => { + it('Should return an experiment', async () => { + const data: ExperimentCreateInput = { + domain: 'dummyDomain', + variables: [], + algorithm: { + id: 'dummyAlgo', + parameters: [], + }, + datasets: ['dummyDataset'], + filter: '', + name: 'dummyExperiment', + }; + const user: User = { + id: 'userId', + username: 'username', + }; + + const expectedExperiment: Experiment = { + ...data, + author: { + fullname: user.username, + username: user.username, + }, + algorithm: { + name: data.algorithm.id, + parameters: [], + }, + id: 'dummyid', + }; + + experimentRepository.create.mockReturnValue(expectedExperiment); + const experiment = await experimentRepository.create(data, user); + + expect(experiment).toStrictEqual(expectedExperiment); + }); + }); + + describe('update', () => { + it('should return updated experiment', async () => { + const user: User = { + id: 'userId', + username: 'username', + }; + const expectedExperiment: Experiment = { + domain: 'dummyDomain', + variables: [], + datasets: ['dummyDataset'], + filter: '', + name: 'dummyExperiment', + author: { + fullname: user.username, + username: user.username, + }, + algorithm: { + name: 'dummyAlgo', + parameters: [], + }, + id: 'dummyid', + }; + + const updateData = { name: 'test' }; + + experimentRepository.findOne.mockReturnValue({ + author: { + ...user, + }, + }); + experimentRepository.save.mockReturnValue(expectedExperiment); + + const experiment = await service.update( + expectedExperiment.id, + updateData, + user, + ); + + expect(experiment).toStrictEqual(expectedExperiment); + }); + }); +}); diff --git a/api/src/experiments/experiments.service.ts b/api/src/experiments/experiments.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..e452228979667e98903967f5c09d181c91a0173d --- /dev/null +++ b/api/src/experiments/experiments.service.ts @@ -0,0 +1,145 @@ +import { + ForbiddenException, + Injectable, + NotFoundException, +} from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { + Experiment, + ExperimentStatus, +} from '../engine/models/experiment/experiment.model'; +import { User } from 'src/users/models/user.model'; +import { FindManyOptions, Like, Repository } from 'typeorm'; +import { ExperimentCreateInput } from './models/input/experiment-create.input'; +import { PaginationArgsInput } from './models/input/pagination-args.input'; +import { ExperimentUpdateDto } from './dto/experiment-update.dto'; + +@Injectable() +export class ExperimentsService { + constructor( + @InjectRepository(Experiment) + private readonly experimentRepository: Repository<Experiment>, + ) {} + + /** + * It takes in a pagination object and a name, and returns a promise that resolves to an array of + * experiments and the total number of experiments + * @param {PaginationArgsInput} pagination - PaginationArgsInput = {} + * @param [name] - The name of the experiment. + * @returns An array of experiments and the total count of experiments. + */ + async findAll(pagination: PaginationArgsInput = {}, name = '') { + const options: FindManyOptions<Experiment> = {}; + + if (name && name != '') { + options.where = { name: Like(`%${name}%`) }; + } + options.order = { + createdAt: 'DESC', + }; + options.skip = pagination.offset ?? 0; + options.take = pagination.limit ?? 10; + + return this.experimentRepository.findAndCount(options); + } + + /** + * It finds an experiment by its id, if the experiment does not exist throws a NotFoundException + * @param {string} id - string - the id of the experiment we want to find + * @returns The experiment object + */ + async findOne(id: string): Promise<Experiment>; + + /** + * It finds an experiment by its id, and if the user is not the author of the experiment, it throws a + * ForbiddenException + * @param {string} id - string - the id of the experiment we want to find + * @param {User} [user] - User - the user that is making the request + * @returns The experiment object + */ + async findOne(id: string, user: User): Promise<Experiment>; + async findOne(id: string, user?: User): Promise<Experiment> { + const experiment = await this.experimentRepository.findOne(id); + + if (!experiment) throw new NotFoundException(`Experiment #${id} not found`); + + if (user && experiment.author.username !== user.username) + throw new ForbiddenException( + `Experiment #${id} is not available for user ${user.username}`, + ); + + return experiment; + } + + dataToExperiment( + data: ExperimentCreateInput, + user: User, + status?: ExperimentStatus, + ): Partial<Experiment> { + return { + ...data, + status, + author: { + username: user.username, + fullname: user.fullname ?? user.username, + }, + createdAt: new Date().toISOString(), + algorithm: { + name: data.algorithm.id, + parameters: data.algorithm.parameters.map((p) => ({ + name: p.id, + value: p.value, + })), + }, + }; + } + + /** + * It creates a new experiment and saves it to the database + * @param {ExperimentCreateInput} data - ExperimentCreateInput + * @param {User} user - User - This is the user that is currently logged in. + * @returns The experiment that was created. + */ + create( + data: ExperimentCreateInput, + user: User, + status = ExperimentStatus.INIT, + ): Promise<Experiment> { + const experiment = this.experimentRepository.create( + this.dataToExperiment(data, user, status), + ); + + return this.experimentRepository.save(experiment); + } + + /** + * It finds an experiment by id and user, then updates it with the new data + * @param {string} id - The id of the experiment to update + * @param {ExperimentUpdateDto} data - ExperimentUpdateDto + * @param {User} user - User - this is the user that is currently logged in. + * @returns The updated experiment + */ + async update(id: string, data: ExperimentUpdateDto, user: User) { + const experiment = await this.findOne(id, user); + + return this.experimentRepository.save({ + ...experiment, + ...data, + id, + updateAt: new Date().toISOString(), + }); + } + + /** + * Find an experiment by id, then remove it. + * + * @param {string} id - The id of the experiment to be deleted. + * @param {User} user - User - This is the user that is currently logged in. + * @returns The experiment that was removed. + */ + async remove(id: string, user: User): Promise<Experiment> { + const experiment = await this.findOne(id, user); + + return this.experimentRepository.remove(experiment); + } +} diff --git a/api/src/experiments/models/input/algorithm-parameter.input.ts b/api/src/experiments/models/input/algorithm-parameter.input.ts new file mode 100644 index 0000000000000000000000000000000000000000..62a0ab458497bbecc9387434f741821ddab9728a --- /dev/null +++ b/api/src/experiments/models/input/algorithm-parameter.input.ts @@ -0,0 +1,10 @@ +import { Field, InputType } from '@nestjs/graphql'; + +@InputType() +export class AlgorithmParamInput { + @Field() + id: string; + + @Field(() => String) + value: string; +} diff --git a/api/src/engine/models/experiment/input/algorithm.input.ts b/api/src/experiments/models/input/algorithm.input.ts similarity index 86% rename from api/src/engine/models/experiment/input/algorithm.input.ts rename to api/src/experiments/models/input/algorithm.input.ts index d87abd4561f975dbb9dfc00964f658265a340085..e815f47f2eee1d40781187d324be32df4d279399 100644 --- a/api/src/engine/models/experiment/input/algorithm.input.ts +++ b/api/src/experiments/models/input/algorithm.input.ts @@ -9,6 +9,6 @@ export class AlgorithmInput { @Field(() => [AlgorithmParamInput], { nullable: true, defaultValue: [] }) parameters: AlgorithmParamInput[]; - @Field() - type: string; + @Field({ nullable: true }) + type?: string; } diff --git a/api/src/engine/models/experiment/input/experiment-create.input.ts b/api/src/experiments/models/input/experiment-create.input.ts similarity index 100% rename from api/src/engine/models/experiment/input/experiment-create.input.ts rename to api/src/experiments/models/input/experiment-create.input.ts diff --git a/api/src/engine/models/experiment/input/experiment-edit.input.ts b/api/src/experiments/models/input/experiment-edit.input.ts similarity index 100% rename from api/src/engine/models/experiment/input/experiment-edit.input.ts rename to api/src/experiments/models/input/experiment-edit.input.ts diff --git a/api/src/experiments/models/input/pagination-args.input.ts b/api/src/experiments/models/input/pagination-args.input.ts new file mode 100644 index 0000000000000000000000000000000000000000..541b089ff8080a78ccd7c89b06bac9997da2e5f7 --- /dev/null +++ b/api/src/experiments/models/input/pagination-args.input.ts @@ -0,0 +1,10 @@ +import { Field, InputType } from '@nestjs/graphql'; + +@InputType() +export class PaginationArgsInput { + @Field() + limit?: number; + + @Field() + offset?: number; +} diff --git a/api/src/files/files.service.ts b/api/src/files/files.service.ts index 685ab86da64b8f1ecf1ead4f11a4e8fe5701ff5d..88638a18e416889b83cb251dbd7c2ddeed31b16c 100644 --- a/api/src/files/files.service.ts +++ b/api/src/files/files.service.ts @@ -1,14 +1,14 @@ import { Inject, Injectable } from '@nestjs/common'; import * as fs from 'fs'; import { join } from 'path/posix'; +import EngineOptions from '../engine/interfaces/engine-options.interface'; import { ENGINE_MODULE_OPTIONS } from '../engine/engine.constants'; -import { IEngineOptions } from '../engine/engine.interfaces'; @Injectable() export class FilesService { constructor( @Inject(ENGINE_MODULE_OPTIONS) - private readonly engineOptions: IEngineOptions, + private readonly engineOptions: EngineOptions, ) {} /** diff --git a/api/src/main.ts b/api/src/main.ts index 6e6fe7b0003a717eba3198989da8e91eaf85afa2..7caa98df79bdb4f135a09809255b6351863cc89a 100644 --- a/api/src/main.ts +++ b/api/src/main.ts @@ -1,9 +1,14 @@ import { NestFactory } from '@nestjs/core'; import { NestExpressApplication } from '@nestjs/platform-express'; -import { AppModule } from './main/app.module'; import * as cookieParser from 'cookie-parser'; +import { getLogLevels } from './common/utils/shared.utils'; +import { AppModule } from './main/app.module'; const CORS_URL = process.env.CORS_URL ?? process.env.ENGINE_BASE_URL; +const DEFAULT_LEVEL = process.env.NODE_ENV === 'production' ? 1 : 4; +const LOG_LEVEL = process.env.LOG_LEVEL + ? parseInt(process.env.LOG_LEVEL) + : DEFAULT_LEVEL; async function bootstrap() { const app = await NestFactory.create<NestExpressApplication>(AppModule, { @@ -15,6 +20,7 @@ async function bootstrap() { CORS_URL, ], }, + logger: getLogLevels(LOG_LEVEL), }); app.use(cookieParser()); diff --git a/api/src/main/app.controller.ts b/api/src/main/app.controller.ts index 1d102bb353ccbbbcc489ad727588b44b4c06e9a1..5f072a16c21768986003df5b69fb0c7248253b82 100644 --- a/api/src/main/app.controller.ts +++ b/api/src/main/app.controller.ts @@ -5,7 +5,7 @@ import { AppService } from './app.service'; export class AppController { constructor(private readonly appService: AppService) {} - @Get() + @Get('status') getStatus(): string { return this.appService.getStatus(); } diff --git a/api/src/main/app.module.ts b/api/src/main/app.module.ts index ebc1be97cb1be3d02a48c3ec4d561280df249b03..5c113466f7233d9ff30419b48c9c495e60ae9f66 100644 --- a/api/src/main/app.module.ts +++ b/api/src/main/app.module.ts @@ -6,9 +6,11 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { GraphQLError } from 'graphql'; import { join } from 'path'; import { AuthModule } from 'src/auth/auth.module'; +import cacheConfig from 'src/config/cache.config'; import dbConfig from 'src/config/db.config'; import matomoConfig from 'src/config/matomo.config'; import { EngineModule } from 'src/engine/engine.module'; +import { ExperimentsModule } from 'src/experiments/experiments.module'; import { FilesModule } from 'src/files/files.module'; import { UsersModule } from 'src/users/users.module'; import { AppController } from './app.controller'; @@ -19,7 +21,7 @@ import { AppService } from './app.service'; ConfigModule.forRoot({ isGlobal: true, envFilePath: ['.env', '.env.defaults'], - load: [dbConfig, matomoConfig], + load: [dbConfig, matomoConfig, cacheConfig], }), GraphQLModule.forRoot<ApolloDriverConfig>({ driver: ApolloDriver, @@ -59,6 +61,7 @@ import { AppService } from './app.service'; }), AuthModule, UsersModule, + ExperimentsModule, FilesModule, ], controllers: [AppController], diff --git a/api/src/migrations/1653484967792-CreateExperimentDB.ts b/api/src/migrations/1653484967792-CreateExperimentDB.ts new file mode 100644 index 0000000000000000000000000000000000000000..12a28c10f0df12c5fd9c091a76d60a2b80826ae6 --- /dev/null +++ b/api/src/migrations/1653484967792-CreateExperimentDB.ts @@ -0,0 +1,19 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class CreateExperimentDB1653484967792 implements MigrationInterface { + name = 'CreateExperimentDB1653484967792'; + + public async up(queryRunner: QueryRunner): Promise<void> { + await queryRunner.query( + `CREATE TYPE "public"."experiment_status_enum" AS ENUM('init', 'pending', 'success', 'warn', 'error')`, + ); + await queryRunner.query( + `CREATE TABLE "experiment" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying NOT NULL, "author" jsonb, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updateAt" TIMESTAMP NOT NULL DEFAULT now(), "finishedAt" character varying, "viewed" boolean DEFAULT false, "status" "public"."experiment_status_enum" NOT NULL DEFAULT 'init', "shared" boolean DEFAULT false, "results" jsonb, "datasets" text array NOT NULL, "filter" character varying, "domain" character varying NOT NULL, "variables" text array NOT NULL, "coVariables" text array, "filterVariables" text array, "formula" jsonb, "algorithm" jsonb, CONSTRAINT "PK_4f6eec215c62eec1e0fde987caf" PRIMARY KEY ("id"))`, + ); + } + + public async down(queryRunner: QueryRunner): Promise<void> { + await queryRunner.query(`DROP TABLE "experiment"`); + await queryRunner.query(`DROP TYPE "public"."experiment_status_enum"`); + } +} diff --git a/api/src/migrations/1653487335545-ExperimentDateTypeUpdate.ts b/api/src/migrations/1653487335545-ExperimentDateTypeUpdate.ts new file mode 100644 index 0000000000000000000000000000000000000000..0ad01fbe7b20792658b7a1cf84b95450cdf28f08 --- /dev/null +++ b/api/src/migrations/1653487335545-ExperimentDateTypeUpdate.ts @@ -0,0 +1,29 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class ExperimentDateTypeUpdate1653487335545 + implements MigrationInterface +{ + name = 'ExperimentDateTypeUpdate1653487335545'; + + public async up(queryRunner: QueryRunner): Promise<void> { + await queryRunner.query(`ALTER TABLE "experiment" DROP COLUMN "createdAt"`); + await queryRunner.query( + `ALTER TABLE "experiment" ADD "createdAt" character varying NOT NULL`, + ); + await queryRunner.query(`ALTER TABLE "experiment" DROP COLUMN "updateAt"`); + await queryRunner.query( + `ALTER TABLE "experiment" ADD "updateAt" character varying`, + ); + } + + public async down(queryRunner: QueryRunner): Promise<void> { + await queryRunner.query(`ALTER TABLE "experiment" DROP COLUMN "updateAt"`); + await queryRunner.query( + `ALTER TABLE "experiment" ADD "updateAt" TIMESTAMP NOT NULL DEFAULT now()`, + ); + await queryRunner.query(`ALTER TABLE "experiment" DROP COLUMN "createdAt"`); + await queryRunner.query( + `ALTER TABLE "experiment" ADD "createdAt" TIMESTAMP NOT NULL DEFAULT now()`, + ); + } +} diff --git a/api/src/schema.gql b/api/src/schema.gql index 8f5b0feba8b4b7d1c700ddf1d9e02563d532fe83..9caebad8a51a50abc7f9e8a0bb6be9058cd2d526 100644 --- a/api/src/schema.gql +++ b/api/src/schema.gql @@ -2,6 +2,17 @@ # THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY) # ------------------------------------------------------ +interface BaseParameter { + name: String! + label: String + + """Small hint (description) for the end user""" + hint: String + isRequired: Boolean + hasMultiple: Boolean + defaultValue: String +} + type User { id: String! username: String! @@ -23,12 +34,16 @@ type Matomo { type Configuration { connectorId: String! hasGalaxy: Boolean + + """Indicates if histograms can handle grouping""" + hasGrouping: Boolean contactLink: String version: String! skipAuth: Boolean skipTos: Boolean enableSSO: Boolean matomo: Matomo + ontologyUrl: String } type Dataset { @@ -45,10 +60,13 @@ type Group { """List of variable's ids""" variables: [String!] + + """List of datasets avalaible, set null if all datasets allowed""" + datasets: [String!] } type Category { - id: String! + value: String! label: String } @@ -59,6 +77,9 @@ type Variable { description: String enumerations: [Category!] groups: [Group!] + + """List of datasets avalaible, set null if all datasets allowed""" + datasets: [String!] } type Domain { @@ -71,27 +92,90 @@ type Domain { rootGroup: Group! } -type AlgorithmParameter { - id: String! - value: String +type OptionValue { + value: String! + label: String! +} + +type NominalParameter implements BaseParameter { + name: String! label: String - description: String + + """Small hint (description) for the end user""" + hint: String + isRequired: Boolean + hasMultiple: Boolean defaultValue: String - isMultiple: Boolean + + """Id of the parameter""" + linkedTo: AllowedLink + allowedValues: [OptionValue!] +} + +"""The supported links.""" +enum AllowedLink { + VARIABLE + COVARIABLE +} + +type NumberParameter implements BaseParameter { + name: String! + label: String + + """Small hint (description) for the end user""" + hint: String isRequired: Boolean - min: String - max: String - type: String + hasMultiple: Boolean + defaultValue: String + min: Float + max: Float + isReal: Boolean +} + +type StringParameter implements BaseParameter { + name: String! + label: String + + """Small hint (description) for the end user""" + hint: String + isRequired: Boolean + hasMultiple: Boolean + defaultValue: String +} + +type VariableParameter { + hint: String + isRequired: Boolean + hasMultiple: Boolean + + """If undefined, all types are allowed""" + allowedTypes: [String!] } type Algorithm { id: String! - parameters: [AlgorithmParameter!] - label: String + parameters: [BaseParameter!] + variable: VariableParameter! + coVariable: VariableParameter + hasFormula: Boolean type: String + label: String description: String } +type FilterConfiguration { + """List of types that can considered as number""" + numberTypes: [String!] +} + +type FormulaOperation { + """Type name of the variable""" + variableType: String! + + """List of operation available for this type""" + operationTypes: [String!]! +} + type GroupResult { name: String! description: String @@ -144,6 +228,7 @@ type LineChartResult { xAxis: ChartAxis yAxis: ChartAxis lines: [LineResult!]! + hasBisector: Boolean } type BarChartResult { @@ -220,15 +305,25 @@ type Formula { interactions: [[String!]!] } +type ParamValue { + name: String! + value: String! +} + +type AlgorithmResult { + name: String! + parameters: [ParamValue!] +} + type Experiment { id: String! name: String! author: Author - createdAt: Float - updateAt: Float - finishedAt: Float + createdAt: String + updateAt: String + finishedAt: String viewed: Boolean - status: String + status: ExperimentStatus shared: Boolean! results: [ResultUnion!] datasets: [String!]! @@ -238,18 +333,26 @@ type Experiment { coVariables: [String!] filterVariables: [String!] formula: Formula - algorithm: Algorithm! + algorithm: AlgorithmResult! +} + +enum ExperimentStatus { + INIT + PENDING + SUCCESS + WARN + ERROR } type PartialExperiment { id: String name: String author: Author - createdAt: Float - updateAt: Float - finishedAt: Float + createdAt: String + updateAt: String + finishedAt: String viewed: Boolean - status: String + status: ExperimentStatus shared: Boolean results: [ResultUnion!] datasets: [String!] @@ -259,32 +362,43 @@ type PartialExperiment { coVariables: [String!] filterVariables: [String!] formula: Formula - algorithm: Algorithm + algorithm: AlgorithmResult } type ListExperiments { currentPage: Float totalPages: Float totalExperiments: Float - experiments: [Experiment!]! + experiments: [Experiment!] } type Query { configuration: Configuration! - domains(ids: [String!] = []): [Domain!]! - experimentList(page: Float = 0, name: String = ""): ListExperiments! - experiment(id: String!): Experiment! + domains: [Domain!]! algorithms: [Algorithm!]! + formula: [FormulaOperation!]! + filter: FilterConfiguration! user: User! + experimentList(page: Float = 0, name: String = ""): ListExperiments! + experiment(id: String!): Experiment! } type Mutation { - createExperiment(data: ExperimentCreateInput!, isTransient: Boolean = false): Experiment! - editExperiment(id: String!, data: ExperimentEditInput!): Experiment! - removeExperiment(id: String!): PartialExperiment! login(variables: AuthenticationInput!): AuthenticationOutput! logout: Boolean! updateUser(updateUserInput: UpdateUserInput!): User! + createExperiment(data: ExperimentCreateInput!, isTransient: Boolean = false): Experiment! + editExperiment(id: String!, data: ExperimentEditInput!): Experiment! + removeExperiment(id: String!): PartialExperiment! +} + +input AuthenticationInput { + username: String! + password: String! +} + +input UpdateUserInput { + agreeNDA: Boolean! } input ExperimentCreateInput { @@ -302,20 +416,14 @@ input ExperimentCreateInput { input AlgorithmInput { id: String! parameters: [AlgorithmParamInput!] = [] - type: String! + type: String } input AlgorithmParamInput { id: String! - type: ParamType = STRING value: String! } -enum ParamType { - STRING - NUMBER -} - input FormulaTransformation { id: String! operation: String! @@ -326,12 +434,3 @@ input ExperimentEditInput { shared: Boolean viewed: Boolean } - -input AuthenticationInput { - username: String! - password: String! -} - -input UpdateUserInput { - agreeNDA: Boolean! -} diff --git a/api/src/users/interceptors/users.interceptor.ts b/api/src/users/interceptors/users.interceptor.ts new file mode 100644 index 0000000000000000000000000000000000000000..d0fd2785ebd82d8823a3163ad7e5f10a936ea00a --- /dev/null +++ b/api/src/users/interceptors/users.interceptor.ts @@ -0,0 +1,33 @@ +import { + CallHandler, + ExecutionContext, + Injectable, + NestInterceptor, +} from '@nestjs/common'; +import { GqlExecutionContext } from '@nestjs/graphql'; +import { Observable } from 'rxjs'; +import { User } from '../models/user.model'; +import { UsersService } from '../users.service'; + +@Injectable() +export class UsersInterceptor implements NestInterceptor { + constructor(private readonly usersService: UsersService) {} + + async intercept( + context: ExecutionContext, + next: CallHandler, + ): Promise<Observable<any>> { + const ctx = GqlExecutionContext.create(context); + const req = ctx.getContext().req ?? ctx.switchToHttp().getRequest(); + + if (req.userExtended) return next.handle(); // user already extended + req.userExtended = true; + + const user: User = req.user; + if (user && user.id) { + await this.usersService.extendedUser(user); + } + + return next.handle(); + } +} diff --git a/api/src/users/users.module.ts b/api/src/users/users.module.ts index 86e30c7f355ff7f1c3e0c3beb3f4f4bbcdbe7296..b5374872ed6f2336185bc5d10c5f9c680b473507 100644 --- a/api/src/users/users.module.ts +++ b/api/src/users/users.module.ts @@ -1,11 +1,20 @@ import { Module } from '@nestjs/common'; +import { APP_INTERCEPTOR } from '@nestjs/core'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { UsersInterceptor } from './interceptors/users.interceptor'; import { User } from './models/user.model'; import { UsersResolver } from './users.resolver'; import { UsersService } from './users.service'; @Module({ imports: [TypeOrmModule.forFeature([User])], - providers: [UsersResolver, UsersService], + providers: [ + UsersResolver, + UsersService, + { + provide: APP_INTERCEPTOR, + useClass: UsersInterceptor, + }, + ], }) export class UsersModule {} diff --git a/api/src/users/users.resolver.spec.ts b/api/src/users/users.resolver.spec.ts index e449f71fb0900a50fc53711d934d8be13fdcc4d1..6f8fe9d584c1ea856cbc22c2090d899161c0a65a 100644 --- a/api/src/users/users.resolver.spec.ts +++ b/api/src/users/users.resolver.spec.ts @@ -1,18 +1,29 @@ import { getMockReq } from '@jest-mock/express'; -import { NotFoundException } from '@nestjs/common'; +import { InternalServerErrorException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { MockFunctionMetadata, ModuleMocker } from 'jest-mock'; -import { ENGINE_SERVICE } from '../engine/engine.constants'; +import EngineService from '../engine/engine.service'; import { UpdateUserInput } from './inputs/update-user.input'; import { User } from './models/user.model'; import { UsersResolver } from './users.resolver'; -import { InternalUser, UsersService } from './users.service'; +import { UsersService } from './users.service'; -const moduleMocker = new ModuleMocker(global); +type MockEngineService = Partial<Record<keyof EngineService, jest.Mock>>; +type MockUsersService = Partial<Record<keyof UsersService, jest.Mock>>; + +const createEngineService = (): MockEngineService => ({ + updateUser: jest.fn(), + has: jest.fn().mockReturnValue(true), +}); + +const createUsersService = (): MockUsersService => ({ + update: jest.fn(), +}); describe('UsersResolver', () => { let resolver: UsersResolver; const req = getMockReq(); + let engineService: MockEngineService; + let usersService: MockUsersService; const user: User = { id: 'guest', @@ -20,108 +31,85 @@ describe('UsersResolver', () => { fullname: 'This is la Peste', }; - const updateData: UpdateUserInput = { - agreeNDA: true, - }; - - const internUser: InternalUser = { - id: 'guest', - agreeNDA: false, - }; - - const internUserWrong: InternalUser = { - id: 'guest1', - agreeNDA: false, - }; - - const findOne = jest - .fn() - .mockResolvedValueOnce(internUserWrong) - .mockResolvedValueOnce(internUserWrong) - .mockImplementationOnce(() => { - throw new NotFoundException(); - }) - .mockResolvedValue(internUser); - - const getActiveUser = jest - .fn() - .mockResolvedValueOnce(user) - .mockResolvedValueOnce({}) - .mockResolvedValue(user); - - const engineService = { - getActiveUser, - updateUser: jest.fn().mockResolvedValue({ ...user, ...updateData }), - }; - beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [UsersResolver], - }) - .useMocker((token) => { - if (token == UsersService) { - return { - findOne, - update: jest.fn().mockResolvedValue({ ...user, ...internUser }), - }; - } - if (token == ENGINE_SERVICE) { - return engineService; - } - if (typeof token === 'function') { - const mockMetadata = moduleMocker.getMetadata( - token, - ) as MockFunctionMetadata<any, any>; - const Mock = moduleMocker.generateFromMetadata(mockMetadata); - return new Mock(); - } - }) - .compile(); + providers: [ + UsersResolver, + { provide: UsersService, useValue: createUsersService() }, + { + provide: EngineService, + useValue: createEngineService(), + }, + ], + }).compile(); resolver = module.get<UsersResolver>(UsersResolver); + engineService = module.get<EngineService>( + EngineService, + ) as unknown as MockEngineService; + usersService = module.get<UsersService>( + UsersService, + ) as unknown as MockUsersService; }); - it('Get user with different id from engine and database', async () => { - expect(await resolver.getUser(req, user)).toStrictEqual({ - ...user, + describe('getUser', () => { + it('Get simple user', async () => { + const excpectedUser: User = { + id: 'guest', + username: 'guest', + fullname: 'This is la Peste', + }; + const result = await resolver.getUser(excpectedUser); + expect(result).toStrictEqual(excpectedUser); }); - }); - it('Get user incomplete merge', async () => { - expect(resolver.getUser(req, user)).rejects.toThrowError(); + it('Undefined user should throw an InternalServerErrorException', async () => { + try { + await resolver.getUser(undefined); + } catch (err) { + expect(err).toBeInstanceOf(InternalServerErrorException); + } + }); }); - it('Get user not found in db', async () => { - expect(await resolver.getUser(req, user)).toStrictEqual(user); - }); + describe('updateUser', () => { + it('Update user from engine ', async () => { + const updateData: UpdateUserInput = { + agreeNDA: true, + }; + const expectedUser = { + ...user, + ...updateData, + }; + + engineService.updateUser.mockReturnValue(expectedUser); + const result = await resolver.updateUser(req, updateData, user); - it('Get user in engine and database (merge)', async () => { - expect(await resolver.getUser(req, user)).toStrictEqual({ - ...user, - ...internUser, + expect(result).toStrictEqual(expectedUser); }); - }); - it('Undefined user should not throw exception', async () => { - expect(await resolver.getUser(req, undefined)).toBeTruthy(); - }); + it('Update user from database', async () => { + const updateData: UpdateUserInput = { + agreeNDA: true, + }; + const expectedUser = { + ...user, + ...updateData, + }; - it('Update user from engine ', async () => { - expect(await resolver.updateUser(req, updateData, user)).toStrictEqual({ - ...user, - ...updateData, - }); - }); + engineService.has.mockReturnValue(false); + usersService.update.mockReturnValue(expectedUser); + const result = await resolver.updateUser(req, updateData, user); - it('Update user from database', async () => { - engineService.updateUser = undefined; - expect(await resolver.updateUser(req, updateData, user)).toStrictEqual({ - ...user, - ...internUser, + expect(result).toStrictEqual(expectedUser); }); - }); - it('Undefined user should not throw exception', async () => { - expect(await resolver.updateUser(req, updateData, user)).toBeTruthy(); + it('Undefined user should throw an exception', async () => { + try { + await resolver.updateUser(req, {}); + } catch (err) { + expect(err).toBeInstanceOf(InternalServerErrorException); + } + }); }); }); diff --git a/api/src/users/users.resolver.ts b/api/src/users/users.resolver.ts index 0b78a0fb5af9b0dfb7de5a8d727e418954697de0..d13ae670e9499d07b768cd3706ff1173f3de4135 100644 --- a/api/src/users/users.resolver.ts +++ b/api/src/users/users.resolver.ts @@ -1,65 +1,39 @@ import { - Inject, InternalServerErrorException, Logger, UseGuards, } from '@nestjs/common'; import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; -import { ENGINE_SERVICE } from '../engine/engine.constants'; -import { IEngineService } from '../engine/engine.interfaces'; -import { CurrentUser } from '../auth/decorators/user.decorator'; -import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; +import { Request } from 'express'; +import { GlobalAuthGuard } from '../auth/guards/global-auth.guard'; +import { GQLRequest } from '../common/decorators/gql-request.decoractor'; +import { CurrentUser } from '../common/decorators/user.decorator'; +import EngineService from '../engine/engine.service'; import { UpdateUserInput } from './inputs/update-user.input'; import { User } from './models/user.model'; import { UsersService } from './users.service'; -import { GQLRequest } from '../common/decorators/gql-request.decoractor'; -import { Request } from 'express'; -@UseGuards(JwtAuthGuard) +@UseGuards(GlobalAuthGuard) @Resolver() export class UsersResolver { private readonly logger = new Logger(UsersResolver.name); constructor( private readonly usersService: UsersService, - @Inject(ENGINE_SERVICE) private readonly engineService: IEngineService, + private readonly engineService: EngineService, ) {} @Query(() => User, { name: 'user' }) /** - * It returns the user from the engine, if it exists. Same from the internal database - * merge internal object over engine one to have a final user. - * @param {Request} request - Request - * @param {User} reqUser - The user that is currently logged in. - * @returns A user object. + * Return the user object + * @param {User} user - User - This is the user object that is passed in from the decorator. + * @returns The user object */ - async getUser(@GQLRequest() request: Request, @CurrentUser() reqUser: User) { - const user: Partial<User> = {}; - - if (this.engineService.getActiveUser) { - const engineUser = await this.engineService.getActiveUser(request); - if (engineUser) Object.assign(user, engineUser); - } - - // Checking if the user exists in the internal database. If it does, it will assign the user to the `user` object. - try { - const internalUser = reqUser - ? await this.usersService.findOne(reqUser.id) - : undefined; - - if (internalUser && (!user.id || internalUser.id === user.id)) { - Object.assign(user, internalUser); - } - } catch (e) { - this.logger.verbose(e); - } - - if (!user.id || !user.username) - throw new InternalServerErrorException( - 'The user cannot be construct from the engine', - ); + async getUser(@CurrentUser() user: User) { + if (!user || !user.id || !user.username) + throw new InternalServerErrorException('User cannot be retrieve'); - return user as User; + return user; } /** @@ -75,15 +49,27 @@ export class UsersResolver { @Args('updateUserInput') updateUserInput: UpdateUserInput, @CurrentUser() user?: User, ) { - if (this.engineService.updateUser) - return await this.engineService.updateUser( + if (!user || !user.id || !user.username) + throw new InternalServerErrorException('User cannot be retrieve'); + + let updatedInfo: Partial<User>; + + if (this.engineService.has('updateUser')) { + updatedInfo = await this.engineService.updateUser( request, user?.id, updateUserInput, ); + } else { + const internalUser = await this.usersService.update( + user.id, + updateUserInput, + ); + if (internalUser) Object.assign(user, internalUser); + } - await this.usersService.update(user.id, updateUserInput); + if (updatedInfo) Object.assign(user, updatedInfo); - return await this.getUser(request, user); + return user; } } diff --git a/api/src/users/users.service.spec.ts b/api/src/users/users.service.spec.ts index f75bffee9617e6e0f29804d72e8f1cf199c7e684..aaad4fb993600d43786cf2b407b963715dea0b32 100644 --- a/api/src/users/users.service.spec.ts +++ b/api/src/users/users.service.spec.ts @@ -1,14 +1,21 @@ +import { NotFoundException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; -import { MockFunctionMetadata, ModuleMocker } from 'jest-mock'; +import { Repository } from 'typeorm'; import { UpdateUserInput } from './inputs/update-user.input'; import { User } from './models/user.model'; import { UsersService } from './users.service'; -const moduleMocker = new ModuleMocker(global); +type MockRepository<T = any> = Partial<Record<keyof Repository<T>, jest.Mock>>; + +const createMockRepository = <T = any>(): MockRepository<T> => ({ + findOne: jest.fn(), + save: jest.fn(), +}); describe('UsersService', () => { let service: UsersService; + let usersRepository: MockRepository; const user: User = { id: 'guest', username: 'guest', @@ -21,40 +28,70 @@ describe('UsersService', () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [UsersService], - }) - .useMocker((token) => { - if (token === getRepositoryToken(User)) { - return { - findOne: jest - .fn() - .mockResolvedValue(user) - .mockResolvedValueOnce(undefined), - save: jest.fn().mockResolvedValue({ ...user, ...updateData }), //todo - }; - } - if (typeof token === 'function') { - const mockMetadata = moduleMocker.getMetadata( - token, - ) as MockFunctionMetadata<any, any>; - const Mock = moduleMocker.generateFromMetadata(mockMetadata); - return new Mock(); - } - }) - .compile(); + providers: [ + UsersService, + { + provide: getRepositoryToken(User), + useValue: createMockRepository<User>(), + }, + ], + }).compile(); service = module.get<UsersService>(UsersService); + usersRepository = module.get<MockRepository>(getRepositoryToken(User)); }); - it('getUser', async () => { - expect(service.findOne('IdThatDoesNotExist')).rejects.toThrow(); - expect(await service.findOne('idThatExist')).toBe(user); + describe('getUser', () => { + describe('when user exist', () => { + it('Should return a user', async () => { + usersRepository.findOne.mockReturnValue(user); + const result = await service.findOne('idThatExist'); + + expect(result).toStrictEqual(user); + }); + }); + + describe('otherwise', () => { + it('Should return a NotFoundException', async () => { + usersRepository.findOne.mockReturnValue(undefined); + + try { + await service.findOne('IdThatDoesNotExist'); + } catch (err) { + expect(err).toBeInstanceOf(NotFoundException); + } + }); + }); }); - it('updateUser', async () => { - expect(await service.update('idThatExist', updateData)).toStrictEqual({ - ...user, - ...updateData, + describe('updateUser', () => { + it('should return an updated user', async () => { + const expectedUser = { ...user, ...updateData }; + usersRepository.save.mockResolvedValue(expectedUser); + + const result = await service.update('idThatExist', updateData); + + expect(result).toStrictEqual(expectedUser); + }); + }); + + describe('extendedUser', () => { + it('should return an extended user', async () => { + const localUser: User = { + id: 'dummyId', + username: 'dummyUsername', + }; + + const expectedUser = { + ...localUser, + agreeNDA: true, + }; + + usersRepository.findOne.mockReturnValue(expectedUser); + + await service.extendedUser(localUser); + + expect(localUser).toStrictEqual(expectedUser); }); }); }); diff --git a/api/src/users/users.service.ts b/api/src/users/users.service.ts index d74ca14ce1d5096305d790d189eb6666b032733b..3138f9a5e748bb329b154d9ac3e8f9319213066f 100644 --- a/api/src/users/users.service.ts +++ b/api/src/users/users.service.ts @@ -1,4 +1,4 @@ -import { Injectable, NotFoundException } from '@nestjs/common'; +import { Injectable, Logger, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { UpdateUserInput } from './inputs/update-user.input'; @@ -13,6 +13,8 @@ export class UsersService { private readonly userRepository: Repository<InternalUser>, ) {} + private readonly logger = new Logger(UsersService.name); + /** * Get a user by id * @param {string} id - The id of the user to be retrieved. @@ -38,6 +40,28 @@ export class UsersService { ...data, }; - return await this.userRepository.save(updateData); + return this.userRepository.save(updateData); + } + + /** + * It takes a user object, checks if it has an id, and if it does, it tries to find the user in the + * database and then merges the database user with the user object + * @param {User} user - User - The user object that is being extended. + */ + async extendedUser(user: User) { + if (!user || !user.id) { + return; + } + + try { + const dbUser = await this.findOne(user.id); + + Object.assign(user, dbUser); + } catch (err) { + if (err instanceof NotFoundException) + this.logger.debug( + `Extension of ${user.id} not needed, user not found in database`, + ); + } } } diff --git a/api/tsconfig.json b/api/tsconfig.json index adb614cab7d04d6f2391c7663b833b296ed334e1..0903174b9e77d1331521c7bc2db68acfd4217691 100644 --- a/api/tsconfig.json +++ b/api/tsconfig.json @@ -6,7 +6,7 @@ "emitDecoratorMetadata": true, "experimentalDecorators": true, "allowSyntheticDefaultImports": true, - "target": "es2017", + "target": "es2021", "sourceMap": true, "outDir": "./dist", "baseUrl": "./", diff --git a/docs/for-developers/configuration/gateway.md b/docs/for-developers/configuration/gateway.md index 2b16dacfcbdfbc415f9091c3086a000032787d21..7f879ca3369f383b0f2b26fbacc31d35d889258b 100644 --- a/docs/for-developers/configuration/gateway.md +++ b/docs/for-developers/configuration/gateway.md @@ -18,6 +18,7 @@ description: >- | GATEWAY\_PORT | number | 8081 | Indicate the port that should be used by the gateway | | NODE\_ENV | string | dev | Value can be `prod` or `dev` | | BASE\_URL\_CONTEXT | string | null | Define context of the gateway. E.g. `api` if the api is under `http://127.0.0.1/api/` | +| ONTOLOGY\_URL | string | null | Define ontology's url | #### Authentication